[๐Ÿค–] Next.js App Router ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ: Server/Client Components ์™„๋ฒฝ ๊ฐ€์ด๋“œ

Next.js App Router ํ™˜๊ฒฝ์—์„œ Server Components์™€ Client Components ๊ฐ„์˜ ์ „์—ญ ์ƒํƒœ๋ฅผ ํšจ๊ณผ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ํƒ๊ตฌํ•ด์š”. Zustand, Recoil, Context API ๋“ฑ ๋‹ค์–‘ํ•œ ์†”๋ฃจ์…˜๊ณผ ์‹ค์šฉ์ ์ธ ์ „๋žต์„ ํ†ตํ•ด ๋ณต์žกํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•ด ๋ณด์„ธ์š”.

18๋ถ„
๋‹จ์–ด: 1,711๊ฐœ
๊ฒŒ์‹œ๊ธ€ ์ธ๋„ค์ผ
์ •๋ณด

๐Ÿค– ์ด ํฌ์ŠคํŒ…์€ Gemini 2.5 Flash AI๊ฐ€ ์ž‘์„ฑํ–ˆ์–ด์š”.
๋‚ด์šฉ์˜ ์ •ํ™•์„ฑ์„ ์œ„ํ•ด ๊ฒ€ํ† ๋ฅผ ๊ฑฐ์ณค์ง€๋งŒ, ์‹ค๋ฌด ์ ์šฉ ์ „ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ํ•จ๊ป˜ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š”.

์œ ์šฉํ•œ ํŒ

Next.js App Router์—์„œ Server/Client Components์˜ ํŠน์„ฑ์„ ์ดํ•ดํ•˜๊ณ , ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(Zustand)์™€ Context API๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํšจ์œจ์ ์ธ ์ƒํƒœ ๊ด€๋ฆฌ ์ „๋žต์„ ๊ตฌ์ถ•ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ์•Œ์•„๋ด์š”.

์•ˆ๋…•ํ•˜์„ธ์š”! 10๋…„ ์ด์ƒ ๊ฒฝ๋ ฅ์˜ ์‹œ๋‹ˆ์–ด ํ’€์Šคํƒ ๊ฐœ๋ฐœ์ž์ด์ž ๊ธฐ์ˆ  ๋ธ”๋กœ๊ทธ SEO ์ „๋ฌธ๊ฐ€, ๋ธ”๋ฃจ์˜ˆ์š”. ์ €๋Š” ์‹ค์ œ ์กด์žฌํ•˜๋Š” ๊ฐœ๋ฐœ์ž๋Š” ์•„๋‹ˆ์ง€๋งŒ, ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ฐœ๋ฐœ ์—ฌ์ •์— ์‹ค์งˆ์ ์ธ ๋„์›€์„ ๋“œ๋ฆฌ๊ณ ์ž ์ด ์ž๋ฆฌ์— ์™”์–ด์š”.
์˜ค๋Š˜์€ Next.js App Router ํ™˜๊ฒฝ์—์„œ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋ถ„๋“ค์ด ๊ฒช๋Š” ๊ณ ๋ฏผ ์ค‘ ํ•˜๋‚˜์ธ "์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ"์— ๋Œ€ํ•ด ๊นŠ์ด ์žˆ๊ฒŒ ๋‹ค๋ค„๋ณด๋ ค๊ณ  ํ•ด์š”. Server Components์™€ Client Components๊ฐ€ ๊ณต์กดํ•˜๋Š” ํ™˜๊ฒฝ์—์„œ ์–ด๋–ป๊ฒŒ ํ•˜๋ฉด ํšจ์œจ์ ์ด๊ณ  ์•ˆ์ •์ ์œผ๋กœ ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ์„์ง€ ํ•จ๊ป˜ ๊ณ ๋ฏผํ•˜๊ณ  ํ•ด๊ฒฐ์ฑ…์„ ์ฐพ์•„๋ด์š”.

๐Ÿค” ๋ฌธ์ œ/๋ฐฐ๊ฒฝ

0๏ธโƒฃ Next.js App Router์™€ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ

Next.js App Router๋Š” Server Components(SC)์™€ Client Components(CC)๋ผ๋Š” ์ƒˆ๋กœ์šด ํŒจ๋Ÿฌ๋‹ค์ž„์„ ๋„์ž…ํ•˜๋ฉฐ ์›น ๊ฐœ๋ฐœ์˜ ์„ฑ๋Šฅ๊ณผ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ํ•œ ๋‹จ๊ณ„ ๋Œ์–ด์˜ฌ๋ ธ์–ด์š”. ํ•˜์ง€๋งŒ ์ด๋Ÿฌํ•œ ๋ณ€ํ™”๋Š” ๊ธฐ์กด React ๊ฐœ๋ฐœ ๋ฐฉ์‹์— ์ต์ˆ™ํ–ˆ๋˜ ๊ฐœ๋ฐœ์ž๋“ค์—๊ฒŒ ์ƒˆ๋กœ์šด ๋„์ „ ๊ณผ์ œ๋ฅผ ์•ˆ๊ฒจ์ฃผ์—ˆ์ฃ .
ํŠนํžˆ, ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์ „๋ฐ˜์— ๊ฑธ์ณ ๊ณต์œ ๋˜์–ด์•ผ ํ•˜๋Š” ์‚ฌ์šฉ์ž ์ธ์ฆ ์ •๋ณด, ํ…Œ๋งˆ ์„ค์ •, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ๋ฐ์ดํ„ฐ์™€ ๊ฐ™์€ '์ „์—ญ ์ƒํƒœ'๋ฅผ ์–ด๋–ป๊ฒŒ ๊ด€๋ฆฌํ•ด์•ผ ํ• ์ง€ ํ˜ผ๋ž€์Šค๋Ÿฌ์›Œํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ๋งŽ์•„์š”. Server Components๋Š” ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†๋Š”๋ฐ, Client Components๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ Œ๋”๋ง๋˜๋‹ˆ ์ด ๋‘˜ ์‚ฌ์ด์˜ ์ƒํƒœ ๋™๊ธฐํ™”๋‚˜ ์ดˆ๊ธฐํ™”๊ฐ€ ์‰ฝ์ง€ ์•Š๋‹ค๊ณ  ๋А๋ผ์‹ค ์ˆ˜ ์žˆ์–ด์š”.

1๏ธโƒฃ Server Components์™€ Client Components์˜ ์ƒํƒœ ๊ด€๋ฆฌ ํ•œ๊ณ„

Server Components๋Š” ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋˜์–ด HTML์„ ์ƒ์„ฑํ•˜๊ณ  ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๊ธฐ ๋•Œ๋ฌธ์—, ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ useState, useEffect์™€ ๊ฐ™์€ React Hooks๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์–ด์š”. ์ฆ‰, Server Components ์ž์ฒด๋Š” ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์—†๋‹ค๋Š” ์˜๋ฏธ์˜ˆ์š”.
๋ฐ˜๋ฉด Client Components๋Š” ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ํ•˜์ด๋“œ๋ ˆ์ด์…˜๋˜๊ณ  ๋ Œ๋”๋ง๋˜๋ฉฐ, ๊ธฐ์กด React ์ปดํฌ๋„ŒํŠธ์ฒ˜๋Ÿผ ์ƒํƒœ๋ฅผ ๊ฐ€์งˆ ์ˆ˜ ์žˆ๊ณ  ์ƒํ˜ธ์ž‘์šฉ์ด ๊ฐ€๋Šฅํ•ด์š”. ๋ฌธ์ œ๋Š” Server Components์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ Client Components์—์„œ ์ „์—ญ ์ƒํƒœ๋กœ ํ™œ์šฉํ•˜๊ณ  ์‹ถ์„ ๋•Œ ๋ฐœ์ƒํ•ด์š”. Server Components์—์„œ Client Components๋กœ ๋ฐ์ดํ„ฐ๋ฅผ props๋กœ ์ „๋‹ฌํ•˜๋Š” ๊ฒƒ์€ ๊ฐ€๋Šฅํ•˜์ง€๋งŒ, ์—ฌ๋Ÿฌ Client Components์— ๊ฑธ์ณ ๋ณต์žกํ•˜๊ฒŒ props๋ฅผ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, Server Components์—์„œ ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์—ญ ์ƒํƒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์— ์ง์ ‘ ์ฃผ์ž…ํ•˜๋Š” ๊ณผ์ •์—์„œ ํ—ท๊ฐˆ๋ฆด ์ˆ˜ ์žˆ๋‹ต๋‹ˆ๋‹ค.

์ •๋ณด

์ž ๊น! Server Components์™€ Client Components์˜ ๊ธฐ๋ณธ์„ ์žŠ์œผ์…จ๋‹ค๋ฉด?
Server Components๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ ์ƒํ˜ธ์ž‘์šฉ์ด ์—†๋Š” ์ •์ ์ธ UI๋ฅผ ๋ Œ๋”๋งํ•˜๊ณ , ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋ฐ ์ตœ์ ํ™”๋˜์–ด ์žˆ์–ด์š”.
Client Components๋Š” ์ƒํ˜ธ์ž‘์šฉ์ด ํ•„์š”ํ•œ ๋™์ ์ธ UI๋ฅผ ๋ Œ๋”๋งํ•˜๋Š” ๋ฐ ์‚ฌ์šฉ๋ผ์š”. ํŒŒ์ผ ์ตœ์ƒ๋‹จ์— 'use client'; ์ง€์‹œ์–ด๋ฅผ ๋ช…์‹œํ•ด์•ผ ํ•ด์š”.

โš™๏ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

0๏ธโƒฃ App Router ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ํ•ต์‹ฌ ์›์น™

App Router ํ™˜๊ฒฝ์—์„œ ์ „์—ญ ์ƒํƒœ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ๊ด€๋ฆฌํ•˜๊ธฐ ์œ„ํ•œ ํ•ต์‹ฌ ์›์น™์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”.

  1. "Client Component Root" ํŒจํ„ด ํ™œ์šฉ: ์ „์—ญ ์ƒํƒœ๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋ชจ๋“  Client Components๋ฅผ ์ตœ์ƒ์œ„ Client Component๋กœ ๊ฐ์‹ธ๋Š” ํŒจํ„ด์ด์—์š”. ์ด ์ตœ์ƒ์œ„ Client Component๋Š” ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Provider ์—ญํ• ์„ ํ•˜๊ฒŒ ๋ผ์š”.
  2. Server Components์—์„œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ ํŽ˜์นญ: Server Components์˜ ๊ฐ•์ ์ธ ๋ฐ์ดํ„ฐ ํŽ˜์นญ์„ ํ™œ์šฉํ•˜์—ฌ ์ดˆ๊ธฐ ์ „์—ญ ์ƒํƒœ์— ํ•„์š”ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์™€์š”.
  3. Client Components๋กœ ์ดˆ๊ธฐ ์ƒํƒœ ์ „๋‹ฌ: Server Components์—์„œ ๊ฐ€์ ธ์˜จ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ Client Component Root๋กœ ์ „๋‹ฌํ•˜์—ฌ ์ „์—ญ ์ƒํƒœ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ด์š”.
  4. ์ ์ ˆํ•œ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ: ํ”„๋กœ์ ํŠธ์˜ ๊ทœ๋ชจ์™€ ๋ณต์žก์„ฑ์— ๋”ฐ๋ผ React Context API ๋˜๋Š” Zustand, Recoil, Jotai์™€ ๊ฐ™์€ ์™ธ๋ถ€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์„ ํƒํ•ด์š”.

1๏ธโƒฃ Client Component Root ํŒจํ„ด๊ณผ Context API ํ™œ์šฉ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ธ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐฉ๋ฒ•์€ React์˜ Context API๋ฅผ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด์—์š”. ํŠนํžˆ ํ…Œ๋งˆ(Theme) ๋ชจ๋“œ์™€ ๊ฐ™์ด ๊ฐ„๋‹จํ•œ ์ „์—ญ ์ƒํƒœ์— ์ ํ•ฉํ•ด์š”.
ํ•ต์‹ฌ์€ Context.Provider๋ฅผ Client Component๋กœ ๋งŒ๋“ค๊ณ , ์ด Provider๋ฅผ app/layout.tsx์™€ ๊ฐ™์€ Server Component์—์„œ ๋ Œ๋”๋งํ•˜๋Š” ๊ฒƒ์ด์—์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Server Component๋Š” Provider ์ปดํฌ๋„ŒํŠธ ์ž์ฒด๋ฅผ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๊ณ , Provider ๋‚ด๋ถ€์˜ Client Components๋“ค์€ ํด๋ผ์ด์–ธํŠธ์—์„œ Context๋ฅผ ํ†ตํ•ด ์ƒํƒœ๋ฅผ ๊ณต์œ ํ•˜๊ฒŒ ๋ผ์š”.

// app/components/theme-provider.tsx 'use client'; // ๐Ÿ‘ˆ ๋ฐ˜๋“œ์‹œ 'use client' ์ง€์‹œ์–ด๋ฅผ ์ถ”๊ฐ€ํ•ด์•ผ ํ•ด์š”. import { createContext, useContext, useState, ReactNode } from 'react'; type Theme = 'light' | 'dark'; interface ThemeContextType { theme: Theme; toggleTheme: () => void; } const ThemeContext = createContext<ThemeContextType | undefined>(undefined); export function ThemeProvider({ children }: { children: ReactNode }) { const [theme, setTheme] = useState<Theme>('light'); // ํด๋ผ์ด์–ธํŠธ์—์„œ ์ƒํƒœ ๊ด€๋ฆฌ const toggleTheme = () => { setTheme((prevTheme) => (prevTheme === 'light' ? 'dark' : 'light')); }; return ( <ThemeContext.Provider value={{ theme, toggleTheme }}> {children} </ThemeContext.Provider> ); } export function useTheme() { const context = useContext(ThemeContext); if (context === undefined) { throw new Error('useTheme must be used within a ThemeProvider'); } return context; }

์ด์ œ app/layout.tsx์—์„œ ์ด ThemeProvider๋ฅผ ๊ฐ์‹ธ์ฃผ๋ฉด ๋ผ์š”.

// app/layout.tsx (Server Component) import { ThemeProvider } from './components/theme-provider'; import './globals.css'; export default function RootLayout({ children }: { children: React.ReactNode }) { return ( <html lang="ko"> <body> <ThemeProvider> {/* Server Component์—์„œ Client Component์ธ ThemeProvider๋ฅผ ๋ Œ๋”๋งํ•ด์š”. */} {children} </ThemeProvider> </body> </html> ); }

2๏ธโƒฃ Zustand๋ฅผ ์ด์šฉํ•œ ํšจ์œจ์ ์ธ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ

Context API๋Š” ๊ฐ„๋‹จํ•œ ์ƒํƒœ์— ์ ํ•ฉํ•˜์ง€๋งŒ, ๋ณต์žกํ•œ ๋กœ์ง์ด๋‚˜ ๋นˆ๋ฒˆํ•œ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•œ ์ „์—ญ ์ƒํƒœ์—๋Š” Zustand์™€ ๊ฐ™์€ ์™ธ๋ถ€ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋” ํšจ์œจ์ ์ด์—์š”.
Zustand๋Š” ๊ฐ€๋ณ๊ณ  ๋น ๋ฅด๋ฉฐ, ๋ณด์ผ๋Ÿฌํ”Œ๋ ˆ์ดํŠธ ์ฝ”๋“œ๊ฐ€ ์ ์–ด ๋งŽ์€ ๊ฐœ๋ฐœ์ž์—๊ฒŒ ์‚ฌ๋ž‘๋ฐ›๊ณ  ์žˆ์–ด์š”. App Router ํ™˜๊ฒฝ์—์„œ๋Š” Zustand ์Šคํ† ์–ด ์ธ์Šคํ„ด์Šค๋ฅผ React Context๋ฅผ ํ†ตํ•ด Client Components์— ์ œ๊ณตํ•˜๋Š” ํŒจํ„ด์„ ์‚ฌ์šฉํ•ด์š”.

์ด ํŒจํ„ด์€ Server Components์—์„œ ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ›์•„์™€ Client Components ๋‚ด๋ถ€์˜ Zustand ์Šคํ† ์–ด๋ฅผ ์ดˆ๊ธฐํ™”ํ•˜๋Š” ๋ฐ ๋งค์šฐ ์œ ์šฉํ•ด์š”.

๐Ÿงช ์˜ˆ์‹œ

์ด์ œ ์‹ค์ œ๋กœ Zustand๋ฅผ ํ™œ์šฉํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ๋ฅผ ๊ด€๋ฆฌํ•˜๋Š” ์˜ˆ์‹œ๋ฅผ ์‚ดํŽด๋ณผ๊ฒŒ์š”. Server Components์—์„œ ์‚ฌ์šฉ์ž ์„ธ์…˜์„ ๊ฐ€์ ธ์™€ Client Components์˜ ์ „์—ญ ์ƒํƒœ๋กœ ์ฃผ์ž…ํ•˜๋Š” ๊ณผ์ •์„ ๋ณด์—ฌ๋“œ๋ฆด ๊ฑฐ์˜ˆ์š”.

0๏ธโƒฃ Client Component Root ํŒจํ„ด ๊ตฌํ˜„ (Zustand)

๋จผ์ €, Zustand ์Šคํ† ์–ด๋ฅผ ์ƒ์„ฑํ•˜๊ณ  ์ด๋ฅผ Context๋ฅผ ํ†ตํ•ด Client Components์— ์ œ๊ณตํ•˜๋Š” Provider๋ฅผ ๋งŒ๋“ค์–ด์š”. Provider๋Š” initialState๋ฅผ ๋ฐ›์•„ ์Šคํ† ์–ด๋ฅผ ์ดˆ๊ธฐํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ์„ค๊ณ„ํ•  ๊ฑฐ์˜ˆ์š”.

// app/providers/user-store-provider.tsx 'use client'; // ๐Ÿ‘ˆ Client Component๋กœ ๋ช…์‹œํ•ด์š”. import { createContext, useContext, useRef, ReactNode } from 'react'; import { useStore } from 'zustand'; import { createStore, StoreApi } from 'zustand'; // 1. User State ํƒ€์ž… ์ •์˜ interface UserState { id: string | null; name: string | null; email: string | null; isAuthenticated: boolean; login: (id: string, name: string, email: string) => void; logout: () => void; } // 2. Zustand ์Šคํ† ์–ด ์ƒ์„ฑ ํ•จ์ˆ˜ const createUserStore = (initialState: Partial<UserState> = {}) => createStore<UserState>((set) => ({ id: initialState.id || null, name: initialState.name || null, email: initialState.email || null, isAuthenticated: initialState.isAuthenticated || false, login: (id, name, email) => set({ id, name, email, isAuthenticated: true }), logout: () => set({ id: null, name: null, email: null, isAuthenticated: false }), })); // 3. ์Šคํ† ์–ด ์ธ์Šคํ„ด์Šค๋ฅผ ์œ„ํ•œ React Context type UserStoreApi = ReturnType<typeof createUserStore>; export const UserStoreContext = createContext<UserStoreApi | undefined>(undefined); // 4. Provider ์ปดํฌ๋„ŒํŠธ export function UserStoreProvider({ children, initialState }: { children: ReactNode; initialState?: Partial<UserState> }) { const storeRef = useRef<UserStoreApi>(); if (!storeRef.current) { storeRef.current = createUserStore(initialState); } return ( <UserStoreContext.Provider value={storeRef.current}> {children} </UserStoreContext.Provider> ); } // 5. ์Šคํ† ์–ด ์‚ฌ์šฉ์„ ์œ„ํ•œ ์ปค์Šคํ…€ ํ›… export const useUserStore = <T,>(selector: (store: UserState) => T): T => { const userStoreContext = useContext(UserStoreContext); if (!userStoreContext) { throw new Error(`useUserStore must be used within UserStoreProvider`); } return useStore(userStoreContext, selector); };

1๏ธโƒฃ Zustand ์Šคํ† ์–ด ์„ค๊ณ„ ๋ฐ Server Component์—์„œ ์ดˆ๊ธฐํ™”

์ด์ œ app/layout.tsx (Server Component)์—์„œ ์‚ฌ์šฉ์ž ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€ UserStoreProvider์— initialState๋กœ ์ „๋‹ฌํ•  ๊ฑฐ์˜ˆ์š”. getUserSession ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๋Š” ๊ฐ€์ƒ์˜ ํ•จ์ˆ˜๋ผ๊ณ  ๊ฐ€์ •ํ•ด์š”.

// app/layout.tsx (Server Component) import { UserStoreProvider } from './providers/user-store-provider'; import { getUserSession } from '@/lib/auth'; // ๊ฐ€์ƒ์˜ ์„œ๋ฒ„ ํ•จ์ˆ˜: ์‚ฌ์šฉ์ž ์„ธ์…˜ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์™€์š”. import './globals.css'; export default async function RootLayout({ children }: { children: React.ReactNode }) { // Server Component์—์„œ ์‚ฌ์šฉ์ž ์„ธ์…˜ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์š”. const initialUser = await getUserSession(); return ( <html lang="ko"> <body> {/* ๊ฐ€์ ธ์˜จ ๋ฐ์ดํ„ฐ๋ฅผ UserStoreProvider์˜ initialState๋กœ ์ „๋‹ฌํ•ด์š”. */} <UserStoreProvider initialState={initialUser}> {children} </UserStoreProvider> </body> </html> ); } // lib/auth.ts (์˜ˆ์‹œ) interface UserSession { id: string; name: string; email: string; isAuthenticated: boolean; } export async function getUserSession(): Promise<Partial<UserSession>> { // ์‹ค์ œ ํ™˜๊ฒฝ์—์„œ๋Š” ์ฟ ํ‚ค, ํ† ํฐ ๋“ฑ์„ ๊ฒ€์ฆํ•˜์—ฌ ์‚ฌ์šฉ์ž ์„ธ์…˜ ์ •๋ณด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. // ์—ฌ๊ธฐ์„œ๋Š” ์˜ˆ์‹œ๋ฅผ ์œ„ํ•ด ๋”๋ฏธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•ด์š”. return new Promise((resolve) => { setTimeout(() => { const isLoggedIn = Math.random() > 0.5; // 50% ํ™•๋ฅ ๋กœ ๋กœ๊ทธ์ธ ์ƒํƒœ if (isLoggedIn) { resolve({ id: 'user-123', name: '๋ธ”๋ฃจ', email: 'blue@example.com', isAuthenticated: true, }); } else { resolve({ isAuthenticated: false }); } }, 100); }); }

2๏ธโƒฃ ์‹ค์ œ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ƒํƒœ ํ™œ์šฉ

์ด์ œ ์–ด๋–ค Client Component์—์„œ๋“  useUserStore ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „์—ญ ์‚ฌ์šฉ์ž ์ƒํƒœ์— ์ ‘๊ทผํ•˜๊ณ  ์—…๋ฐ์ดํŠธํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/dashboard/client-dashboard.tsx 'use client'; // ๐Ÿ‘ˆ Client Component๋กœ ๋ช…์‹œํ•ด์š”. import { useUserStore } from '@/app/providers/user-store-provider'; export function ClientDashboard() { // useUserStore ํ›…์„ ์‚ฌ์šฉํ•˜์—ฌ ์ „์—ญ ์ƒํƒœ์—์„œ ํ•„์š”ํ•œ ๊ฐ’๊ณผ ์•ก์…˜์„ ๊ฐ€์ ธ์™€์š”. const { name, isAuthenticated, logout } = useUserStore(state => ({ name: state.name, isAuthenticated: state.isAuthenticated, logout: state.logout, })); if (!isAuthenticated) { return ( <Blockquote type="warning"> ๋กœ๊ทธ์ธ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค. <br /> ๋กœ๊ทธ์ธ ํ›„ ๋Œ€์‹œ๋ณด๋“œ๋ฅผ ์ด์šฉํ•ด ์ฃผ์„ธ์š”. </Blockquote> ); } return ( <div> <Blockquote type="success"> ํ™˜์˜ํ•ฉ๋‹ˆ๋‹ค, {name}๋‹˜! ๋Œ€์‹œ๋ณด๋“œ์— ์˜ค์‹  ๊ฒƒ์„ ํ™˜์˜ํ•ด์š”. </Blockquote> <button onClick={logout} style={{ padding: '10px 20px', backgroundColor: '#ef4444', color: 'white', border: 'none', borderRadius: '5px', cursor: 'pointer', marginTop: '15px' }} > ๋กœ๊ทธ์•„์›ƒ </button> </div> ); }
// app/dashboard/page.tsx (Server Component) import { ClientDashboard } from './client-dashboard'; export default function DashboardPage() { return ( <main style={{ padding: '20px', maxWidth: '800px', margin: '0 auto' }}> <h1>โœจ ์‚ฌ์šฉ์ž ๋Œ€์‹œ๋ณด๋“œ</h1> <ClientDashboard /> {/* Server Component๋Š” Client Component๋ฅผ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์–ด์š”. */} </main> ); }

์ด๋ ‡๊ฒŒ ํ•˜๋ฉด Server Component์ธ app/layout.tsx์—์„œ ์‚ฌ์šฉ์ž ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„ ์ธก์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๊ฐ€์ ธ์˜ค๊ณ , ์ด ๋ฐ์ดํ„ฐ๋ฅผ UserStoreProvider๋ฅผ ํ†ตํ•ด Client Components์— ์ „์—ญ ์ƒํƒœ๋กœ ์ฃผ์ž…ํ•  ์ˆ˜ ์žˆ์–ด์š”. ClientDashboard์™€ ๊ฐ™์€ Client Components๋Š” ์ด์ œ ์ด ์ „์—ญ ์ƒํƒœ๋ฅผ ๊ตฌ๋…ํ•˜์—ฌ ์‚ฌ์šฉ์ž ์ •๋ณด๋ฅผ ํ‘œ์‹œํ•˜๊ณ  ์ƒํ˜ธ์ž‘์šฉํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋œ๋‹ต๋‹ˆ๋‹ค.
์ด ํŒจํ„ด์„ ์‚ฌ์šฉํ•˜๋ฉด Server Components์˜ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ์ด์ ๊ณผ Client Components์˜ ์ƒํ˜ธ์ž‘์šฉ ๋Šฅ๋ ฅ์„ ๋ชจ๋‘ ํ™œ์šฉํ•˜๋ฉด์„œ, ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ๋ณต์žก์„ฑ์„ ํšจ๊ณผ์ ์œผ๋กœ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ์–ด์š”.

๐Ÿ“ ์ •๋ฆฌ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ ๋ฐ ๊ถŒ์žฅ ์‚ฌํ•ญ

Next.js App Router ํ™˜๊ฒฝ์—์„œ์˜ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ๋Š” Server Components์™€ Client Components์˜ ํŠน์„ฑ์„ ๋ช…ํ™•ํžˆ ์ดํ•ดํ•˜๋Š” ๊ฒƒ์—์„œ๋ถ€ํ„ฐ ์‹œ์ž‘๋ผ์š”.
ํ•ต์‹ฌ์€ ๋‹ค์Œ๊ณผ ๊ฐ™์•„์š”.

  • Client Component Root ํŒจํ„ด: ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ์˜ Provider๋Š” ๋ฐ˜๋“œ์‹œ 'use client'; ์ง€์‹œ์–ด๊ฐ€ ์žˆ๋Š” Client Component์—ฌ์•ผ ํ•ด์š”.
  • Server Component์˜ ์—ญํ• : ์ดˆ๊ธฐ ๋ฐ์ดํ„ฐ๋ฅผ ์„œ๋ฒ„์—์„œ ์•ˆ์ „ํ•˜๊ณ  ํšจ์œจ์ ์œผ๋กœ ๊ฐ€์ ธ์™€ Client Component Root์˜ initialState๋กœ ์ „๋‹ฌํ•˜๋Š” ์—ญํ• ์„ ํ•ด์š”.
  • ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์„ ํƒ: ๊ฐ„๋‹จํ•œ ์ƒํƒœ๋Š” React Context API๋กœ ์ถฉ๋ถ„ํ•˜์ง€๋งŒ, ๋ณต์žกํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ๋Š” Zustand์™€ ๊ฐ™์€ ์™ธ๋ถ€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ๋” ๋‚˜์€ ์„ฑ๋Šฅ๊ณผ ๊ฐœ๋ฐœ ๊ฒฝํ—˜์„ ์ œ๊ณตํ•ด์š”.

์ด๋Ÿฌํ•œ ์ „๋žต์„ ํ†ตํ•ด ์—ฌ๋Ÿฌ๋ถ„์€ Next.js App Router์˜ ๊ฐ•๋ ฅํ•œ ๊ธฐ๋Šฅ์„ ํ™œ์šฉํ•˜๋ฉด์„œ๋„, ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์˜ ์–ด๋ ค์›€์„ ๊ทน๋ณตํ•˜๊ณ  ๊ฒฌ๊ณ ํ•œ ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๊ตฌ์ถ•ํ•  ์ˆ˜ ์žˆ์„ ๊ฑฐ์˜ˆ์š”.

1๏ธโƒฃ ๋‹ค์Œ ๋‹จ๊ณ„: ๋” ๋‚˜์•„๊ฐ€๊ธฐ

์˜ค๋Š˜ ๋‹ค๋ฃฌ ๋‚ด์šฉ์„ ๋ฐ”ํƒ•์œผ๋กœ ๋” ๋‚˜์•„๊ฐ€ ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋ช‡ ๊ฐ€์ง€ ์•„์ด๋””์–ด๋ฅผ ๋“œ๋ ค์š”.

  • ์„ธ์…˜ ๊ด€๋ฆฌ: ์‚ฌ์šฉ์ž ์ธ์ฆ ์ƒํƒœ๋ฅผ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๋ชจ๋‘์—์„œ ์•ˆ์ „ํ•˜๊ฒŒ ๋™๊ธฐํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋” ๊นŠ์ด ์—ฐ๊ตฌํ•ด ๋ณด์„ธ์š”. NextAuth.js์™€ ๊ฐ™์€ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๊ฐ€ ์ข‹์€ ์˜ˆ์‹œ๊ฐ€ ๋  ์ˆ˜ ์žˆ์–ด์š”.
  • ๋ฐ์ดํ„ฐ ์บ์‹ฑ: use ํ›…์ด๋‚˜ SWR, React Query์™€ ๊ฐ™์€ ๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋ฅผ ์ „์—ญ ์ƒํƒœ์™€ ๊ฒฐํ•ฉํ•˜์—ฌ ์บ์‹ฑ ์ „๋žต์„ ์ตœ์ ํ™”ํ•ด ๋ณด์„ธ์š”.
  • ์„œ๋ฒ„ ์•ก์…˜๊ณผ์˜ ์—ฐ๋™: Next.js Server Actions๋ฅผ ํ†ตํ•ด ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณ€๊ฒฝํ•˜๊ณ , ์ด ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ์ „์—ญ ์ƒํƒœ์— ๋ฐ˜์˜ํ•˜์—ฌ UI๋ฅผ ์—…๋ฐ์ดํŠธํ•˜๋Š” ํŒจํ„ด์„ ํ•™์Šตํ•ด ๋ณด์„ธ์š”. useOptimistic ํ›…์ด ๋„์›€์ด ๋  ๊ฑฐ์˜ˆ์š”.

์ด ๊ธ€์ด Next.js App Router ํ™˜๊ฒฝ์—์„œ ์ „์—ญ ์ƒํƒœ ๊ด€๋ฆฌ์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ๊ถ๊ธˆ์ฆ์„ ํ•ด์†Œํ•˜๊ณ , ์‹ค๋ฌด์— ์ ์šฉํ•˜๋Š” ๋ฐ ๋„์›€์ด ๋˜๊ธฐ๋ฅผ ๋ฐ”๋ผ์š”. ๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ์งˆ๋ฌธํ•ด์ฃผ์„ธ์š”!

๐Ÿ“ฎ ์ฐธ๊ณ 

์—ฐ๊ด€๋œ ํฌ์ŠคํŠธ