[๐Ÿค–] Next.js App Router: Server Components์™€ Client Components, ์™„๋ฒฝ ์ •๋ณตํ•ด์š”!

Next.js 13+ App Router์—์„œ Server Components์™€ Client Components๊ฐ€ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๊ณ , ์–ธ์ œ ์–ด๋–ค ์ปดํฌ๋„ŒํŠธ๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š”์ง€ ๋ช…ํ™•ํžˆ ์ดํ•ดํ•˜์—ฌ ์„ฑ๋Šฅ ์ตœ์ ํ™”์™€ ํšจ์œจ์ ์ธ ๊ฐœ๋ฐœ์„ ์ด๋ฃจ๋Š” ๋ฐฉ๋ฒ•์„ ์ž์„ธํžˆ ์•Œ์•„๋ด์š”.

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

Next.js 13+ App Router์˜ ํ•ต์‹ฌ ๊ฐœ๋…์ธ Server Components์™€ Client Components๋ฅผ ๊นŠ์ด ์žˆ๊ฒŒ ์ดํ•ดํ•˜๊ณ , ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์˜ฌ๋ฐ”๋ฅด๊ฒŒ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ๋ฐฐ์›Œ๋ด์š”.

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

0๏ธโƒฃ ์™œ ์ด ์ฃผ์ œ๋ฅผ ๋‹ค๋ฃจ๋Š”๊ฐ€

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

1๏ธโƒฃ ๊ธฐ์กด ๋ฐฉ์‹์˜ ํ•œ๊ณ„

๊ธฐ์กด์˜ React ๊ฐœ๋ฐœ์ด๋‚˜ Next.js Pages Router ํ™˜๊ฒฝ์—์„œ๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๊ธฐ๋ณธ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋ง๋˜์—ˆ์–ด์š”. ์„œ๋ฒ„ ๋ Œ๋”๋ง(SSR)๋„ ๊ฒฐ๊ตญ์€ ์„œ๋ฒ„์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฏธ๋ฆฌ ๋ Œ๋”๋งํ•˜์—ฌ HTML์„ ์ƒ์„ฑํ•œ ํ›„, ํด๋ผ์ด์–ธํŠธ์—์„œ ๋‹ค์‹œ ํ•˜์ด๋“œ๋ ˆ์ด์…˜(Hydration) ๊ณผ์ •์„ ๊ฑฐ์ณ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๋งŒ๋“œ๋Š” ๋ฐฉ์‹์ด์—ˆ์ฃ . ์ด๋Ÿฌํ•œ ๋ฐฉ์‹์€ ๋‹ค์Œ๊ณผ ๊ฐ™์€ ํ•œ๊ณ„์ ์„ ๊ฐ€์ง€๊ณ  ์žˆ์—ˆ์–ด์š”.

  • ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ฆ๊ฐ€: ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ๋กœ์ง์ด ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์–ด ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†ก๋˜์—ˆ์–ด์š”. ์ด๋Š” ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ๊ฐ„์„ ๊ธธ๊ฒŒ ๋งŒ๋“ค๊ณ , ํŠนํžˆ ์ €์‚ฌ์–‘ ๊ธฐ๊ธฐ๋‚˜ ๋„คํŠธ์›Œํฌ ํ™˜๊ฒฝ์—์„œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ €ํ•ดํ•˜๋Š” ์›์ธ์ด ๋  ์ˆ˜ ์žˆ์—ˆ์ฃ .
  • ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ ์ €ํ•˜: ํด๋ผ์ด์–ธํŠธ ์ธก์—์„œ ๋ชจ๋“  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ๋‹ค์šด๋กœ๋“œํ•˜๊ณ  ์‹คํ–‰ํ•ด์•ผ๋งŒ ํŽ˜์ด์ง€๊ฐ€ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•ด์ง€๋ฏ€๋กœ, First Contentful Paint(FCP)๋‚˜ Largest Contentful Paint(LCP) ๊ฐ™์€ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์ง€ํ‘œ์— ๋ถ€์ •์ ์ธ ์˜ํ–ฅ์„ ์ฃผ์—ˆ์–ด์š”.
  • ์„œ๋ฒ„ ์ž์› ๋‚ญ๋น„ (์ผ๋ถ€ ๊ฒฝ์šฐ): ์„œ๋ฒ„์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์„œ ํด๋ผ์ด์–ธํŠธ์— ์ „๋‹ฌํ•˜๊ณ , ํด๋ผ์ด์–ธํŠธ๋Š” ์ด ๋ฐ์ดํ„ฐ๋ฅผ ๋‹ค์‹œ ๊ฐ€๊ณตํ•ด์„œ UI๋ฅผ ๊ทธ๋ฆฌ๋Š” ๊ณผ์ •์ด ๋ฐ˜๋ณต๋˜๋Š” ๊ฒฝ์šฐ๋„ ์žˆ์—ˆ์–ด์š”. ๋ฏผ๊ฐํ•œ ๋ฐ์ดํ„ฐ๋‚˜ ์„œ๋ฒ„์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๋กœ์ง์„ ํด๋ผ์ด์–ธํŠธ ์ฝ”๋“œ์—์„œ ๋‹ค๋ฃจ๊ธฐ ์–ด๋ ค์› ๊ณ ์š”.
์ •๋ณด

ํ•˜์ด๋“œ๋ ˆ์ด์…˜(Hydration)์€ ์„œ๋ฒ„์—์„œ ์ƒ์„ฑ๋œ ์ •์  HTML์„ ํด๋ผ์ด์–ธํŠธ ์ธก React ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด "์ธ์ˆ˜๋ฐ›์•„" ์ƒํ˜ธ์ž‘์šฉ ๊ฐ€๋Šฅํ•œ ์ƒํƒœ๋กœ ๋งŒ๋“œ๋Š” ๊ณผ์ •์„ ์˜๋ฏธํ•ด์š”.

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

0๏ธโƒฃ ํ•ต์‹ฌ ์•„์ด๋””์–ด

Next.js App Router์˜ ํ•ต์‹ฌ ์•„์ด๋””์–ด๋Š” **"์„œ๋ฒ„์˜ ์ด์ ๊ณผ ํด๋ผ์ด์–ธํŠธ์˜ ์ด์ ์„ ๋ชจ๋‘ ํ™œ์šฉํ•˜์ž"**์˜ˆ์š”. ์ด๋ฅผ ์œ„ํ•ด ์ปดํฌ๋„ŒํŠธ๋ฅผ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋˜๋Š” Server Components์™€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋ง๋˜๋Š” Client Components๋กœ ๋ช…ํ™•ํ•˜๊ฒŒ ๊ตฌ๋ถ„ํ–ˆ์–ด์š”.

  • Server Components:

    • ๊ธฐ๋ณธ๊ฐ’์ด์—์š”. "use client" ๋””๋ ‰ํ‹ฐ๋ธŒ๊ฐ€ ์—†๋Š” ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” Server Component๋กœ ๊ฐ„์ฃผ๋ผ์š”.
    • ์„œ๋ฒ„์—์„œ๋งŒ ์‹คํ–‰๋˜๊ธฐ ๋•Œ๋ฌธ์— ํด๋ผ์ด์–ธํŠธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š์•„์š”.
    • ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ง์ ‘ ์ ‘๊ทผ, ํŒŒ์ผ ์‹œ์Šคํ…œ ์ ‘๊ทผ, API ํ‚ค์™€ ๊ฐ™์€ ๋ฏผ๊ฐํ•œ ์ •๋ณด ๊ด€๋ฆฌ ๋“ฑ ์„œ๋ฒ„ ํ™˜๊ฒฝ์˜ ์ด์ ์„ ํ™œ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”.
    • ์ดˆ๊ธฐ ํŽ˜์ด์ง€ ๋กœ๋”ฉ ์†๋„์™€ SEO์— ๋งค์šฐ ์œ ๋ฆฌํ•ด์š”.
  • Client Components:

    • ํŒŒ์ผ ์ƒ๋‹จ์— "use client" ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ์„ ์–ธํ•ด์•ผ ํ•ด์š”.
    • ๋ธŒ๋ผ์šฐ์ €์—์„œ๋งŒ ์‹คํ–‰๋  ์ˆ˜ ์žˆ๋Š” useState, useEffect, onClick ๊ฐ™์€ React ํ›…์ด๋‚˜ ๋ธŒ๋ผ์šฐ์ € API(์˜ˆ: window, localStorage)๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ์ปดํฌ๋„ŒํŠธ์˜ˆ์š”.
    • ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•œ ๋ชจ๋“  ๋ถ€๋ถ„์— ์‚ฌ์šฉ๋ผ์š”.

์ด๋Ÿฌํ•œ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•ด Next.js๋Š” ๋ถˆํ•„์š”ํ•œ ํด๋ผ์ด์–ธํŠธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ „์†ก์„ ์ตœ์†Œํ™”ํ•˜๊ณ , ์ดˆ๊ธฐ ๋กœ๋”ฉ ์„ฑ๋Šฅ์„ ๊ทน๋Œ€ํ™”ํ•˜๋ฉด์„œ๋„ ํ’๋ถ€ํ•œ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ๊ฒŒ ๋˜์—ˆ์–ด์š”.

1๏ธโƒฃ ์ ์šฉ ๋ฐฉ๋ฒ•

๋‘ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์–ธ์ œ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•ด์•ผ ํ• ๊นŒ์š”? ๋ช…ํ™•ํ•œ ๊ธฐ์ค€์„ ๊ฐ€์ง€๊ณ  ์„ค๊ณ„ํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.

Server Components์˜ ํ™œ์šฉ

๊ฑฐ์˜ ๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ๋Š” ๊ธฐ๋ณธ์ ์œผ๋กœ Server Component๋กœ ์‹œ์ž‘ํ•ด์•ผ ํ•œ๋‹ค๊ณ  ์ƒ๊ฐํ•˜์‹œ๋ฉด ๋ผ์š”.

  • ๋ฐ์ดํ„ฐ ํŽ˜์นญ: ์„œ๋ฒ„์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์˜ฌ ๋•Œ (์˜ˆ: ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค, ๋‚ด๋ถ€ API).

    // app/page.tsx import { Product } from '@/lib/types'; // ํƒ€์ž… ์ •์˜๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•ด์š”. async function getProducts(): Promise<Product[]> { const res = await fetch('https://api.example.com/products'); // ์„œ๋ฒ„์—์„œ ์ง์ ‘ ๋ฐ์ดํ„ฐ๋ฅผ ๊ฐ€์ ธ์™€์š”. if (!res.ok) { throw new Error('Failed to fetch products'); } return res.json(); } export default async function HomePage() { const products = await getProducts(); // await ํ‚ค์›Œ๋“œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์š”. return ( <section> <h1>๋ฒ ์ŠคํŠธ์…€๋Ÿฌ ์ƒํ’ˆ</h1> <ul> {products.map((product) => ( <li key={product.id}>{product.name} - {product.price}์›</li> ))} </ul> </section> ); }
    ์œ ์šฉํ•œ ํŒ

    Server Components์—์„œ๋Š” async/await๋ฅผ ์ง์ ‘ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด์„œ useEffect๋‚˜ useState ์—†์ด๋„ ๋ฐ์ดํ„ฐ๋ฅผ ํšจ์œจ์ ์œผ๋กœ ํŽ˜์นญํ•  ์ˆ˜ ์žˆ์–ด์š”.

  • ๋ฏผ๊ฐํ•œ ์ •๋ณด ์ฒ˜๋ฆฌ: API ํ‚ค, ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค ์ž๊ฒฉ ์ฆ๋ช… ๋“ฑ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜๋ฉด ์•ˆ ๋˜๋Š” ์ •๋ณด๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๋กœ์ง.

  • ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”: ์ธํ„ฐ๋ž™์…˜์ด ์—†๋Š” ์ •์ ์ธ UI, ๋งˆํฌ๋‹ค์šด ๋ Œ๋”๋ง, ๋ณต์žกํ•œ ๊ณ„์‚ฐ ๋กœ์ง ๋“ฑ์€ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ์‹œํ‚ฌ ํ•„์š”๊ฐ€ ์—†์œผ๋ฏ€๋กœ Server Component๋กœ ์ž‘์„ฑํ•ด์š”.

  • SEO: ์ดˆ๊ธฐ ๋ Œ๋”๋ง ์‹œ ์™„์ „ํ•œ HTML์ด ์ƒ์„ฑ๋˜๋ฏ€๋กœ ๊ฒ€์ƒ‰ ์—”์ง„ ์ตœ์ ํ™”์— ์œ ๋ฆฌํ•ด์š”.

Client Components์˜ ํ™œ์šฉ

"use client" ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ๋ช…์‹œํ•˜์—ฌ Client Component๋กœ ๋งŒ๋“œ์„ธ์š”.

  • ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜: useState, useEffect, onClick, onChange ๋“ฑ ์‚ฌ์šฉ์ž ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ๋‚˜ ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ๋•Œ.
    // components/Counter.tsx 'use client'; // ์ด ํŒŒ์ผ์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋ Œ๋”๋ง๋˜์–ด์•ผ ํ•จ์„ ๋ช…์‹œํ•ด์š”. import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); return ( <div> <p>ํ˜„์žฌ ์นด์šดํŠธ: {count}</p> <button onClick={() => setCount(count + 1)}>์ฆ๊ฐ€</button> </div> ); }
  • ๋ธŒ๋ผ์šฐ์ € API ์‚ฌ์šฉ: window, document, localStorage ๋“ฑ ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•œ ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•  ๋•Œ.
  • ์„œ๋“œํŒŒํ‹ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ: ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์œผ๋กœ ์„ค๊ณ„๋œ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ (์˜ˆ: ํŠน์ • UI ์ปดํฌ๋„ŒํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ, ์ฐจํŠธ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ๋“ฑ)๋ฅผ ์‚ฌ์šฉํ•  ๋•Œ.
  • ํผ ์ฒ˜๋ฆฌ: ํด๋ผ์ด์–ธํŠธ์—์„œ ํผ์˜ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ ๋ฐ ์ œ์ถœ ์ฒ˜๋ฆฌ๊ฐ€ ํ•„์š”ํ•  ๋•Œ.

Server Components์™€ Client Components์˜ ๊ฒฐํ•ฉ

๊ฐ€์žฅ ์ด์ƒ์ ์ธ ํŒจํ„ด์€ Server Components ๋‚ด๋ถ€์— Client Components๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด์—์š”. ์ด ๊ณผ์ •์—์„œ children prop์„ ํ™œ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ค‘์š”ํ•ด์š”.

Server Component๋Š” Client Component๋ฅผ ์ง์ ‘ importํ•  ์ˆ˜ ์—†์ง€๋งŒ, Client Component๋ฅผ prop์œผ๋กœ ๋ฐ›์•„์„œ ๋ Œ๋”๋งํ•  ์ˆ˜ ์žˆ์–ด์š”.

// app/page.tsx (Server Component) import dynamic from 'next/dynamic'; // Client Component๋ฅผ Server Component์—์„œ import ์‹œ dynamic ์‚ฌ์šฉ (SSR ์ œ์™ธ) // 'use client'๊ฐ€ ๋ถ™์€ Counter ์ปดํฌ๋„ŒํŠธ๋ฅผ import ํ•ด์š”. // Server Component์—์„œ ์ง์ ‘ Client Component๋ฅผ importํ•˜๋ฉด ์•ˆ ๋˜์ง€๋งŒ, // ์ด ๊ฒฝ์šฐ์—๋Š” children์œผ๋กœ ์ „๋‹ฌํ•˜๊ฑฐ๋‚˜, dynamic import๋กœ SSR์„ ๋ˆ๋‹ค๋ฉด ๊ฐ€๋Šฅํ•ด์š”. // ์—ฌ๊ธฐ์„œ๋Š” Server Component๊ฐ€ children์œผ๋กœ Client Component๋ฅผ ๋ฐ›๋Š” ๋ฐฉ์‹์„ ๋ณด์—ฌ๋“œ๋ฆด๊ฒŒ์š”. // components/ProductDetail.tsx (Server Component) import FavoriteButton from '@/components/FavoriteButton'; // Client Component๋ผ๊ณ  ๊ฐ€์ •ํ•ด์š”. interface ProductDetailProps { productId: string; productName: string; children: React.ReactNode; // Client Component๋ฅผ children์œผ๋กœ ๋ฐ›์„ ๊ฑฐ์˜ˆ์š”. } export default async function ProductDetail({ productId, productName, children }: ProductDetailProps) { // ์„œ๋ฒ„์—์„œ ์ƒํ’ˆ ์ƒ์„ธ ์ •๋ณด๋ฅผ ๊ฐ€์ ธ์˜ค๋Š” ๋กœ์ง (์˜ˆ์‹œ) const productInfo = await fetch(`https://api.example.com/products/${productId}`).then(res => res.json()); return ( <div> <h2>{productName}</h2> <p>{productInfo.description}</p> {/* Server Component๋Š” FavoriteButton์„ ์ง์ ‘ importํ•ด์„œ ์‚ฌ์šฉํ•˜์ง€ ์•Š๊ณ , children์œผ๋กœ ๋ฐ›์•„์„œ ๋ Œ๋”๋งํ•ด์š”. */} {children} </div> ); } // app/products/[id]/page.tsx (Server Component) import ProductDetail from '@/components/ProductDetail'; import FavoriteButton from '@/components/FavoriteButton'; // Client Component๋ฅผ Server Component์—์„œ importํ•ด์š”. interface ProductPageProps { params: { id: string }; } export default function ProductPage({ params }: ProductPageProps) { return ( <ProductDetail productId={params.id} productName="์˜ˆ์‹œ ์ƒํ’ˆ"> {/* Client Component๋ฅผ Server Component์˜ children์œผ๋กœ ์ „๋‹ฌํ•ด์š”. */} <FavoriteButton productId={params.id} /> </ProductDetail> ); } // components/FavoriteButton.tsx (Client Component) 'use client'; import { useState } from 'react'; interface FavoriteButtonProps { productId: string; } export default function FavoriteButton({ productId }: FavoriteButtonProps) { const [isFavorited, setIsFavorited] = useState(false); // ํด๋ผ์ด์–ธํŠธ ์ƒํƒœ ๊ด€๋ฆฌ const toggleFavorite = () => { // API ํ˜ธ์ถœ ๋กœ์ง ๋“ฑ... setIsFavorited(!isFavorited); console.log(`Product ${productId} ${isFavorited ? 'unfavorited' : 'favorited'}`); }; return ( <button onClick={toggleFavorite}> {isFavorited ? 'โค๏ธ ์ฆ๊ฒจ์ฐพ๊ธฐ ํ•ด์ œ' : '๐Ÿค ์ฆ๊ฒจ์ฐพ๊ธฐ ์ถ”๊ฐ€'} </button> ); }

์œ„ ์˜ˆ์‹œ์—์„œ ProductPage๋Š” Server Component์ด๊ณ , ProductDetail๋„ Server Component์˜ˆ์š”. FavoriteButton์€ Client Component์ธ๋ฐ, ProductPage์—์„œ FavoriteButton์„ ProductDetail์˜ children์œผ๋กœ ์ „๋‹ฌํ•˜๋Š” ๋ฐฉ์‹์œผ๋กœ ์‚ฌ์šฉํ–ˆ์–ด์š”. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ProductDetail Server Component๋Š” Client Component์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์„ importํ•˜์ง€ ์•Š๊ณ , ๋‹จ์ง€ ์„œ๋ฒ„์—์„œ ๋ Œ๋”๋ง๋œ HTML์— Client Component์˜ placeholder๋ฅผ ํฌํ•จ์‹œ์ผœ ํด๋ผ์ด์–ธํŠธ๋กœ ๋ณด๋‚ด๊ฒŒ ๋ผ์š”. ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” ์ด placeholder๋ฅผ ๋ฐ”ํƒ•์œผ๋กœ FavoriteButton ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํ•˜์ด๋“œ๋ ˆ์ด์…˜๋˜๊ณ  ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๋™์ž‘ํ•˜๊ฒŒ ๋˜๋Š” ๊ฑฐ์ฃ .

๐Ÿงช ์˜ˆ์‹œ

0๏ธโƒฃ ์ฝ”๋“œ/์„ค์ • ์˜ˆ์‹œ

์‹ค์ œ ์‚ฌ์šฉ ์‹œ๋‚˜๋ฆฌ์˜ค๋ฅผ ํ†ตํ•ด Server Components์™€ Client Components์˜ ์กฐํ•ฉ์„ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์‚ดํŽด๋ณผ๊ฒŒ์š”.

Server Component (๋ฐ์ดํ„ฐ ํŽ˜์นญ ๋ฐ ์ •์  UI)

PostList๋Š” ์„œ๋ฒ„์—์„œ ๊ฒŒ์‹œ๊ธ€ ๋ชฉ๋ก์„ ๊ฐ€์ ธ์™€์„œ ๋ Œ๋”๋งํ•˜๋Š” Server Component์˜ˆ์š”. ์—ฌ๊ธฐ์„œ๋Š” ์–ด๋–ค ์ธํ„ฐ๋ž™์…˜๋„ ํ•„์š” ์—†์œผ๋ฏ€๋กœ "use client"๋ฅผ ๋ถ™์ด์ง€ ์•Š์•„์š”.

// app/blog/page.tsx import Link from 'next/link'; interface Post { id: string; title: string; author: string; } async function getPosts(): Promise<Post[]> { const res = await fetch('https://api.example.com/posts'); if (!res.ok) { throw new Error('๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์–ด์š”.'); } return res.json(); } export default async function BlogPage() { const posts = await getPosts(); return ( <section> <h1>์ตœ์‹  ๊ฒŒ์‹œ๊ธ€</h1> <ul> {posts.map((post) => ( <li key={post.id}> <Link href={`/blog/${post.id}`}> <h2>{post.title}</h2> <p>์ž‘์„ฑ์ž: {post.author}</p> </Link> </li> ))} </ul> </section> ); }

Client Component (์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ๋Œ“๊ธ€ ์ž…๋ ฅ ํผ)

CommentForm์€ ์‚ฌ์šฉ์ž๊ฐ€ ๋Œ“๊ธ€์„ ์ž…๋ ฅํ•˜๊ณ  ์ œ์ถœํ•˜๋Š” ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•ด์š”. ๋”ฐ๋ผ์„œ useState์™€ ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ "use client"๋ฅผ ์„ ์–ธํ•ด์š”.

// components/CommentForm.tsx 'use client'; import { useState } from 'react'; interface CommentFormProps { postId: string; onCommentSubmitted: () => void; // ๋Œ“๊ธ€ ์ œ์ถœ ํ›„ ์ฝœ๋ฐฑ ํ•จ์ˆ˜ } export default function CommentForm({ postId, onCommentSubmitted }: CommentFormProps) { const [comment, setComment] = useState(''); const [isSubmitting, setIsSubmitting] = useState(false); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); setIsSubmitting(true); try { const res = await fetch(`/api/posts/${postId}/comments`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ comment }), }); if (!res.ok) { throw new Error('๋Œ“๊ธ€ ์ œ์ถœ์— ์‹คํŒจํ–ˆ์–ด์š”.'); } setComment(''); onCommentSubmitted(); // ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์— ์•Œ๋ฆผ } catch (error) { console.error('๋Œ“๊ธ€ ์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜:', error); alert('๋Œ“๊ธ€ ์ œ์ถœ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด์š”.'); } finally { setIsSubmitting(false); } }; return ( <form onSubmit={handleSubmit}> <textarea value={comment} onChange={(e) => setComment(e.target.value)} placeholder="๋Œ“๊ธ€์„ ์ž‘์„ฑํ•ด์ฃผ์„ธ์š”..." rows={4} disabled={isSubmitting} /> <button type="submit" disabled={isSubmitting}> {isSubmitting ? '๋Œ“๊ธ€ ์ž‘์„ฑ ์ค‘...' : '๋Œ“๊ธ€ ์ž‘์„ฑ'} </button> </form> ); }

Server Component ๋‚ด๋ถ€์— Client Component ์‚ฌ์šฉ

PostDetail Server Component๋Š” ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ์„ ์„œ๋ฒ„์—์„œ ๊ฐ€์ ธ์™€ ๋ Œ๋”๋งํ•˜๊ณ , ๊ทธ ์•„๋ž˜์— CommentForm Client Component๋ฅผ ๋ฐฐ์น˜ํ•ด์š”.

// app/blog/[id]/page.tsx import CommentForm from '@/components/CommentForm'; // Client Component๋ฅผ importํ•ด์š”. import { revalidatePath } from 'next/cache'; // ISR์„ ์œ„ํ•œ revalidate ํ•จ์ˆ˜ (App Router) interface Post { id: string; title: string; content: string; } async function getPost(id: string): Promise<Post> { const res = await fetch(`https://api.example.com/posts/${id}`); if (!res.ok) { throw new Error('๊ฒŒ์‹œ๊ธ€์„ ๋ถˆ๋Ÿฌ์˜ค๋Š”๋ฐ ์‹คํŒจํ–ˆ์–ด์š”.'); } return res.json(); } export default async function PostDetail({ params }: { params: { id: string } }) { const post = await getPost(params.id); const handleCommentSubmitted = () => { 'use server'; // Server Action! ์ด ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋ผ์š”. // ๋Œ“๊ธ€ ์ œ์ถœ ํ›„ ๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ๋ Œ๋”๋งํ•˜๋„๋ก ์œ ๋„ revalidatePath(`/blog/${params.id}`); console.log('๋Œ“๊ธ€์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ œ์ถœ๋˜์–ด ํŽ˜์ด์ง€๋ฅผ ๋‹ค์‹œ ๊ฒ€์ฆํ•ฉ๋‹ˆ๋‹ค.'); }; return ( <article> <h1>{post.title}</h1> <p>{post.content}</p> <h3>๋Œ“๊ธ€ ๋‹ฌ๊ธฐ</h3> {/* Server Component๋Š” CommentForm Client Component๋ฅผ ๋ Œ๋”๋งํ•ด์š”. */} <CommentForm postId={params.id} onCommentSubmitted={handleCommentSubmitted} /> </article> ); }
์œ ์šฉํ•œ ํŒ

Client Component์˜ prop์œผ๋กœ Server Component์˜ ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•  ๋•Œ๋Š” "use server" ๋””๋ ‰ํ‹ฐ๋ธŒ๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ Server Action์œผ๋กœ ๋งŒ๋“ค์–ด์•ผ ํ•ด์š”. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์—์„œ ์„œ๋ฒ„ ํ•จ์ˆ˜๋ฅผ ์ง์ ‘ ํ˜ธ์ถœํ•  ์ˆ˜ ์—†์–ด์š”.

1๏ธโƒฃ ์ ์šฉ ๊ฒฐ๊ณผ

์œ„ ์˜ˆ์‹œ์—์„œ BlogPage์™€ PostDetail์€ Server Components๋กœ ๋™์ž‘ํ•˜์—ฌ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์‹œ ์ตœ์†Œํ•œ์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋งŒ ์ „์†ก๋˜๊ณ , ๊ฒ€์ƒ‰ ์—”์ง„์— ์นœํ™”์ ์ธ ์™„์ „ํ•œ HTML์„ ์ œ๊ณตํ•ด์š”. PostDetail ํŽ˜์ด์ง€์—์„œ ๊ฒŒ์‹œ๊ธ€ ๋‚ด์šฉ ์ž์ฒด๋Š” ์„œ๋ฒ„์—์„œ ๋น ๋ฅด๊ฒŒ ๋ Œ๋”๋ง๋˜๊ณ , CommentForm์ด๋ผ๋Š” ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•œ ๋ถ€๋ถ„๋งŒ Client Component๋กœ ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์–ด ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ๊ณผ์ •์„ ๊ฑฐ์ณ์š”.

์ด๋Ÿฌํ•œ ๋ถ„๋ฆฌ๋ฅผ ํ†ตํ•ด:

  • ์„ฑ๋Šฅ: ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„๊ฐ€ ํ–ฅ์ƒ๋˜๊ณ , ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค ํฌ๊ธฐ๊ฐ€ ์ค„์–ด๋“ค์–ด ํŽ˜์ด์ง€ ์‘๋‹ต์„ฑ์ด ์ข‹์•„์ ธ์š”.
  • ๊ฐœ๋ฐœ ๊ฒฝํ—˜: ๊ฐœ๋ฐœ์ž๋Š” ๊ฐ ์ปดํฌ๋„ŒํŠธ์˜ ์—ญํ• (๋ฐ์ดํ„ฐ & ์ •์  UI vs. ์ธํ„ฐ๋ž™์…˜)์— ์ง‘์ค‘ํ•˜์—ฌ ๋” ๋ช…ํ™•ํ•˜๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•  ์ˆ˜ ์žˆ์–ด์š”.
  • ๋ณด์•ˆ: ๋ฏผ๊ฐํ•œ ์„œ๋ฒ„ ๋กœ์ง์€ ํด๋ผ์ด์–ธํŠธ์— ๋…ธ์ถœ๋˜์ง€ ์•Š๊ณ  ์•ˆ์ „ํ•˜๊ฒŒ ์ฒ˜๋ฆฌ๋ผ์š”.

๐Ÿ“ ์ •๋ฆฌ

0๏ธโƒฃ ํ•ต์‹ฌ ์š”์•ฝ

Next.js App Router์˜ Server Components์™€ Client Components๋Š” ๊ฐœ๋ฐœ์ž๊ฐ€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์„ฑ๋Šฅ๊ณผ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ๊ทน๋Œ€ํ™”ํ•  ์ˆ˜ ์žˆ๋„๋ก ๋•๋Š” ๊ฐ•๋ ฅํ•œ ๋„๊ตฌ์˜ˆ์š”.

  • Server Components๋Š” ๊ธฐ๋ณธ๊ฐ’์ด๋ฉฐ, ๋ฐ์ดํ„ฐ ํŽ˜์นญ, ์ •์  UI ๋ Œ๋”๋ง, ์„œ๋ฒ„ ๋กœ์ง ์ฒ˜๋ฆฌ ๋ฐ ๋ฒˆ๋“ค ์‚ฌ์ด์ฆˆ ์ตœ์ ํ™”์— ์‚ฌ์šฉ๋ผ์š”. ํด๋ผ์ด์–ธํŠธ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ๋˜์ง€ ์•Š์•„ ์ดˆ๊ธฐ ๋กœ๋”ฉ ์†๋„์™€ SEO์— ์œ ๋ฆฌํ•ด์š”.
  • Client Components๋Š” "use client" ๋””๋ ‰ํ‹ฐ๋ธŒ๋กœ ์„ ์–ธํ•˜๋ฉฐ, ์‚ฌ์šฉ์ž ์ธํ„ฐ๋ž™์…˜(useState, useEffect, ์ด๋ฒคํŠธ ํ•ธ๋“ค๋Ÿฌ), ๋ธŒ๋ผ์šฐ์ € API ์‚ฌ์šฉ, ํด๋ผ์ด์–ธํŠธ ์ „์šฉ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ์—ฐ๋™์— ํ•„์š”ํ•ด์š”.
  • ๊ฐ€์žฅ ํšจ์œจ์ ์ธ ๋ฐฉ๋ฒ•์€ Server Components๋ฅผ ๊ธฐ๋ณธ์œผ๋กœ ํ•˜๊ณ , ์ธํ„ฐ๋ž™์…˜์ด ํ•„์š”ํ•œ ๋ถ€๋ถ„๋งŒ Client Components๋กœ ๋ถ„๋ฆฌํ•˜์—ฌ children prop ๋“ฑ์„ ํ†ตํ•ด ๊ฒฐํ•ฉํ•˜๋Š” ๊ฒƒ์ด์—์š”. Client Component์— Server Component์—์„œ ์˜จ ํ•จ์ˆ˜๋ฅผ props๋กœ ์ „๋‹ฌํ•  ๋•Œ๋Š” "use server"๋กœ Server Action์„ ๋งŒ๋“ค์–ด์•ผ ํ•ด์š”.

1๏ธโƒฃ ๋‹ค์Œ ์•ก์…˜

์ด์ œ Server Components์™€ Client Components์˜ ๊ฐœ๋…์„ ์ดํ•ดํ•˜์…จ์œผ๋‹ˆ, ๋‹ค์Œ ๋‹จ๊ณ„๋กœ ์•„๋ž˜๋ฅผ ์‹œ๋„ํ•ด ๋ณด๋Š” ๊ฒƒ์„ ์ถ”์ฒœํ•ด์š”.

  1. ๊ธฐ์กด ํ”„๋กœ์ ํŠธ ๋ฆฌํŒฉํ† ๋ง: Next.js App Router๋ฅผ ์‚ฌ์šฉํ•˜๋Š” ํ”„๋กœ์ ํŠธ๊ฐ€ ์žˆ๋‹ค๋ฉด, ๊ฐ ์ปดํฌ๋„ŒํŠธ๊ฐ€ Server Component๋กœ ์œ ์ง€๋  ์ˆ˜ ์žˆ๋Š”์ง€, ์•„๋‹ˆ๋ฉด "use client"๊ฐ€ ํ•„์š”ํ•œ์ง€ ๊ฒ€ํ† ํ•ด ๋ณด์„ธ์š”.
  2. ์ƒˆ๋กœ์šด ํ”„๋กœ์ ํŠธ ์‹œ์ž‘: App Router๋กœ ์ƒˆ ํ”„๋กœ์ ํŠธ๋ฅผ ์‹œ์ž‘ํ•˜๋ฉด์„œ, ์˜์‹์ ์œผ๋กœ Server Component ์šฐ์„  ์ ‘๊ทผ ๋ฐฉ์‹์„ ์ ์šฉํ•ด ๋ณด์„ธ์š”.
  3. ๋ฐ์ดํ„ฐ ํŽ˜์นญ ์ „๋žต ์‹ฌํ™”: Server Components์—์„œ์˜ ๋ฐ์ดํ„ฐ ํŽ˜์นญ(์บ์‹ฑ, ์žฌ๊ฒ€์ฆ ๋“ฑ)์— ๋Œ€ํ•ด ๋” ๊นŠ์ด ํ•™์Šตํ•ด ๋ณด์„ธ์š”. revalidatePath์™€ revalidateTag ๊ฐ™์€ Server Action๊ณผ ์—ฐ๊ณ„๋œ ๊ธฐ๋Šฅ์„ ํŒŒ์•…ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์•„์š”.

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“ ์ง€ ๊ด€๋ จ ๊ณต์‹ ๋ฌธ์„œ๋ฅผ ์ฐธ๊ณ ํ•˜๊ฑฐ๋‚˜ ์ง์ ‘ ์ฝ”๋“œ๋ฅผ ์ž‘์„ฑํ•ด ๋ณด๋ฉด์„œ ์ต์ˆ™ํ•ด์ง€๋Š” ๊ฒƒ์ด ๊ฐ€์žฅ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์—์š”.

๐Ÿ“ฎ ์ฐธ๊ณ 

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