๐ ํ๋ก์ ํธ ๊ตฌ์กฐ ๋ถ์
Next.js 15.5.5 - React 19.1.0 ๊ธฐ๋ฐ
TypeScript 5 - ์๊ฒฉํ ํ์
์์คํ
Tailwind CSS 4 - ์ ํธ๋ฆฌํฐ ๊ธฐ๋ฐ ์คํ์ผ๋ง
Turbopack - Next.js์ ์๋ก์ด ๋ฒ๋ค๋ฌ ์ฌ์ฉ
์ํ ๊ด๋ฆฌ & ๋ฐ์ดํฐ ํ์นญ
Zustand - ๊ฒฝ๋ ์ํ ๊ด๋ฆฌ (ํ ํฐ & ์ฌ์ฉ์ ์ ๋ณด)
React Query (TanStack Query) - ์๋ฒ ์ํ ๊ด๋ฆฌ
Axios - HTTP ํด๋ผ์ด์ธํธ
shadcn/ui (New York ์คํ์ผ) - Radix UI ๊ธฐ๋ฐ ์ปดํฌ๋ํธ
Lucide React - ์์ด์ฝ
class-variance-authority (cva) - ๋ณํ ์คํ์ผ ๊ด๋ฆฌ
Pretendard Variable - ํ๊ธ ํฐํธ
React Hook Form - ํผ ์ํ ๊ด๋ฆฌ
Zod - ์คํค๋ง ๊ฒ์ฆ
๐ ํ๋ก์ ํธ ์ํคํ
์ฒ
src/
โโโ app/ # Next.js App Router
โ โโโ (authorized)/ # ์ธ์ฆ๋ ์ฌ์ฉ์ ์ ์ฉ ๋ผ์ฐํธ ๊ทธ๋ฃน
โ โ โโโ creator/ # ํฌ๋ฆฌ์์ดํฐ ํ์ด์ง
โ โ โโโ learner/ # ๋ฌ๋ ํ์ด์ง
โ โโโ login/ # ๋ก๊ทธ์ธ ํ์ด์ง
โ โโโ signup/ # ํ์๊ฐ์
ํ์ด์ง
โ โโโ _components/ # ์ฑ ๋ ๋ฒจ ์ปดํฌ๋ํธ
โ
โโโ shared/ # ๊ณต์ ๋ฆฌ์์ค (์ฌ์ฌ์ฉ ๊ฐ๋ฅ)
โโโ components/ui/ # UI ์ปดํฌ๋ํธ ๋ผ์ด๋ธ๋ฌ๋ฆฌ
โโโ lib/ # ์ ํธ๋ฆฌํฐ ํจ์
โโโ services/ # API ์๋น์ค ๋ ์ด์ด
โ โโโ [domain]/ # ๋๋ฉ์ธ๋ณ ๊ตฌ์ฑ
โ โโโ *.service.ts # API ํธ์ถ ํจ์
โ โโโ *.hook.ts # React Query ํ
โ โโโ *.type.ts # ํ์
์ ์
โโโ stores/ # Zustand ์คํ ์ด
โโโ providers/ # React Context Providers
โโโ types/ # ์ ์ญ ํ์
์ ์
(authorized) - ์ธ์ฆ ํ์ํ ํ์ด์ง ๊ทธ๋ฃนํ
(mypage) - ๋ง์ดํ์ด์ง ๊ด๋ จ ๋ผ์ฐํธ ๊ทธ๋ฃน
URL์ ์ํฅ์ ์ฃผ์ง ์์ผ๋ฉด์ ๋ ์ด์์ ๊ณต์
Primary Colors (๋ธ๋๋ ๊ทธ๋ฆฐ)
--color-primary-green-50 : # f6fdf1 --color-primary-green-100: # e8fbd9 --color-primary-green-200: # d1f8b4
--color-primary-green-300: # b2f381 --color-primary-green-400: # 9bef5b --color-primary-green-500: # 7dea29
/* ๋ฉ์ธ ์ปฌ๋ฌ */ --color-primary-green-600: # 67d215 /* ๋ฒํผ ๊ธฐ๋ณธ */ --color-primary-green-700: # 51a611 /* ๋ฒํผ ํธ๋ฒ */
--color-primary-green-800: # 3b780c --color-primary-green-900: # 244a08 ;
์๋งจํฑ ์ปฌ๋ฌ (oklch ์์ ๊ณต๊ฐ ์ฌ์ฉ)
Background : ํฐ์ (Light) / ์ด๋์ด ํ์ (Dark)
Primary : ๊ธฐ๋ณธ ๋ฒํผ ๋ฐ ๊ฐ์กฐ ์์
Secondary : ๋ณด์กฐ ๋ฒํผ
Muted : ๋นํ์ฑํ ์์
Destructive : ์ญ์ /๊ฒฝ๊ณ ์ก์
ํฐํธ : Pretendard Variable (๊ฐ๋ณ ํฐํธ)
ํด๋ฐฑ : Apple SD Gothic Neo, Noto Sans KR, Malgun Gothic
--radius : 0.625rem (10px ) --radius-sm: 6px --radius-md: 8px --radius-lg: 10px --radius-xl: 14px ;
CSS ๋ณ์ ๊ธฐ๋ฐ ํ
๋ง ์ ํ
.dark ํด๋์ค๋ก ์ ์ด
ํ์
๊ท์น
์์
์ปดํฌ๋ํธ
PascalCase
Button.tsx, ScrollAnimation.tsx
ํ์ด์ง
page.tsx
app/login/page.tsx
๋ ์ด์์
layout.tsx
app/(authorized)/layout.tsx
์๋น์ค
domain.service.ts
auth.service.ts
ํ
domain.hook.ts
auth.hook.ts
ํ์
domain.type.ts
auth.type.ts
์ ํธ
kebab-case.ts
cookie-storage.ts
API ํจ์ ๋ช
๋ช
(HTTP ๋ฉ์๋ ์ ๋์ฌ)
// โ
์ฌ๋ฐ๋ฅธ ์์
export const POST_login = async ( data : LoginRequest ) => { } ;
export const GET_profile = async ( ) => { } ;
export const PUT_profile = async ( data : UserRequest ) => { } ;
export const DELETE_withdraw = async ( ) => { } ;
// โ
์ฌ๋ฐ๋ฅธ ์์
export const usePostLogin = ( ) => { } ;
export const useGetProfile = ( ) => { } ;
// Request/Response ์ ๋ฏธ์ฌ ์ฌ์ฉ
export type LoginRequest = { ... }
export type LoginResponse = { ... }
export type UserResponse = { ... }
// ํ์
๊ณผ Enum์ ํจ๊ป export
export type UserType = 'LEARNER' | 'CREATOR' ;
export const enum UserTypeEnum {
LEARNER = 'LEARNER' ,
CREATOR = 'CREATOR' ,
}
3. Import ์์ (Prettier ์๋ ์ ๋ ฌ)
// 1. ์๋ ๊ฒฝ๋ก import (CSS ์ ์ธ)
import { LoginRequest } from './auth.type' ;
// 2. CSS/SCSS
import './globals.css' ;
// 3. React
import { useState } from 'react' ;
// 4. Next.js
import Link from 'next/link' ;
// 6. @/ ๊ฒฝ๋ก (์ํ๋ฒณ ์)
import { Button } from '@/shared/components/ui/button' ;
import { useAuthStore } from '@/shared/stores/auth' ;
// 5. Third-party ๋ผ์ด๋ธ๋ฌ๋ฆฌ
import { useMutation } from '@tanstack/react-query' ;
import axios from 'axios' ;
{
"strict" : true , // ์๊ฒฉ ๋ชจ๋
"target" : " ES2017" ,
"module" : " esnext" ,
"moduleResolution" : " bundler" ,
"paths" : {
"@/*" : [" ./src/*" ] // ์ ๋ ๊ฒฝ๋ก alias
}
}
๐๏ธ ์ํคํ
์ฒ ํจํด
1. ์๋น์ค ๋ ์ด์ด ํจํด (3๋จ ๊ตฌ์กฐ)
โ Service Layer (*.service.ts)
// API ํธ์ถ ๋ก์ง๋ง ๋ด๋น
export const POST_login = async ( data : LoginRequest ) : Promise < ApiResponse < LoginResponse > > => {
const response = await api . post ( '/api/v1/auth/login' , data ) ;
return response . data ;
} ;
โก Hook Layer (*.hook.ts)
// React Query๋ฅผ ์ฌ์ฉํ ์ํ ๊ด๋ฆฌ
export const usePostLogin = ( ) => {
return useMutation ( {
mutationKey : [ POST_login . name ] ,
mutationFn : ( data : LoginRequest ) => POST_login ( data ) ,
} ) ;
} ;
// ๋น์ฆ๋์ค ๋ก์ง ์คํ
const { mutateAsync : postLogin } = usePostLogin ( ) ;
const onSubmit = async ( data : LoginRequest ) => {
await postLogin ( data ) . then ( ( response ) => {
// ์ฑ๊ณต ์ฒ๋ฆฌ
} ) ;
} ;
2. ๊ณตํต API Response ํ์
export interface ApiResponse < T > {
success : boolean ;
code : string ;
message : string ;
data : T ;
}
3. Axios ์ธํฐ์
ํฐ ํจํด
Zustand ์คํ ์ด์์ accessToken ๊ฐ์ ธ์ค๊ธฐ
Authorization ํค๋ ์๋ ์ถ๊ฐ
401 ์๋ฌ ์ ํ ํฐ ์๋ ๊ฐฑ์
Refresh Token ๊ธฐ๋ฐ ์ฌ์ธ์ฆ
Queue ํจํด์ผ๋ก ๋์ ์์ฒญ ์ฒ๋ฆฌ
Zustand (ํด๋ผ์ด์ธํธ ์ํ)
// ํ ํฐ์ ์ฟ ํค์ persist
export const useAuthStore = create (
persist < AuthState & AuthActions > (
( set ) => ( { ... } ) ,
{
name : '@insty-app.token' ,
storage : createJSONStorage ( ( ) => cookieStorage ) ,
}
)
) ;
// ์ฌ์ฉ์ ์ ๋ณด๋ ๋ฉ๋ชจ๋ฆฌ์๋ง ์ ์ฅ
export const useUserStore = create < UserState & UserActions > ( ( set ) => ( { ... } ) ) ;
React Query (์๋ฒ ์ํ)
const queryClient = new QueryClient ( {
defaultOptions : {
queries : {
staleTime : 60 * 1000 , // 1๋ถ
} ,
} ,
} ) ;
{
"singleQuote" : true , // ์์๋ฐ์ดํ ์ฌ์ฉ
"semi" : true , // ์ธ๋ฏธ์ฝ๋ก ํ์
"tabWidth" : 2 , // ๋ค์ฌ์ฐ๊ธฐ 2์นธ
"trailingComma" : " all" , // ํํ ์ผํ
"printWidth" : 120 , // ์ค ๊ธธ์ด 120์
"arrowParens" : " always" , // ํ์ดํ ํจ์ ๊ดํธ ํญ์
"plugins" : [" @trivago/prettier-plugin-sort-imports" , " prettier-plugin-tailwindcss" ]
}
๐ฏ UI ์ปดํฌ๋ํธ ํจํด
1. shadcn/ui ๊ธฐ๋ฐ ์ปดํฌ๋ํธ
Button ์ปดํฌ๋ํธ ๋ณํ
const buttonVariants = cva ( 'base-styles' , {
variants : {
variant : {
default : 'bg-primary-green-600 text-primary-foreground hover:bg-primary-green-700' ,
destructive : 'bg-destructive text-white hover:bg-destructive/90' ,
outline : 'border bg-background shadow-xs hover:bg-accent' ,
secondary : 'bg-secondary text-secondary-foreground' ,
ghost : 'hover:bg-accent hover:text-accent-foreground' ,
link : 'text-primary underline-offset-4 hover:underline' ,
} ,
size : {
default : 'h-9 px-4 py-2' ,
sm : 'h-8 rounded-md gap-1.5 px-3' ,
lg : 'h-10 rounded-md px-6' ,
icon : 'size-9' ,
} ,
} ,
} ) ;
2. ํผ ํจํด (React Hook Form)
const form = useForm ( {
defaultValues : { email : '' , password : '' } ,
} ) ;
< Form { ...form } >
< form onSubmit = { form . handleSubmit ( onSubmit ) } >
< FormField
control = { form . control}
name = "email"
rules = { {
required : { value : true , message : '์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์.' } ,
pattern : { value : emailReg , message : '์ด๋ฉ์ผ ํ์์ด ์๋ชป๋์์ต๋๋ค.' } ,
} }
render = { ( { field } ) => (
< FormItem >
< FormLabel > ์ด๋ฉ์ผ < / F o r m L a b e l >
< FormControl >
< Input { ...field } placeholder = "์ด๋ฉ์ผ์ ์
๋ ฅํด์ฃผ์ธ์." / >
< / F o r m C o n t r o l >
< FormMessage / >
< / FormItem >
) }
/ >
< / form >
< / Form >
1. ์ฟ ํค ๊ธฐ๋ฐ ํ ํฐ ์ ์ฅ
storage: createJSONStorage ( ( ) => cookieStorage ) ;
NEXT_PUBLIC_BACK_BASE_URL - API ๊ธฐ๋ณธ URL
// next.config.ts
rewrites: async ( ) => {
return [
{
source : '/api/:path*' ,
destination : process . env . NEXT_PUBLIC_BACK_BASE_URL + '/api/:path*' ,
} ,
] ;
} ;
์ฌ์ฉ์ ํ์
๋ณ ๋ถ๋ฆฌ
/creator - ํฌ๋ฆฌ์์ดํฐ ์ ์ฉ
/learner - ๋ฌ๋ ์ ์ฉ
/login/[userType] - ๋์ ๋ผ์ฐํธ
RootLayout (์ ์ญ)
โโ AuthorizedLayout (์ธ์ฆ ํ์)
โโ Header
โโ Main Content
โโ Footer
๐ ๏ธ ๊ฐ๋ฐ ๋๊ตฌ ์ค์
next/core-web-vitals ํ์ฅ
next/typescript ํ์ฅ
@tailwindcss/postcss - Tailwind v4 ํ๋ฌ๊ทธ์ธ
tw-animate-css - Tailwind ์ ๋๋ฉ์ด์
ํ์ฅ
๐ ์ฃผ์ ํน์ง ์์ฝ
๋ชจ๋ํ๋ ์๋น์ค ๋ ์ด์ด : ๋๋ฉ์ธ๋ณ๋ก service/hook/type 3๋จ ๊ตฌ์กฐ
ํ์
์์ ์ฑ : ๋ชจ๋ API Response์ ์ ๋ค๋ฆญ ํ์
์ ์ฉ
์๋ ํ ํฐ ๊ฐฑ์ : Interceptor ํจํด์ผ๋ก ๋ฌด์ค๋จ ์ธ์ฆ
์ปดํฌ๋ํธ ์ฌ์ฌ์ฉ์ฑ : shadcn/ui + cva๋ก ๋ณํ ๊ด๋ฆฌ
์ผ๊ด๋ ์ฝ๋ฉ ์คํ์ผ : Prettier + ESLint ์๋ ํฌ๋งทํ
์ฌ์ฉ์ ํ์
๋ถ๋ฆฌ : Creator/Learner ๋ณ๋ ๋ผ์ฐํ
๋คํฌ๋ชจ๋ ์ง์ : CSS ๋ณ์ ๊ธฐ๋ฐ ํ
๋ง ์์คํ
์ต์ Next.js : App Router + Turbopack