Skip to content

Commit 27f0bf4

Browse files
authored
Merge pull request #33 from DeveloperBlog-Devflow/feature/landing-page
feat: 랜딩페이지 애니메이션 추가
2 parents b3bf487 + 97fe32c commit 27f0bf4

8 files changed

Lines changed: 212 additions & 20 deletions

File tree

animations/landingGSAP.ts

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
import { gsap } from 'gsap';
2+
import { ScrollTrigger } from 'gsap/ScrollTrigger';
3+
4+
let pluginsRegistered = false;
5+
6+
interface AnimateOptions {
7+
trigger: HTMLElement; // 스크롤 트리거 기준 요소
8+
targets: gsap.TweenTarget; // 애니메이션 대상
9+
stagger?: number; // 순차 재생 간격
10+
start?: string; // 스크롤 시작 지점
11+
yOffset?: number; // y축 이동 거리
12+
}
13+
14+
/* Fade Up 함수 */
15+
const animateFadeUp = ({
16+
trigger,
17+
targets,
18+
stagger = 0,
19+
start = 'top center-=50',
20+
yOffset = 24,
21+
}: AnimateOptions) => {
22+
if (!pluginsRegistered) {
23+
gsap.registerPlugin(ScrollTrigger);
24+
pluginsRegistered = true;
25+
}
26+
27+
const ctx = gsap.context(() => {
28+
gsap.fromTo(
29+
targets,
30+
{ opacity: 0, y: yOffset },
31+
{
32+
opacity: 1,
33+
y: 0,
34+
duration: 0.6,
35+
ease: 'power2.out',
36+
stagger: stagger,
37+
scrollTrigger: {
38+
trigger: trigger,
39+
start: start,
40+
toggleActions: 'play none none reverse',
41+
// markers: true,
42+
},
43+
}
44+
);
45+
});
46+
47+
return () => ctx.revert();
48+
};
49+
50+
/* --- 1. Hero Section --- */
51+
export type HeroSectionRefs = {
52+
titleEl: HTMLHeadingElement;
53+
subTextEl: HTMLDivElement;
54+
};
55+
56+
export const initHeroSectionAnimation = ({
57+
titleEl,
58+
subTextEl,
59+
}: HeroSectionRefs) => {
60+
return animateFadeUp({
61+
trigger: titleEl,
62+
targets: subTextEl,
63+
start: 'top center-=150',
64+
yOffset: 40,
65+
});
66+
};
67+
68+
/* --- 2. Feature List Section --- */
69+
export type FeatureListRefs = {
70+
titleEl: HTMLHeadingElement;
71+
subTextEl: HTMLDivElement;
72+
cardsContainerEl: HTMLDivElement;
73+
};
74+
75+
export const initFeatureListAnimation = ({
76+
titleEl,
77+
subTextEl,
78+
cardsContainerEl,
79+
}: FeatureListRefs) => {
80+
const cards = Array.from(cardsContainerEl.children);
81+
82+
return animateFadeUp({
83+
trigger: titleEl,
84+
targets: [subTextEl, ...cards],
85+
stagger: 0.12,
86+
});
87+
};
88+
89+
/* --- 3. Landing Preview Section --- */
90+
export type LandingPreviewRefs = {
91+
titleEl: HTMLHeadingElement;
92+
contentContainerEl: HTMLDivElement;
93+
};
94+
95+
export const initLandingPreviewAnimation = ({
96+
titleEl,
97+
contentContainerEl,
98+
}: LandingPreviewRefs) => {
99+
const items = contentContainerEl.querySelectorAll('.fc-item');
100+
101+
return animateFadeUp({
102+
trigger: titleEl,
103+
targets: items,
104+
stagger: 0.2,
105+
});
106+
};

components/landing/FeatureContent.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ const FeatureContent = () => {
2323
return (
2424
<ul className="space-y-6">
2525
{ITEMS.map((item) => (
26-
<li key={item.title}>
26+
<li key={item.title} className="fc-item">
2727
<div className="flex gap-3">
2828
<FiCheckCircle className="text-primary text-2xl" />
2929
<p className="text-xl font-medium">{item.title}</p>

components/landing/FeatureList.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,16 @@
1+
'use client';
2+
13
import FeatureCard from '@/components/landing/FeatureCard';
4+
import { useLayoutEffect, useRef } from 'react';
5+
import { initFeatureListAnimation } from '@/animations/landingGSAP';
6+
27
import {
38
FiBookOpen,
49
FiTarget,
510
FiCheckCircle,
611
FiTrendingUp,
712
} from 'react-icons/fi';
13+
814
const FEATURES = [
915
{
1016
icon: <FiBookOpen className="text-xl" />,
@@ -32,14 +38,34 @@ const FEATURES = [
3238
},
3339
];
3440
const FeatureList = () => {
41+
const titleRef = useRef<HTMLHeadingElement>(null);
42+
const subTextRef = useRef<HTMLDivElement>(null);
43+
const gridRef = useRef<HTMLDivElement>(null);
44+
45+
useLayoutEffect(() => {
46+
if (!titleRef.current || !subTextRef.current || !gridRef.current) return;
47+
48+
const cleanup = initFeatureListAnimation({
49+
titleEl: titleRef.current,
50+
subTextEl: subTextRef.current,
51+
cardsContainerEl: gridRef.current,
52+
});
53+
54+
return cleanup;
55+
}, []);
3556
return (
36-
<div className="flex h-full flex-col items-center justify-center space-y-3 text-center">
37-
<h1 className="text-4xl font-bold">개발자 성장을 위한</h1>
57+
<div className="flex h-full flex-col items-center justify-center space-y-3 pt-100 text-center">
58+
<h1 ref={titleRef} className="text-4xl font-bold">
59+
개발자 성장을 위한
60+
</h1>
3861
<h1 className="text-primary text-4xl font-bold">완벽한 도구</h1>
39-
<div className="text-xl font-light">
62+
<div ref={subTextRef} className="text-xl font-light">
4063
<h2>체계적인 학습과 지속적인 성장을 돕는 강력한 기능들</h2>
4164
</div>
42-
<div className="mt-15 grid max-w-7xl grid-cols-1 gap-6 px-20 sm:grid-cols-2 lg:grid-cols-4">
65+
<div
66+
ref={gridRef}
67+
className="mt-15 grid max-w-7xl grid-cols-1 gap-6 px-20 sm:grid-cols-2 lg:grid-cols-4"
68+
>
4369
{FEATURES.map((feature) => (
4470
<FeatureCard key={feature.title} {...feature} />
4571
))}

components/landing/HeroSection.tsx

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,30 @@
1+
'use client';
2+
3+
import { useLayoutEffect, useRef } from 'react';
4+
import { initHeroSectionAnimation } from '@/animations/landingGSAP';
5+
16
const HeroSection = () => {
7+
const titleRef = useRef<HTMLHeadingElement>(null);
8+
const subTextRef = useRef<HTMLDivElement>(null);
9+
10+
useLayoutEffect(() => {
11+
if (!titleRef.current || !subTextRef.current) return;
12+
13+
const cleanup = initHeroSectionAnimation({
14+
titleEl: titleRef.current,
15+
subTextEl: subTextRef.current,
16+
});
17+
18+
return cleanup;
19+
}, []);
20+
221
return (
3-
<div className="flex flex-col items-center justify-center space-y-3 pt-30 text-center">
4-
<h1 className="text-8xl font-bold">매일 성장하는</h1>
22+
<div className="flex flex-col items-center justify-center space-y-3 pt-100 text-center">
23+
<h1 ref={titleRef} className="text-8xl font-bold">
24+
매일 성장하는
25+
</h1>
526
<h1 className="text-primary text-8xl font-bold">개발자의 여정</h1>
6-
<div className="text-2xl font-light">
27+
<div ref={subTextRef} className="text-2xl font-light">
728
<h2>TIL 작성부터 목표 관리까지, DevFlow와 함께</h2>
829
<h2>체계적인 학습으로 더 나은 개발자로 성장하세요</h2>
930
</div>

components/landing/LandingHeader.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import Link from 'next/link';
22

33
const LandingHeader = () => {
44
return (
5-
<div className="fixed flex w-full items-center justify-between bg-white">
5+
<div className="fixed z-50 flex w-full items-center justify-between bg-white">
66
<h1 className="flex items-center gap-2 px-10 py-2 text-3xl font-bold">
77
<span className="text-primary">&lt;/&gt;</span>
88
<span>DevFlow</span>

components/landing/LandingPreview.tsx

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,45 @@
1+
'use client';
2+
13
import Image from 'next/image';
24
import FeatureContent from '@/components/landing/FeatureContent';
5+
import { useLayoutEffect, useRef } from 'react';
6+
import { initLandingPreviewAnimation } from '@/animations/landingGSAP';
37

48
const LandingPreview = () => {
9+
const titleRef = useRef<HTMLHeadingElement>(null);
10+
const contentRef = useRef<HTMLDivElement>(null);
11+
12+
useLayoutEffect(() => {
13+
if (!titleRef.current || !contentRef.current) return;
14+
15+
return initLandingPreviewAnimation({
16+
titleEl: titleRef.current,
17+
contentContainerEl: contentRef.current,
18+
});
19+
}, []);
520
return (
6-
<div className="mt-30 flex h-full flex-col items-center justify-center space-y-3 pb-50 text-center">
7-
<h1 className="text-4xl font-bold">체계적인 학습으로</h1>
21+
<div className="mt-30 flex h-full flex-col items-center justify-center space-y-3 pt-50 pb-50 text-center">
22+
<h1 ref={titleRef} className="text-4xl font-bold">
23+
체계적인 학습으로
24+
</h1>
825
<h1 className="text-primary text-4xl font-bold">더 빠른 성장</h1>
9-
<div className="mx-20 mt-10 flex flex-col items-center gap-10 lg:flex-row lg:items-start lg:gap-50">
10-
<FeatureContent />
11-
<Image
12-
src="/PreviewImage.png"
13-
alt="DevFlow 미리보기"
14-
width={720}
15-
height={400}
16-
className="w-full max-w-[480px] min-w-[360px] flex-shrink-0 object-contain md:max-w-[560px] lg:max-w-[640px] xl:max-w-[720px]"
17-
/>
26+
<div
27+
className="mx-20 mt-10 grid max-w-7xl grid-cols-1 items-start gap-10 lg:grid-cols-2 lg:gap-2"
28+
ref={contentRef}
29+
>
30+
<div className="flex min-w-[355px] justify-center">
31+
<FeatureContent />
32+
</div>
33+
34+
<div className="flex justify-center">
35+
<Image
36+
src="/PreviewImage.png"
37+
alt="DevFlow 미리보기"
38+
width={720}
39+
height={400}
40+
className="fc-item w-full max-w-[480px] min-w-[360px] object-contain md:max-w-[560px] lg:max-w-[640px] xl:max-w-[720px]"
41+
/>
42+
</div>
1843
</div>
1944
</div>
2045
);

package-lock.json

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,11 @@
1111
"dependencies": {
1212
"clsx": "^2.1.1",
1313
"firebase": "^12.7.0",
14+
15+
"gsap": "^3.14.2",
16+
1417
"lucide-react": "^0.562.0",
18+
1519
"next": "16.1.1",
1620
"react": "19.2.3",
1721
"react-dom": "19.2.3",

0 commit comments

Comments
 (0)