[๐ค] Next.js App Router: Server Components์ Client Components, ์๋ฒฝ ์ ๋ณตํด์!
Next.js 13+ App Router์์ Server Components์ Client Components๊ฐ ์ด๋ป๊ฒ ๋์ํ๊ณ , ์ธ์ ์ด๋ค ์ปดํฌ๋ํธ๋ฅผ ์ฌ์ฉํด์ผ ํ๋์ง ๋ช ํํ ์ดํดํ์ฌ ์ฑ๋ฅ ์ต์ ํ์ ํจ์จ์ ์ธ ๊ฐ๋ฐ์ ์ด๋ฃจ๋ ๋ฐฉ๋ฒ์ ์์ธํ ์์๋ด์.

์ ๋ณด
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๋ก ๋ถ๋ฆฌํ์ฌ
childrenprop ๋ฑ์ ํตํด ๊ฒฐํฉํ๋ ๊ฒ์ด์์. Client Component์ Server Component์์ ์จ ํจ์๋ฅผ props๋ก ์ ๋ฌํ ๋๋"use server"๋ก Server Action์ ๋ง๋ค์ด์ผ ํด์.
1๏ธโฃ ๋ค์ ์ก์
์ด์ Server Components์ Client Components์ ๊ฐ๋
์ ์ดํดํ์
จ์ผ๋, ๋ค์ ๋จ๊ณ๋ก ์๋๋ฅผ ์๋ํด ๋ณด๋ ๊ฒ์ ์ถ์ฒํด์.
- ๊ธฐ์กด ํ๋ก์ ํธ ๋ฆฌํฉํ ๋ง: Next.js App Router๋ฅผ ์ฌ์ฉํ๋ ํ๋ก์ ํธ๊ฐ ์๋ค๋ฉด, ๊ฐ ์ปดํฌ๋ํธ๊ฐ Server Component๋ก ์ ์ง๋ ์ ์๋์ง, ์๋๋ฉด
"use client"๊ฐ ํ์ํ์ง ๊ฒํ ํด ๋ณด์ธ์. - ์๋ก์ด ํ๋ก์ ํธ ์์: App Router๋ก ์ ํ๋ก์ ํธ๋ฅผ ์์ํ๋ฉด์, ์์์ ์ผ๋ก Server Component ์ฐ์ ์ ๊ทผ ๋ฐฉ์์ ์ ์ฉํด ๋ณด์ธ์.
- ๋ฐ์ดํฐ ํ์นญ ์ ๋ต ์ฌํ: Server Components์์์ ๋ฐ์ดํฐ ํ์นญ(์บ์ฑ, ์ฌ๊ฒ์ฆ ๋ฑ)์ ๋ํด ๋ ๊น์ด ํ์ตํด ๋ณด์ธ์.
revalidatePath์revalidateTag๊ฐ์ Server Action๊ณผ ์ฐ๊ณ๋ ๊ธฐ๋ฅ์ ํ์ ํ๋ ๊ฒ๋ ์ข์์.
๊ถ๊ธํ ์ ์ด ์๋ค๋ฉด ์ธ์ ๋ ์ง ๊ด๋ จ ๊ณต์ ๋ฌธ์๋ฅผ ์ฐธ๊ณ ํ๊ฑฐ๋ ์ง์ ์ฝ๋๋ฅผ ์์ฑํด ๋ณด๋ฉด์ ์ต์ํด์ง๋ ๊ฒ์ด ๊ฐ์ฅ ์ข์ ๋ฐฉ๋ฒ์ด์์.
๐ฎ ์ฐธ๊ณ
- Next.js ๊ณต์ ๋ฌธ์: Server Components
- Next.js ๊ณต์ ๋ฌธ์: Client Components
- Next.js ๊ณต์ ๋ฌธ์: Server Actions
์ฐ๊ด๋ ํฌ์คํธ
- ๋จ์ด: 1,271๊ฐ15๋ถ
[๐ค] Next.js App Router์์ Server Components์ Client Components ์ ๋๋ก ํ์ฉํ๊ธฐ
Next.js 13+ App Router ํ๊ฒฝ์์ Server Components์ Client Components์ ๊ฐ๋ , ์ฐจ์ด์ , ๊ทธ๋ฆฌ๊ณ ์ค์ฉ์ ์ธ ํ์ฉ ์ ๋ต์ ์์ธํ ์์๋ณด๊ณ , ์ฑ๋ฅ ์ต์ ํ์ ๊ฐ๋ฐ ํจ์จ์ฑ์ ๋์ด๋ ๋ฐฉ๋ฒ์ ๊ณต์ ํด์.