728x90
안녕하세요, fellow 개발자 여러분! 오늘은 GreenSock Animation Platform(GSAP)과 Vue.js를 조합하여 웹에서 눈길을 사로잡는 화려한 애니메이션을 구현하는 방법을 소개하려고 합니다. GSAP의 강력한 애니메이션 기능과 Vue의 반응형 프레임워크가 만나면 어떤 마법이 펼쳐질까요? 바로 실습을 통해 확인해보겠습니다!
예제: 스크롤에 반응하는 화려한 카드 플립 애니메이션
이 예제에서는 사용자가 스크롤할 때마다 카드가 뒤집히며 내용을 드러내는 애니메이션을 만들어 보겠습니다. GSAP의 ScrollTrigger와 Vue의 컴포넌트 시스템을 활용해 부드럽고 매력적인 효과를 구현할게요.
1. 프로젝트 설정
먼저 Vue 3 프로젝트를 설정하고 GSAP를 설치합니다. 터미널에서 다음 명령어를 실행하세요:
npm install vue
npm install gsap
GSAP와 ScrollTrigger를 사용할 것이므로, GSAP를 등록해야 합니다. 메인 파일(main.js)에 다음 코드를 추가하세요:
import { createApp } from 'vue';
import App from './App.vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
createApp(App).mount('#app');
2. 컴포넌트 작성
이제 App.vue 파일에서 카드 플립 애니메이션을 구현해 봅시다. 아래는 전체 코드입니다:
<template>
<div class="container">
<div v-for="(card, index) in cards" :key="index" class="card" ref="cardRefs">
<div class="card-inner">
<div class="card-front">
<h2>{{ card.title }}</h2>
</div>
<div class="card-back">
<p>{{ card.content }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { gsap } from 'gsap';
export default {
name: 'CardFlipAnimation',
setup() {
const cards = ref([
{ title: 'Card 1', content: '이 카드는 GSAP의 마법을 보여줍니다!' },
{ title: 'Card 2', content: 'Vue와 함께라면 더 강력해요.' },
{ title: 'Card 3', content: '스크롤로 생동감을 불어넣어 보세요!' },
]);
const cardRefs = ref([]);
onMounted(() => {
cardRefs.value.forEach((card, index) => {
const inner = card.querySelector('.card-inner');
gsap.fromTo(
inner,
{ rotationY: 0 },
{
rotationY: 180,
duration: 1,
ease: 'power2.inOut',
scrollTrigger: {
trigger: card,
start: 'top 80%',
end: 'top 20%',
scrub: true,
markers: false, // 디버깅용 마커, 필요 시 true로 변경
},
}
);
});
});
return { cards, cardRefs };
},
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 50px;
padding: 100px 20px;
min-height: 100vh;
}
.card {
perspective: 1000px;
width: 300px;
height: 200px;
margin: 0 auto;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.card-front {
background-color: #3498db;
color: white;
}
.card-back {
background-color: #e74c3c;
color: white;
transform: rotateY(180deg);
}
</style>
3. 코드 설명
-
HTML 구조: 각 카드는 앞면(card-front)과 뒷면(card-back)을 가진 3D 플립 효과를 위해 card-inner로 감싸져 있습니다. v-for를 사용해 동적으로 카드를 렌더링합니다.
-
GSAP 애니메이션: gsap.fromTo를 사용해 카드가 rotationY 0도에서 180도로 회전하며 뒤집히도록 설정했습니다. ScrollTrigger는 스크롤 위치에 따라 애니메이션을 부드럽게 실행합니다.
-
Vue의 ref: cardRefs를 사용해 DOM 요소에 접근하며, onMounted 훅에서 애니메이션을 초기화합니다.
-
스타일: perspective와 transform-style: preserve-3d를 활용해 3D 효과를 구현했습니다.
4. 결과물
브라우저에서 실행하면 스크롤할 때마다 카드가 부드럽게 뒤집히며 앞면에서 뒷면으로 전환됩니다. 색상과 그림자 효과로 화려함을 더했죠! 필요하다면 ease 속성을 bounce나 elastic으로 바꿔 더 역동적인 느낌을 줄 수도 있습니다.
더 나아가기
-
타임라인 추가: GSAP의 Timeline을 사용해 카드들이 순차적으로 뒤집히도록 설정해보세요.
-
인터랙션 강화: 마우스 호버 시 약간의 흔들림 효과를 추가하거나, 클릭 시 다른 애니메이션을 트리거할 수 있습니다.
-
반응형 디자인: 카드 크기를 뷰포트에 맞게 조정해 모바일에서도 멋지게 보이도록 해보세요.
GSAP Timeline으로 순차적인 카드 플립 애니메이션 구현하기
안녕하세요! 이전에 GSAP와 Vue.js로 스크롤에 반응하는 카드 플립 애니메이션을 만들어보았죠. 이번에는 요청하신 대로 GSAP의 Timeline을 활용해 카드들이 순차적으로 뒤집히는 효과를 추가해 보겠습니다. 스크롤 트리거와 함께 타임라인을 사용하면 각 카드가 자연스럽고 순서대로 애니메이션되는 멋진 결과를 얻을 수 있어요. 바로 시작해봅시다!
수정된 코드
아래는 Timeline을 적용한 App.vue의 전체 코드입니다. 이전 예제에서 타임라인을 추가하고, 각 카드가 순차적으로 뒤집히도록 조정했습니다.
<template>
<div class="container">
<div v-for="(card, index) in cards" :key="index" class="card" ref="cardRefs">
<div class="card-inner">
<div class="card-front">
<h2>{{ card.title }}</h2>
</div>
<div class="card-back">
<p>{{ card.content }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export default {
name: 'CardFlipAnimation',
setup() {
const cards = ref([
{ title: 'Card 1', content: '첫 번째 카드가 뒤집힙니다!' },
{ title: 'Card 2', content: '두 번째는 살짝 늦게요.' },
{ title: 'Card 3', content: '마지막으로 화려하게!' },
]);
const cardRefs = ref([]);
onMounted(() => {
// GSAP Timeline 생성
const tl = gsap.timeline({
scrollTrigger: {
trigger: cardRefs.value[0], // 첫 번째 카드를 트리거로 설정
start: 'top 80%',
end: 'bottom 20%',
scrub: 1, // 스크롤에 따라 부드럽게 진행
markers: false, // 디버깅용 마커 (필요 시 true)
},
});
// 각 카드를 순차적으로 뒤집기
cardRefs.value.forEach((card, index) => {
const inner = card.querySelector('.card-inner');
tl.fromTo(
inner,
{ rotationY: 0 },
{
rotationY: 180,
duration: 1,
ease: 'power2.inOut',
delay: index * 0.3, // 각 카드마다 0.3초 지연
},
0 // 타임라인의 시작점에서 동시에 실행되도록 설정
);
});
});
return { cards, cardRefs };
},
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 50px;
padding: 100px 20px;
min-height: 100vh;
}
.card {
perspective: 1000px;
width: 300px;
height: 200px;
margin: 0 auto;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.card-front {
background-color: #3498db;
color: white;
}
.card-back {
background-color: #e74c3c;
color: white;
transform: rotateY(180deg);
}
</style>
주요 변경 사항 설명
-
GSAP Timeline 추가:
-
gsap.timeline()을 생성하여 모든 카드 애니메이션을 하나의 타임라인으로 관리합니다.
-
scrollTrigger를 타임라인에 직접 연결해 스크롤에 따라 전체 애니메이션이 진행되도록 설정했습니다.
-
-
순차적인 뒤집기:
-
tl.fromTo() 호출에서 delay: index * 0.3를 추가해 각 카드가 0.3초 간격으로 순차적으로 뒤집히게 했습니다.
-
0 인자를 마지막에 추가해 타임라인의 시작점에서 모든 애니메이션이 동시에 시작되지만, delay로 순차 효과를 구현했습니다.
-
-
ScrollTrigger 설정:
-
트리거를 첫 번째 카드(cardRefs.value[0])로 설정하여 컨테이너 전체가 아닌 첫 카드의 위치를 기준으로 스크롤 애니메이션이 시작되도록 했습니다.
-
scrub: 1로 설정해 스크롤 속도에 따라 애니메이션이 부드럽게 반응하도록 조정했습니다.
-
결과물
이 코드를 실행하면 스크롤을 내릴 때 첫 번째 카드가 먼저 뒤집히고, 0.3초 뒤 두 번째 카드, 다시 0.3초 뒤 세 번째 카드가 순차적으로 뒤집힙니다. 스크롤을 올리면 역순으로 원래 상태로 돌아가며, scrub 덕분에 부드럽고 자연스러운 전환이 가능해요. 색상과 그림자 효과는 그대로 유지되어 화려함을 더합니다.
GSAP의 Timeline을 기반으로 한 순차적 카드 플립 애니메이션에 마우스 호버 시 흔들림 효과와 클릭 시 새로운 애니메이션을 추가해 보겠습니다. 아래는 이를 구현한 수정된 코드와 설명입니다.
수정된 코드
App.vue 파일에 마우스 이벤트와 추가 애니메이션을 반영한 전체 코드입니다:
vue
<template>
<div class="container">
<div
v-for="(card, index) in cards"
:key="index"
class="card"
ref="cardRefs"
@mouseenter="onHover(index)"
@mouseleave="onLeave(index)"
@click="onClick(index)"
>
<div class="card-inner">
<div class="card-front">
<h2>{{ card.title }}</h2>
</div>
<div class="card-back">
<p>{{ card.content }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export default {
name: 'CardFlipAnimation',
setup() {
const cards = ref([
{ title: 'Card 1', content: '이 카드는 GSAP의 마법을 보여줍니다!' },
{ title: 'Card 2', content: 'Vue와 함께라면 더 강력해요.' },
{ title: 'Card 3', content: '스크롤로 생동감을 불어넣어 보세요!' },
]);
const cardRefs = ref([]);
onMounted(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.container',
start: 'top 80%',
end: 'bottom 20%',
scrub: true,
markers: false,
},
});
cardRefs.value.forEach((card, index) => {
const inner = card.querySelector('.card-inner');
tl.fromTo(
inner,
{ rotationY: 0 },
{
rotationY: 180,
duration: 1,
ease: 'power2.inOut',
},
index * 0.5
);
});
});
// 호버 시 흔들림 효과
const onHover = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
rotationY: '+=10',
yoyo: true,
repeat: 3,
duration: 0.2,
ease: 'sine.inOut',
});
};
// 호버 종료 시 원래 상태로
const onLeave = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
rotationY: gsap.getProperty(inner, 'rotationY'), // 현재 상태 유지
duration: 0.3,
ease: 'power2.out',
});
};
// 클릭 시 새로운 애니메이션 (확대 후 축소)
const onClick = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
scale: 1.2,
duration: 0.3,
ease: 'power1.inOut',
yoyo: true,
repeat: 1,
});
};
return { cards, cardRefs, onHover, onLeave, onClick };
},
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 50px;
padding: 100px 20px;
min-height: 100vh;
}
.card {
perspective: 1000px;
width: 300px;
height: 200px;
margin: 0 auto;
cursor: pointer; /* 클릭 가능함을 시각적으로 표시 */
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
}
.card-front {
background-color: #3498db;
color: white;
}
.card-back {
background-color: #e74c3c;
color: white;
transform: rotateY(180deg);
}
</style>
코드 설명
1. 이벤트 핸들러 추가
-
마우스 호버 이벤트:
-
@mouseenter와 @mouseleave를 카드에 추가해 호버 상태를 감지합니다.
-
onHover와 onLeave 메서드를 setup에서 정의합니다.
-
-
클릭 이벤트:
-
@click을 추가해 클릭 시 onClick 메서드가 실행되도록 설정합니다.
-
2. 호버 시 흔들림 효과 (onHover & onLeave)
-
onHover:
-
gsap.to를 사용해 카드가 rotationY를 10도씩 흔들리도록 설정합니다.
-
yoyo: true와 repeat: 3으로 3번 왕복하며 흔들리게 하고, duration: 0.2와 ease: 'sine.inOut'으로 부드럽게 처리합니다.
-
-
onLeave:
-
호버가 끝나면 현재 rotationY 값을 유지하며 흔들림을 멈추고, power2.out 이징으로 자연스럽게 정지합니다.
-
3. 클릭 시 애니메이션 (onClick)
-
클릭 시 카드가 1.2배로 커졌다가 다시 원래 크기로 돌아오는 효과를 구현했습니다.
-
scale: 1.2, yoyo: true, repeat: 1로 설정해 확대 후 축소되며, duration: 0.3과 ease: 'power1.inOut'으로 부드러운 전환을 만듭니다.
4. 스타일 보완
-
.card에 cursor: pointer를 추가해 사용자가 클릭 가능함을 인지하도록 했습니다.
결과
-
스크롤: 여전히 카드는 0.5초 간격으로 순차적으로 뒤집힙니다.
-
호버: 마우스를 카드 위에 올리면 약간 흔들리며 인터랙티브한 느낌을 줍니다.
-
클릭: 카드를 클릭하면 살짝 커졌다가 원래 크기로 돌아오는 효과가 나타납니다.
카드 크기를 뷰포트에 맞게 조정하여 데스크톱과 모바일 모두에서 멋지게 보이도록 반응형 디자인을 적용해 보겠습니다. 이를 위해 CSS에서 뷰포트 단위(vw, vh)와 미디어 쿼리를 활용하고, GSAP 애니메이션은 그대로 유지하면서 크기 변화에 영향을 받지 않도록 설정하겠습니다.
수정된 코드
아래는 반응형 카드 크기를 적용한 App.vue의 전체 코드입니다:
vue
<template>
<div class="container">
<div
v-for="(card, index) in cards"
:key="index"
class="card"
ref="cardRefs"
@mouseenter="onHover(index)"
@mouseleave="onLeave(index)"
@click="onClick(index)"
>
<div class="card-inner">
<div class="card-front">
<h2>{{ card.title }}</h2>
</div>
<div class="card-back">
<p>{{ card.content }}</p>
</div>
</div>
</div>
</div>
</template>
<script>
import { ref, onMounted } from 'vue';
import { gsap } from 'gsap';
import { ScrollTrigger } from 'gsap/ScrollTrigger';
gsap.registerPlugin(ScrollTrigger);
export default {
name: 'CardFlipAnimation',
setup() {
const cards = ref([
{ title: 'Card 1', content: '이 카드는 GSAP의 마법을 보여줍니다!' },
{ title: 'Card 2', content: 'Vue와 함께라면 더 강력해요.' },
{ title: 'Card 3', content: '스크롤로 생동감을 불어넣어 보세요!' },
]);
const cardRefs = ref([]);
onMounted(() => {
const tl = gsap.timeline({
scrollTrigger: {
trigger: '.container',
start: 'top 80%',
end: 'bottom 20%',
scrub: true,
markers: false,
},
});
cardRefs.value.forEach((card, index) => {
const inner = card.querySelector('.card-inner');
tl.fromTo(
inner,
{ rotationY: 0 },
{
rotationY: 180,
duration: 1,
ease: 'power2.inOut',
},
index * 0.5
);
});
});
const onHover = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
rotationY: '+=10',
yoyo: true,
repeat: 3,
duration: 0.2,
ease: 'sine.inOut',
});
};
const onLeave = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
rotationY: gsap.getProperty(inner, 'rotationY'),
duration: 0.3,
ease: 'power2.out',
});
};
const onClick = (index) => {
const inner = cardRefs.value[index].querySelector('.card-inner');
gsap.to(inner, {
scale: 1.2,
duration: 0.3,
ease: 'power1.inOut',
yoyo: true,
repeat: 1,
});
};
return { cards, cardRefs, onHover, onLeave, onClick };
},
};
</script>
<style scoped>
.container {
display: flex;
flex-direction: column;
gap: 5vh; /* 뷰포트 높이에 따라 간격 조정 */
padding: 10vh 2vw; /* 상하좌우 패딩을 뷰포트 단위로 */
min-height: 100vh;
}
.card {
perspective: 1000px;
width: 80vw; /* 뷰포트 너비의 80%로 설정 */
max-width: 400px; /* 최대 너비 제한 */
height: 25vh; /* 뷰포트 높이의 25%로 설정 */
max-height: 250px; /* 최대 높이 제한 */
margin: 0 auto;
cursor: pointer;
}
.card-inner {
position: relative;
width: 100%;
height: 100%;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.card-front,
.card-back {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
display: flex;
align-items: center;
justify-content: center;
border-radius: 10px;
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.2);
font-size: clamp(1rem, 2.5vw, 2rem); /* 텍스트 크기 반응형 */
}
.card-front {
background-color: #3498db;
color: white;
}
.card-back {
background-color: #e74c3c;
color: white;
transform: rotateY(180deg);
}
/* 모바일 디바이스 조정 */
@media (max-width: 768px) {
.card {
width: 90vw; /* 모바일에서 더 넓게 */
height: 20vh; /* 높이 약간 줄임 */
}
.container {
gap: 3vh; /* 모바일에서 간격 축소 */
padding: 5vh 2vw;
}
}
</style>
변경 사항 설명
1. 반응형 크기 설정
-
.card 크기:
-
width: 80vw로 뷰포트 너비의 80%를 기본으로 설정하고, max-width: 400px로 너무 커지지 않도록 제한했습니다.
-
height: 25vh로 뷰포트 높이의 25%를 사용하며, max-height: 250px로 상한선을 뒀습니다.
-
-
.container:
-
gap: 5vh와 padding: 10vh 2vw로 뷰포트 단위 기반의 간격과 여백을 설정해 화면 크기에 따라 유연하게 조정되도록 했습니다.
-
-
텍스트 크기:
-
font-size: clamp(1rem, 2.5vw, 2rem)를 사용해 최소 1rem, 최대 2rem 사이에서 뷰포트 너비에 따라 텍스트 크기가 반응형으로 변하도록 했습니다.
-
2. 모바일 최적화 (미디어 쿼리)
-
768px 이하 화면:
-
width: 90vw로 카드 너비를 늘려 모바일에서 더 넓게 보이게 했습니다.
-
height: 20vh로 높이를 줄여 화면 비율을 맞췄습니다.
-
gap: 3vh와 padding: 5vh 2vw로 간격과 여백을 축소해 모바일에서도 깔끔하게 배치되도록 했습니다.
-
3. GSAP 애니메이션 유지
-
GSAP 애니메이션은 상대적인 값(rotationY, scale)을 사용하므로 크기 변경에 영향을 받지 않습니다. 따라서 스크롤, 호버, 클릭 이벤트는 그대로 잘 작동합니다.
결과
-
데스크톱: 카드가 뷰포트의 80% 너비와 25% 높이로 적절히 표시되며, 최대 크기 제한으로 너무 커지지 않습니다.
-
모바일: 768px 이하에서 카드가 90% 너비와 20% 높이로 조정되어 화면을 꽉 채우면서도 비율이 유지됩니다. 텍스트 크기도 화면에 맞춰 자연스럽게 변합니다.
-
애니메이션: 스크롤 시 순차 플립, 호버 시 흔들림, 클릭 시 확대/축소 효과가 모든 디바이스에서 문제없이 작동합니다.
추가 개선 아이디어
-
세로 모드 고려: 모바일 세로 모드에서 카드가 너무 길어질 경우, aspect-ratio: 3 / 2를 추가해 비율을 고정할 수 있습니다.
-
더 작은 화면: @media (max-width: 480px)를 추가해 초소형 화면에서 font-size나 padding을 더 세밀히 조정할 수 있습니다.
-
트랜지션 부드럽게: .card-inner의 transition 속성을 transform 0.3s ease로 조정해 크기 변화 시 부드러운 전환을 추가할 수 있습니다.
이제 이 코드는 데스크톱부터 모바일까지 모든 뷰포트에서 멋지게 보일 겁니다. 반응형 웹의 매력을 느껴보세요! 추가 질문이 있다면 언제든 말씀해주세요!
728x90
'Vue.js 를 배워보자' 카테고리의 다른 글
Vue.js에서 사용되는 모든 특수문자 완벽 정리 (0) | 2025.03.25 |
---|---|
Vue.js에서 watch로 버튼과 그리드의 disabled 상태 동적 제어하기 (0) | 2025.03.16 |
Vue.js로 실시간 판매량 그리드에 부드러운 숫자 애니메이션 구현하기 (0) | 2025.03.14 |
Vue.js로 장바구니 수량 관리 기능 만들기 외 2개 예제 (0) | 2025.03.14 |
Vue.js 그리드 데이터 흐름 제어: 임시 이동, 커서 위치 갱신, 롤백, 반응형 데이터 처리 A to Z (0) | 2025.03.13 |