๐จ 3๋จ๊ณ : ์จ๋ณด๋ฉ ๋๋ฉ์ธ ํ ํฌ์คํ - 100-hours-a-week/7-team-ddb-wiki GitHub Wiki
์์ฑ์ผ | 2025-05-30 |
---|---|
๋ฒ์ | 1.1 |
์์ฑ์ | suzy.kang (๊ฐ์์ง) |
๊ฒํ ์ | kevin |
์ํ | ์๋ฃ |
๋ฒ์ | ๋ ์ง | ์์ฑ์ | ๊ฒํ ์ | ๋ณ๊ฒฝ๋ด์ฉ |
---|---|---|---|---|
1.0 | 2025-04-26 | suzy | kevin | ์ด์ |
1.1 | 2025-05-30 | suzy | kevin | MVP ๊ฐ๋ฐ ์ดํ ๋ฌธ์ ๋๊ธฐํ |
- ์๋น์ค์ ํต์ฌ ๊ฐ์น์ ๊ธฐ๋ฅ์ ํจ๊ณผ์ ์ผ๋ก ์๊ฐ
- ์ง๊ด์ ์ด๊ณ ๋งค๋ ฅ์ ์ธ ์จ๋ณด๋ฉ UI/UX ์ ๊ณต
- ์นด์นด์ค ์์ ๋ก๊ทธ์ธ์ผ๋ก์ ์์ฐ์ค๋ฌ์ด ์ ๋
- ๋ค๊ตญ์ด ์ง์
- ์์ธํ ์๋น์ค ์ด์ฉ ๋ฐฉ๋ฒ ์ค๋ช
- ๋ณต์กํ ์ธํฐ๋์ ์ด๋ ์ ๋๋ฉ์ด์
-
Next.js (v15.3.0) + React (v19.1.0)
- App Router ๊ธฐ๋ฐ ๋ผ์ฐํ
- SSR/ISR ํ์ฉ์ผ๋ก SEO ์ต์ ํ
- React 19์ use() ํ ํ์ฉํ ๋ฐ์ดํฐ ํ์นญ
-
Framer Motion
- ์ค์์ดํ ์ ์ค์ฒ ์ง์
- ๋ถ๋๋ฌ์ด ํ์ด์ง ์ ํ
-
Tailwind CSS (v4.1)
- ์ ํธ๋ฆฌํฐ ํผ์คํธ ์ ๊ทผ์ผ๋ก ๋น ๋ฅธ ๊ฐ๋ฐ
- ์ปค์คํ ํ ๋ง๋ก ๋ธ๋๋ ๋์์ธ ์์คํ ๊ตฌ์ถ
-
shadcn/ui
- Tailwind ๊ธฐ๋ฐ ์ปดํฌ๋ํธ
- ํ์ํ ์ปดํฌ๋ํธ๋ง ๊ฐ์ ธ์ ๋ฒ๋ค ํฌ๊ธฐ ์ต์ ํ
-
Next.js Image Component
- ์๋ ์ด๋ฏธ์ง ์ต์ ํ
- priority ์์ฑ์ผ๋ก ์ค์ ์ด๋ฏธ์ง ์ฐ์ ๋ก๋ฉ
src/
โโโ app/(auth)/onboarding/
โ โโโ page.tsx # ์จ๋ณด๋ฉ ํ์ด์ง
โโโ features/onboarding/
โโโ components/ # UI ์ปดํฌ๋ํธ
โ โโโ dolpin-logo/
โ โโโ kakao-login-button/
โ โโโ onboarding-slider/
โ โโโ OnboardingSlider.tsx
โ โโโ OnboardingSlide.tsx
โ โโโ SlideIndicator.tsx
โโโ constants/ # ์์
โโโ slides.ts # ์ฌ๋ผ์ด๋ ๋ฐ์ดํฐ
-
๊ตฌํ๋ ๊ธฐ๋ฅ:
- 4๊ฐ์ ์จ๋ณด๋ฉ ์ฌ๋ผ์ด๋
- ์ค์์ดํ ์ ์ค์ฒ ์ง์
- ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ
- ์ฌ๋ผ์ด๋ ์ธ๋์ผ์ดํฐ
-
์ฌ์ฉ ์ปดํฌ๋ํธ:
-
DolpinLogo
- ๋ํ ๋ก๊ณ -
OnboardingSlider
- ์ฌ๋ผ์ด๋ ์ปจํ ์ด๋ -
KakaoLoginButton
- ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ
-
-
๋ ์ด์์:
- ๋ก๊ณ : ์ผ์ชฝ ์๋จ ๊ณ ์
- ์ฌ๋ผ์ด๋: ์ค์ ๋ฐฐ์น
- ๋ก๊ทธ์ธ ๋ฒํผ: ํ๋จ ๊ณ ์
-
์ญํ : ์จ๋ณด๋ฉ ์ฌ๋ผ์ด๋๋ค์ ๊ด๋ฆฌํ๊ณ ์ค์์ดํ ๊ธฐ๋ฅ์ ์ ๊ณต
-
Props & Interface:
interface OnboardingSliderProps { slides: { imageUrl: string; title: string; description: string; }[]; }
-
๊ธฐ๋ฅ:
- ์ค์์ดํ ์ ์ค์ฒ (๋๋๊ทธ)
- ์คํ๋ง ์ ๋๋ฉ์ด์ ์ ํ
- ๋ฐฉํฅ ๊ฐ์ง (์ผ์ชฝ/์ค๋ฅธ์ชฝ)
- ๊ฒฝ๊ณ ๊ฒ์ฌ (์ฒซ/๋ง์ง๋ง ์ฌ๋ผ์ด๋)
-
์ํ ๊ด๋ฆฌ:
-
activeIndex
: ํ์ฌ ํ์ฑ ์ฌ๋ผ์ด๋ -
direction
: ์ ๋๋ฉ์ด์ ๋ฐฉํฅ
-
-
์ ๋๋ฉ์ด์ :
- Framer Motion ์ฌ์ฉ
- enter/center/exit ์ํ
- ์คํ๋ง ํจ๊ณผ (stiffness: 300, damping: 30)
-
์ญํ : ๊ฐ ์จ๋ณด๋ฉ ๋จ๊ณ์ ์ด๋ฏธ์ง์ ํ ์คํธ๋ฅผ ํ์
-
Props & Interface:
interface OnboardingSlideProps { imageUrl: string; title: string; description: string; }
-
๊ธฐ๋ฅ:
- ์ด๋ฏธ์ง ํ์
- ์ ๋ชฉ ๋ฐ ์ค๋ช ํ ์คํธ
- ๋ฐ์ํ ๋ ์ด์์
-
์คํ์ผ๋ง:
- ํ ์คํธ ์ค์ ์ ๋ ฌ
- ๋ผ์ธ ๋ธ๋ ์ดํฌ ์ง์ (\n)
- ์ด๋ฏธ์ง: 256px ๋์ด, ์ ์ฒด ๋๋น
- ๋ฅ๊ทผ ๋ชจ์๋ฆฌ (rounded-md)
- ์ฌ๋ฐฑ: mb-6 (์ด๋ฏธ์ง), mb-5 (์ ๋ชฉ)
-
์ญํ : ํ์ฌ ์ฌ๋ผ์ด๋ ์์น๋ฅผ ๋ํธ๋ก ํ์
-
Props & Interface:
interface SlideIndicatorProps { total: number; active: number; }
-
๊ธฐ๋ฅ:
- ์ ์ฒด ์ฌ๋ผ์ด๋ ์๋งํผ ๋ํธ ํ์
- ํ์ฑ ๋ํธ ํ์ด๋ผ์ดํธ
-
์คํ์ผ๋ง:
- ๋ํธ ํฌ๊ธฐ: 8x8px
- ํ์ฑ ์ํ: ์งํ ์์ + ๋๋น 16px
- ๋นํ์ฑ ์ํ: ์ฐํ ์์
- ํธ๋์ง์ : 300ms
- ์ญํ : ์นด์นด์ค ๋ก๊ทธ์ธ API ํธ์ถ ๋ฐ ๋ฆฌ๋ค์ด๋ ํธ ์ฒ๋ฆฌ
-
๊ธฐ๋ฅ:
- OAuth ๋ฆฌ๋ค์ด๋ ํธ URL ํ๋
- ์นด์นด์ค ๋ก๊ทธ์ธ ํ์ด์ง๋ก ์ด๋
- ๋ก๋ฉ ์ค ๋ฒํผ ๋นํ์ฑํ
-
์ํ ๊ด๋ฆฌ:
-
redirectUrl
: OAuth URL ์ํ - useEffect๋ก ์ปดํฌ๋ํธ ๋ง์ดํธ ์ URL ํ๋
-
-
์คํ์ผ๋ง:
- ์นด์นด์ค ์ฌ๋ณผ + ํ ์คํธ
- ํ์ดํ ์์ด์ฝ
- ์ ์ฒด ๋๋น ๋ฒํผ
- ์ญํ : ์๋น์ค ๋ก๊ณ ํ์
-
๊ธฐ๋ฅ:
- SVG ๋ก๊ณ ํ์
- priority ๋ก๋ฉ
-
์คํ์ผ๋ง:
- ํฌ๊ธฐ: 120x120px
- ์์น: ์ผ์ชฝ ์๋จ ๊ณ ์
export const slides = [
{
imageUrl: '/img/onboarding-1.png',
title: '์ผ์์ ํํํ๋ฏ ๊ธฐ๋กํ์ธ์',
description:
'๋งค์ผ ๊ฑท๋ ๊ฑฐ๋ฆฌ๋ ๊ฒ์์ฒ๋ผ, \n ๋น์ ์ ๋ฐ์์ทจ๋ฅผ ๋ฐ๋ผ \n ๋๋ง์ ์ด์ผ๊ธฐ ์ง๋๋ฅผ ๋ง๋ค์ด๋ณด์ธ์.',
},
{
imageUrl: '/img/onboarding-2.png',
title: '๋ด ์์น, ๋ด ๊ธฐ๋ก์ผ๋ก ์์ฑ๋๋ ์ถ์ฒ',
description:
'์ง๊ธ ์ด๊ณณ๊ณผ ์ง๋ ๋๋ฅผ ๊ธฐ๋ฐ์ผ๋ก \n ๋ฑ ๋ง๋ ์ฅ์๋ฅผ ์ถ์ฒํด๋๋ฆฝ๋๋ค.',
},
{
imageUrl: '/img/onboarding-3.png',
title: '๊ฒ์ํ๋ ๋จ์ด์๋ ์๋ฏธ๋ฅผ ๋ด์',
description: '๋น์ ์ด ์ํ๋ ์ฅ์,\n ์ง๊ธ ๊ฐ์ฅ ์ด์ธ๋ฆฌ๋๊ณต๊ฐ์ ์ฐพ์๋๋ ค์.',
},
{
imageUrl: '/img/onboarding-4.png',
title: 'ํ ํ๋๋ก, ํ๋ฃจ๋ฅผ ๊ธฐ๋กํ์ธ์',
description:
'ํ์ฌ ์์น์ ํ์ ์ฐ๊ณ \n ๊ทธ ์๊ฐ์ ๊ฐ์ ๊ณผ ๊ธฐ์ต์ \n ๊ธฐ๋ก์ผ๋ก ๋จ๊ฒจ๋ณด์ธ์.',
},
];
-
OnboardingSlider:
-
activeIndex
: ํ์ฌ ํ์ฑํ๋ ์ฌ๋ผ์ด๋ ์ธ๋ฑ์ค -
direction
: ์ ๋๋ฉ์ด์ ๋ฐฉํฅ (-1, 0, 1)
-
-
KakaoLoginButton:
-
redirectUrl
: OAuth ๋ฆฌ๋ค์ด๋ ํธ URL
-
-
GET /api/v1/auth/oauth
- OAuth ๋ฆฌ๋ค์ด๋ ํธ URL ํ๋
// ์นด์นด์ค ๋ก๊ทธ์ธ URL ํ๋
const { redirect_url } = await getOAuthRedirectUrl();
window.location.href = redirect_url;
-
์จ๋ณด๋ฉ ์ง์
- 4๊ฐ์ ์ฌ๋ผ์ด๋ ์๋ ํ์
- ์ฒซ ๋ฒ์งธ ์ฌ๋ผ์ด๋๋ถํฐ ์์
-
์ฌ๋ผ์ด๋ ํ์
- ์ข์ฐ ์ค์์ดํ๋ก ์ด๋
- ์ธ๋์ผ์ดํฐ๋ก ํ์ฌ ์์น ํ์ธ
-
๋ก๊ทธ์ธ
- ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ ํด๋ฆญ
- ์นด์นด์ค ์ธ์ฆ ํ์ด์ง๋ก ์ด๋
- ์ธ์ฆ ์๋ฃ ํ ๋ฉ์ธ ํ์ด์ง๋ก ๋ฆฌ๋ค์ด๋ ํธ
const variants = {
enter: (direction) => ({
x: direction > 0 ? 300 : -300,
opacity: 0,
position: 'absolute',
}),
center: {
x: 0,
opacity: 1,
position: 'relative',
},
exit: (direction) => ({
x: direction < 0 ? 300 : -300,
opacity: 0,
position: 'absolute',
}),
};
-
dragConstraints
: ์ข์ฐ ์ ํ -
dragElastic
: 0.2 (ํ์ฑ) - ์๊ณ๊ฐ: 100px ๋๋ 500 velocity
import { slides } from '@/features/onboarding';
<OnboardingSlider slides={slides} />;
// ์ฌ๋ผ์ด๋
<OnboardingSlide
imageUrl="/img/onboarding-1.png"
title="์ผ์์ ํํํ๋ฏ ๊ธฐ๋กํ์ธ์"
description="๋งค์ผ ๊ฑท๋ ๊ฑฐ๋ฆฌ๋ ๊ฒ์์ฒ๋ผ..."
/>
// ์ธ๋์ผ์ดํฐ
<SlideIndicator total={4} active={0} />
// ๋ก๊ทธ์ธ ๋ฒํผ
<Suspense>
<KakaoLoginButton />
</Suspense>
// ๋ก๊ณ
<DolpinLogo />
<div className="relative min-h-screen bg-white">
<div className="absolute left-2 top-0 z-10">
<DolpinLogo />
</div>
<div className="flex min-h-screen flex-col items-center justify-center pt-20">
<OnboardingSlider slides={slides} />
<div className="mt-8 w-full max-w-md px-4">
<Suspense>
<KakaoLoginButton />
</Suspense>
</div>
</div>
</div>
graph TD
A[Onboarding Page] --> B[DolpinLogo]
A --> C[OnboardingSlider]
A --> D[KakaoLoginButton]
C --> E[OnboardingSlide]
C --> F[SlideIndicator]
C --> G[Framer Motion]
D --> H[getOAuthRedirectUrl API]
<motion.div
drag="x"
dragConstraints={{ left: 0, right: 0 }}
dragElastic={0.2}
onDragEnd={(e, info) => {
const offsetX = info.offset.x;
const velocityX = info.velocity.x;
if (offsetX < -100 || velocityX < -500) {
paginate(1); // ๋ค์ ์ฌ๋ผ์ด๋
} else if (offsetX > 100 || velocityX > 500) {
paginate(-1); // ์ด์ ์ฌ๋ผ์ด๋
}
}}
/>
useEffect(() => {
const handleRedirect = async () => {
const { redirect_url } = await getOAuthRedirectUrl();
setRedirectUrl(redirect_url);
};
handleRedirect();
}, []);
-
์ด๋ฏธ์ง ์ต์ ํ
- Next.js Image ์ปดํฌ๋ํธ ์ฌ์ฉ
- priority ์์ฑ์ผ๋ก ๋ก๊ณ ์ฐ์ ๋ก๋ฉ
-
์ ๋๋ฉ์ด์
์ต์ ํ
- GPU ๊ฐ์ ์ฌ์ฉ (transform)
- will-change ์์ฑ ํ์ฉ
-
๋ฒ๋ค ํฌ๊ธฐ
- Framer Motion ํธ๋ฆฌ ์์ดํน
-
์ด๋ฏธ์ง ๋์ฒด ํ
์คํธ
- ๋ชจ๋ ์ด๋ฏธ์ง์ alt ์์ฑ
-
๋ฒํผ
- ์ ์ ํ ํด๋ฆญ ์์ญ
- ์๊ฐ์ ํผ๋๋ฐฑ
- ๋ฒํผ ๋นํ์ฑํ ์ ์ง (ํฅํ)
- ์ฝ์ ์๋ฌ ๋ก๊น
- Next.js ๊ธฐ๋ณธ ์๋ฌ ์ฒ๋ฆฌ
- ๋์ฒด ์ด๋ฏธ์ง ๊ณ ๋ ค (ํฅํ)
-
min-height: 100svh
์ฌ์ฉ - ์์ ์์ญ ๊ณ ๋ ค
- ๋ฐ์ํ ํฐํธ ํฌ๊ธฐ
- ๋ชจ๋ฐ์ผ ํด์๋๋ณ ์ด๋ฏธ์ง
- WebP ํฌ๋งท ์ง์
- ์ ์ ํ sizes ์์ฑ
- React Native ์คํ์ผ ์ ์ค์ฒ
- ์ฑ๋ฅ ์ต์ ํ
- ์ ์ธ์ API
- ์ ์ ์ฝํ ์ธ ๋ก ๋น ๋ฅธ ๋ก๋ฉ
- ์ด๋ฏธ์ง ์ฌ์ ์ต์ ํ
- CDN ์บ์ฑ ํ์ฉ
- ์นด์นด์ค ๋ก๊ทธ์ธ ๋ฒํผ ์ง์ฐ ๋ก๋ฉ
- ์ฌ์ฉ์ ๊ฒฝํ ๊ฐ์
- ์๋ฌ ๋ฐ์ด๋๋ฆฌ ํ์ฉ
- ๊ฐ๋จํ๊ณ ์ง๊ด์ ์ธ UI
- ๋ถ๋๋ฌ์ด ์ ๋๋ฉ์ด์
- ๋น ๋ฅธ ๋ก๋ฉ (SSG)
- ๋ชจ๋ฐ์ผ ์ต์ ํ
- ์ ๊ทผ์ฑ ๊ฐํ
- ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง