๐Ÿš€ Next.js 5์žฅ: Client Boundary ์„ค๊ณ„ โ€” ์–ด๋””์„œ ์„ ์„ ๊ทธ์„ ๊ฒƒ์ธ๊ฐ€?

๐Ÿ“‹ ๊ฐœ์š”

Client Component ๊ฒฝ๊ณ„๋ฅผ ์–ด๋””์— ๊ทธ์–ด์•ผ ํ•˜๋Š”์ง€ โ€” ์„ค๊ณ„ ์›์น™๊ณผ ํ”ํ•œ ์‹ค์ˆ˜๋ฅผ ์ •๋ฆฌํ•ฉ๋‹ˆ๋‹ค.

๐Ÿ“‹ ๋ชฉ์ฐจ


๐Ÿ“Œ ์ด ๋ฌธ์„œ๋ฅผ ์ฝ๊ธฐ ์ „์—

โฑ๏ธ ์˜ˆ์ƒ ์ฝ๊ธฐ ์‹œ๊ฐ„: 15๋ถ„(์ „์ฒด) / ํ•ต์‹ฌ ํŒŒํŠธ๋งŒ: 8๋ถ„

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ํ๋ฆ„
use client ์˜ ์—„๋ฐ€ํ•œ ์ •์˜ โ†’ ์˜ค์—ผ ๋ฐฉ์ง€ ํŒจํ„ด(Leaf) โ†’ Children Composition ๋งˆ๋ฒ• โ†’ ์‹ค๋ฌด ์—๋Ÿฌ ๋Œ€์ฒ˜

๐ŸŽฏ ์ด ๋ฌธ์„œ๋ฅผ ๋‹ค ์ฝ์œผ๋ฉด ํ•  ์ˆ˜ ์žˆ๋Š” ๊ฒƒ

  • ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(๋ถ€๋ชจ)์™€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ(์ž์‹)๋ฅผ ์ž์œ ์ž์žฌ๋กœ ์—ฎ์–ด์„œ ์„ฑ๋Šฅ ์†์‹ค ์—†๋Š” ์™„๋ฒฝํ•œ ํŽ˜์ด์ง€ ํŠธ๋ฆฌ๋ฅผ ์งค ์ˆ˜ ์žˆ๋‹ค.
  • "Client Component ์•ˆ์— Server Component๋ฅผ ๋„ฃ์„ ์ˆ˜ ์—†๋‹ค" ๋Š” ํ”ํ•œ ์˜คํ•ด๋ฅผ {children} ํŒจํ„ด์œผ๋กœ ์šฐ์•„ํ•˜๊ฒŒ ํ•ด๊ฒฐํ•  ์ˆ˜ ์žˆ๋‹ค.

๐Ÿ—บ๏ธ ์ด ๋ฌธ์„œ์˜ ๋ฐฐ๊ฒฝ ์„ธ๊ณ„๊ด€: '์˜์ˆ˜๋„ค ์ปค๋ฎค๋‹ˆํ‹ฐ'

  • ์˜์ฒ (์ƒˆ๋กœ ์˜จ ์ฃผ๋‹ˆ์–ด): "๊ฒŒ์‹œ๊ธ€ ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ํ†ต์งธ๋กœ ์งฐ๋Š”๋ฐ, ์ข‹์•„์š” ๋ฒ„ํŠผ ํ•˜๋‚˜ ๋„ฃ์œผ๋ ค๋‹ˆ ์ฝ˜์†”์—์„œ ์—๋Ÿฌ๋ฅผ ๋ฟœ์–ด์š”. ์งœ์ฆ ๋‚˜์„œ ํŽ˜์ด์ง€ ๋งจ ์œ„์— 'use client' ๋‹ฌ์•˜๋”๋‹ˆ ์ด๋ฒˆ์—” DB ์ปค๋„ฅ์…˜ ์ฝ”๋“œ๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๋กœ ๋„˜์–ด๊ฐ€๋ ค๋‹ค ์„œ๋ฒ„๊ฐ€ ์™„์ „ํžˆ ํ„ฐ์ ธ๋ฒ„๋ ธ์–ด์š”! ๐Ÿ˜ญ"
  • ์˜ํ˜ธ(FE ๋ฆฌ๋“œ): "์˜์ฒ  ๋‹˜... use client ๋Š” '์ด ํŒŒ์ผ์€ ํด๋ผ์ด์–ธํŠธ ์ „์šฉ์ด์•ผ' ๋ผ๋Š” ๋œป์ด ์•„๋‹ˆ๋ผ, '์—ฌ๊ธฐ์„œ๋ถ€ํ„ฐ ํ•˜๋ฅ˜๋กœ ํ๋ฅด๋Š” ๋ชจ๋“  ์ž์‹๋“ค์€ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์— ํฌํ•จ์‹œํ‚จ๋‹ค' ๋Š” ๋ฌด์„œ์šด ์˜ค์—ผ ๊ฒฝ๊ณ„์„ (Boundary) ์„ ์–ธ๋ฌธ์ด์—์š”. ๋Œ€๋“ค๋ณด(์„œ๋ฒ„)๋ฅผ ๊นŽ์•„๋‚ด์ง€ ๋ง๊ณ  ์ „๊ตฌ(ํด๋ผ์ด์–ธํŠธ)๋งŒ ๋งค๋‹ฌ์•˜์–ด์•ผ์ฃ !"

๐Ÿค” ์™œ ์•Œ์•„์•ผ ํ•˜๋Š”๊ฐ€

Next.js App Router ์ƒํƒœ๊ณ„์—์„œ ๊ฐ€์žฅ ์–ด๋ ต๊ณ  ์ขŒ์ ˆ์„ ๋งŽ์ด ๊ฒช๋Š” ๋ณ‘๋ชฉ ๊ตฌ๊ฐ„์ด ๋ฐ”๋กœ ์—ฌ๊ธฐ์•ผ.

์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ(RSC)๋Š” DB๋ž‘ ์ง๊ฒฐ๋˜๊ณ  ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์šฉ๋Ÿ‰์ด 0(Zero)์ด๋ผ๋Š” ๋งˆ๋ฒ• ๊ฐ™์€ ์žฅ์ ์ด ์žˆ์–ด. ๊ทธ๋Ÿฐ๋ฐ ์‚ฌ์šฉ์ž๊ฐ€ ํ™”๋ฉด๊ณผ ์ƒํ˜ธ์ž‘์šฉํ•˜๋ ค๋ฉด ๋ฌด์กฐ๊ฑด ์ƒํƒœ(useState)๋‚˜ ์ด๋ฒคํŠธ(onClick)๊ฐ€ ํ•„์š”ํ•˜๊ณ , ์ด๊ฑด ์˜ค์ง ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—์„œ๋งŒ ๋Œ์•„๊ฐ€์ž–์•„?

๊ฒฐ๊ตญ ์šฐ๋ฆฌ๋Š” ํ•˜๋‚˜์˜ ํŽ˜์ด์ง€ ์•ˆ์—์„œ ํ•„์—ฐ์ ์œผ๋กœ ์„œ๋ฒ„ ์˜์—ญ๊ณผ ํด๋ผ์ด์–ธํŠธ ์˜์—ญ์„ ์„ž์–ด ์จ์•ผ๋งŒ ํ•ด. ์ด๋•Œ ์ด ๋‘ ์˜์—ญ ์‚ฌ์ด์˜ ๊ฒฝ๊ณ„์„ (Boundary)์„ ์–ด๋””์—, ์–ด๋–ป๊ฒŒ ๊ธ‹๋А๋ƒ์— ๋”ฐ๋ผ ์•ฑ์˜ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค ์šฉ๋Ÿ‰์ด 0KB๋กœ ์œ ์ง€๋  ์ˆ˜๋„ ์žˆ๊ณ , ์ˆœ์‹๊ฐ„์— 2MB์งœ๋ฆฌ ๋šฑ๋šฑํ•œ ์“ฐ๋ ˆ๊ธฐ๊ฐ€ ๋  ์ˆ˜๋„ ์žˆ์–ด. ์ด ๊ฒฝ๊ณ„์„  ๊ธ‹๊ธฐ ๋†€์ด์˜ ์ตœ๊ฐ•์ž๊ฐ€ ๋˜๋Š” ๊ฒƒ์ด ์‹œ๋‹ˆ์–ด ํ”„๋ก ํŠธ์—”๋“œ ๊ฐœ๋ฐœ์ž์˜ ํ•„์ˆ˜ ์กฐ๊ฑด์ด์•ผ.


๐Ÿ—๏ธ ๋น„์œ ๋กœ ๋จผ์ € ์ดํ•ดํ•˜๊ธฐ

๐Ÿง’ 5์‚ด์—๊ฒŒ ์„ค๋ช…ํ•œ๋‹ค๋ฉด? (์ž‰ํฌ์™€ ๊ฐ•๋ฌผ)

๋ง‘์€ ๊ฐ•๋ฌผ(์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ) ์ด ์ƒ๋ฅ˜์—์„œ ํ•˜๋ฅ˜๋กœ ํ๋ฅด๊ณ  ์žˆ์–ด.
์—ฌ๊ธฐ์— ์•„์ฃผ ์ž‘์€ "ํด๋ผ์ด์–ธํŠธ ๋ณ€ํ™˜๊ธฐ(use client)" ๋ผ๋Š” ํŒŒ๋ž€์ƒ‰ ์ž‰ํฌ ํ•œ ๋ฐฉ์šธ์„ ๋˜‘ ๋–จ์–ด๋œจ๋ฆฌ๋ฉด ์–ด๋–ป๊ฒŒ ๋ ๊นŒ?

์ž‰ํฌ๊ฐ€ ๋–จ์–ด์ง„ ์ง€์ ๋ถ€ํ„ฐ ๊ทธ ์•„๋ž˜๋กœ ํ๋ฅด๋Š” ๋ชจ๋“  ํ•˜๋ฅ˜ ๊ฐ•๋ฌผ(์ž์‹ ์ปดํฌ๋„ŒํŠธ๋“ค) ์€ ์ „๋ถ€ ๋‹ค ํŒŒ๋ž€์ƒ‰(ํด๋ผ์ด์–ธํŠธ ๋ฒˆ๋“ค) ์œผ๋กœ ๋ฌผ๋“ค์–ด ๋ฒ„๋ ค!

๊ทธ๋Ÿฌ๋‹ˆ๊นŒ ์ž‰ํฌ๋ฅผ ์ƒ๋ฅ˜(page.tsx ์ตœ์ƒ๋‹จ) ์— ๋–จ์–ด๋œจ๋ฆฌ๋ฉด ์˜จ ๊ฐ•๋ฌผ์ด ์˜ค์—ผ๋ผ์„œ ์‚ฌ์šฉ์ž๋Š” ๋ฌด๊ฑฐ์šด ํŒŒ๋ž€ ๋ฌผ์„ ๋งˆ์…”์•ผ ํ•ด.
์ž‰ํฌ๋Š” ๊ฐ•๋ฌผ์ด ๋ฐ”๋‹ค๋กœ ๋น ์ง€๊ธฐ ์ง์ „, ์•„์ฃผ ๋งจ ๋ ํ•˜๋ฅ˜(Leaf) ์— ์กฐ์‹ฌ์Šค๋Ÿฝ๊ฒŒ ๋˜‘! ํ•˜๊ณ  ๋–จ์–ด๋œจ๋ ค์•ผ๋งŒ ๋ง‘์€ ๊ฐ•๋ฌผ์„ ์ตœ๋Œ€ํ•œ ๊ธธ๊ฒŒ ์ง€ํ‚ฌ ์ˆ˜ ์žˆ์–ด.


๐Ÿงฉ use client์˜ ์ง„์งœ ์˜๋ฏธ (๊ฒฝ๊ณ„์„  ์„ ์–ธ) ๐ŸŸข

์˜์ฒ ์ด๊ฐ€ ์ฐฉ๊ฐํ–ˆ๋˜ ๊ฐ€์žฅ ํ”ํ•œ ์˜คํ•ด๋ถ€ํ„ฐ ๊นจ๋ถ€์ˆด๋ณด์ž.

'use client' // <- ์ด๊ฒƒ์˜ ์ง„์งœ ์˜๋ฏธ๋Š”?

โŒ ํ”ํ•œ ์˜คํ•ด: "์ด ํŒŒ์ผ์€ ์ด์ œ ๋ธŒ๋ผ์šฐ์ € ์ „์šฉ ์ปดํฌ๋„ŒํŠธ์•ผ! (CSR)"
โœ… ์ง„์งœ ์˜๋ฏธ: "์ด์ œ๋ถ€ํ„ฐ ์ด ํŒŒ์ผ๊ณผ, ์ด ํŒŒ์ผ ์•ˆ์—์„œ import ๋˜๋Š” ๋ชจ๋“  ์ž์‹ ์ปดํฌ๋„ŒํŠธ ๋ฐ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ๋“ค์€ ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ์‹คํ–‰ํ•ด์•ผ ํ•˜๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฌถ์Œ(Client Bundle)์— ํฌํ•จ์‹œํ‚จ๋‹ค!"

์˜ค์—ผ์˜ ์ „ํŒŒ (Contagion)

use client๋ฅผ ์„ ์–ธํ•˜๋Š” ์ˆœ๊ฐ„, ๊ทธ ์ง€์ ์„ ๊ธฐ์ ์œผ๋กœ ํด๋ผ์ด์–ธํŠธ ๊ฒฝ๊ณ„(Client Boundary) ๊ฐ€ ํ˜•์„ฑ๋ผ.
์ด ๊ฒฝ๊ณ„์„  ์•ˆ์œผ๋กœ import ๋˜๋Š” ๋…€์„๋“ค์€, ์ž๊ธฐ ์ž์‹  ํŒŒ์ผ์— 'use client'๋ผ๊ณ  ์•ˆ ์ ์–ด๋†จ์–ด๋„ ๊ฐ•์ œ๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ณ€ํ™˜๋‹นํ•ด. ์—ฐ๋Œ€ ์ฑ…์ž„์„ ์ง€๊ฒŒ ๋˜๋Š” ๊ฑฐ์ง€.


๐ŸŒฑ Leaf Component Pattern โ€” ์˜ค์—ผ์„ ์ตœ์†Œํ™”ํ•˜๋ผ ๐ŸŸก

์˜์ฒ ์ด๊ฐ€ ์Šคํ„ฐ๋”” ์ƒ์„ธ ํŽ˜์ด์ง€๋ฅผ ๊ทธ๋ ธ๋˜ ์ˆœ์ง„ํ•œ ์ฝ”๋“œ(Naive Approach)๋ฅผ ๋‹ค์‹œ ๋ณด์ž.

โŒ ์˜์ฒ ์ด์˜ ์ƒ๋ฅ˜ ์˜ค์—ผ (The Top-level Contamination)

// app/studies/[id]/page.tsx
'use client' // ๐Ÿ’ฃ ๊ท€์ฐฎ์•„์„œ ์ตœ์ƒ๋‹จ์— ๋ฐ•์•„๋ฒ„๋ฆผ
 
import db from '@/lib/db' // ๐Ÿ’ฅ DB ์ปค๋„ฅํ„ฐ๋ฅผ ๋ธŒ๋ผ์šฐ์ €๋กœ ๋ณด๋‚ด๋ ค๊ณ ?! (๋Œ€ํ˜• ์‚ฌ๊ณ  ๋ฐœ๋ฐœ)
import StudyContent from './StudyContent' 
import LikeButton from './LikeButton'
 
// ์˜์ฒ : "์ข‹์•„์š” ๋ฒ„ํŠผ(LikeButton)์— onClick ๋‹ฌ์•„์•ผ ํ•˜๋‹ˆ๊นŒ ํŽ˜์ด์ง€ ์ „์ฒด๋ฅผ ํด๋ผ์ด์–ธํŠธ๋กœ ํ•˜์ž!"
export default function StudyPage() {
  const [likes, setLikes] = useState(0);
 
  return (
    <article>
      <h1>์Šคํ„ฐ๋”” ์ƒ์„ธ๊ธ€</h1>
      <StudyContent /> {/* ์ˆ˜์ฒœ ์ค„์งœ๋ฆฌ ์ •์  ๊ธ€์”จ๊ฐ€ ๋ชจ๋‘ JS ๋ฒˆ๋“ค์— ํฌํ•จ๋จ */}
      <LikeButton onClick={() => setLikes(likes + 1)} likes={likes} />
    </article>
  )
}

๊ฒฐ๊ณผ: DB ์‹œํฌ๋ฆฟ ํ‚ค๊ฐ€ ๋ธŒ๋ผ์šฐ์ €๋กœ ํ†ต์งธ๋กœ ๋…ธ์ถœ๋  ๋ป”ํ•œ ๋ณด์•ˆ ์‚ฌ๊ณ  ๋ฐœ์ƒ. ๊ฒŒ๋‹ค๊ฐ€ ํŽ˜์ด์ง€ ์ „์ฒด๊ฐ€ ๋ Œ๋”๋ง๋˜๋ฉด์„œ ์—„์ฒญ๋‚œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ๋ฒˆ๋“ค์ด ์‚ฌ์šฉ์ž ํฐ์œผ๋กœ ๋‹ค์šด๋กœ๋“œ๋จ.

โœ… ์˜ํ˜ธ์˜ ๋ฆฌํŒฉํ† ๋ง: ๋‚˜๋ญ‡์žŽ(Leaf)์œผ๋กœ ๋ฐ€์–ด๋‚ด๊ธฐ

ํ•ด๊ฒฐ์ฑ…์€ ๊ฐ„๋‹จํ•ด. ์ƒํ˜ธ์ž‘์šฉ(onClick, useState)์ด ํ•„์š”ํ•œ ์ตœ์†Œ ๋‹จ์œ„์˜ ์ปดํฌ๋„ŒํŠธ๋งŒ ๊ฐ€์œ„๋กœ ์˜ค๋ ค๋‚ด์„œ ๋ณ„๋„ ํŒŒ์ผ๋กœ ๋ถ„๋ฆฌ ํ•œ ๋’ค, ๊ฑฐ๊ธฐ์—๋งŒ ์ž‰ํฌ(use client)๋ฅผ ์น ํ•˜๋Š” ๊ฑฐ์•ผ.

// app/studies/[id]/LikeButton.tsx (๐Ÿ’ก ๋ถ„๋ฆฌ๋œ Leaf ์ปดํฌ๋„ŒํŠธ)
'use client' // ์˜ค์ง ์ด ํŒŒ์ผ๋งŒ JS ๋ฒˆ๋“ค์— ๋ฌถ์ธ๋‹ค!
import { useState } from 'react'
 
export default function LikeButton() {
  const [likes, setLikes] = useState(0)
  // ๋ถ€๋ชจ(์„œ๋ฒ„)์—๊ฒŒ ์ƒํƒœ๋ฅผ ๊ตฌ๊ฑธํ•˜์ง€ ์•Š๊ณ , ์Šค์Šค๋กœ ์ƒํƒœ๋ฅผ ๋“ค๊ณ  ๋…๋ฆฝํ•œ๋‹ค.
  return <button onClick={() => setLikes(l => l + 1)}>๐Ÿ‘ {likes}</button>
}
// app/studies/[id]/page.tsx
// ๐Ÿš€ ์•„๋ฌด๋Ÿฐ ์ง€์‹œ์ž ์—†์Œ -> ๊ธฐ๋ณธ๊ฐ’์ธ Server Component ์œ ์ง€! (๋ฒˆ๋“ค 0KB)
 
import db from '@/lib/db' 
import StudyContent from './StudyContent' 
import LikeButton from './LikeButton' // ์ž‰ํฌ๊ฐ€ ์น ํ•ด์ง„ Leaf ์ปดํฌ๋„ŒํŠธ๋งŒ ์กฐ๋ฆฝ!
 
export default async function StudyPage() {
  const data = await db.fetchStudy() // ์•ˆ์ „ํ•œ ์„œ๋ฒ„ ์ „์šฉ ํ†ต์‹ 
 
  return (
    <article>
      <h1>{data.title}</h1>
      <StudyContent data={data.content} /> {/* ์˜์›ํžˆ ์ˆœ์ˆ˜ํ•œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์œ ์ง€ */}
      <LikeButton /> {/* ํด๋ผ์ด์–ธํŠธ ๊ฒฝ๊ณ„์„ ์€ ์—ฌ๊ธฐ์„œ ๋”ฑ ํ•œ ๋ฒˆ ์‹œ์ž‘ํ•˜๊ณ  ๋๋‚จ. ์™„๋ฒฝ! */}
    </article>
  )
}

๐Ÿ›ก๏ธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํด๋ผ์ด์–ธํŠธ๋กœ ๋„˜๊ธฐ๋Š” ๋งˆ๋ฒ• ({children}) ๐Ÿ”ด

๊ฐ€์žฅ ๋Œ€๋‹ตํ•˜๊ธฐ ํž˜๋“  ๋ฉด์ ‘ ์งˆ๋ฌธ ์ค‘ ํ•˜๋‚˜์•ผ.
"Client Component(use client) ๋‚ด๋ถ€์— ์ž์‹์œผ๋กœ Server Component๋ฅผ ๋„ฃ์„ ์ˆ˜ ์žˆ๋‚˜์š”?"

โŒ import ๋ถˆ๊ฐ€๋Šฅ!
โœ… {children} Composition(ํ•ฉ์„ฑ)์œผ๋กœ๋Š” 100% ๊ฐ€๋Šฅ!

์ด ํŒจํ„ด์€ ๋„ค๋น„๊ฒŒ์ด์…˜ ๋ฐ”, ์‚ฌ์ด๋“œ๋ฐ”, ๋ ˆ์ด์•„์›ƒ ๋“ฑ ํ™”๋ฉด์—์„œ ๋„“์€ ๋ฉด์ ์„ ์ฐจ์ง€ํ•˜๋ฉด์„œ ์ƒํƒœ(State)๋ฅผ ๊ฐ€์ ธ์•ผ ํ•˜๋Š” Layout ์ปดํฌ๋„ŒํŠธ ๋ฅผ ์„ค๊ณ„ํ•  ๋•Œ ๊ตฌ์›์ž์™€ ๊ฐ™์•„.

์ƒํ™ฉ: ํŽผ์นจ/๋‹ซํž˜ ์ƒํƒœ๋ฅผ ๊ฐ€์ง„ ๊ฑฐ๋Œ€ํ•œ ๋ ˆ์ด์•„์›ƒ

์˜์ˆ™(๋””์ž์ด๋„ˆ)์ด ์š”๊ตฌํ–ˆ์–ด. "์–‘์˜† ์‚ฌ์ด๋“œ๋ฐ” ์ถ•์†Œ/ํ™•๋Œ€ ์• ๋‹ˆ๋ฉ”์ด์…˜์ด ๋“ค์–ด๊ฐ„ 3๋‹จ ๋ ˆ์ด์•„์›ƒ์„ ์งœ์ฃผ์„ธ์š”."
๋‹น์—ฐํžˆ ๋ฉ”๋‰ด๋ฅผ ์—ด๊ณ  ๋‹ซ์œผ๋ ค๋ฉด useState๊ฐ€ ํ•„์š”ํ•œ๋ฐ, ๋ ˆ์ด์•„์›ƒ ์ปดํฌ๋„ŒํŠธ ๋ฉ์น˜๊ฐ€ ๋„ˆ๋ฌด ์ปค!

// โŒ ์ˆœ์ง„ํ•œ ์ฝ”๋“œ: ์ž‰ํฌ๋ณ‘ ์—Ž์ง€๋ฅด๊ธฐ
'use client' // ๋ ˆ์ด์•„์›ƒ์— ์ด๊ฑธ ๋ฐ•๋Š” ์ˆœ๊ฐ„, ๋‚ด๋ถ€์˜ ๋ชจ๋“  ์ž์‹(page.tsx) ๋“ค์ด ๋ชจ์กฐ๋ฆฌ ํด๋ผ์ด์–ธํŠธ๋กœ ์˜ค์—ผ ๊ฐ•๋“ฑ๋‹นํ•จ!
 
import ServerHeader from './ServerHeader'
 
export default function RootLayout({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(true);
 
  return (
    <body>
      <ServerHeader /> {/* ๊ฐ•์ œ ํด๋ผ์ด์–ธํŠธ ๋ณ€ํ™˜๋จ */}
      <button onClick={() => setIsOpen(!isOpen)}>๋ฉ”๋‰ด ๋‹ซ๊ธฐ</button>
      <main className={isOpen ? 'w-full' : 'w-1/2'}>
        {children} {/* ๐Ÿ’ฃ ์ด ์ž๋ฆฌ์— ๊ฝ‚ํžˆ๋Š” ๋ฌด๊ณ ํ•œ page.tsx๋“ค๊นŒ์ง€ ์ „๋ถ€ ์˜ค์—ผ๋จ */}
      </main>
    </body>
  )
}

โœ… ์šฐ์•„ํ•œ ํ•ด๊ฒฐ์ฑ…: Children Hole (๊ตฌ๋ฉ ๋šซ๊ธฐ) ํŒจํ„ด

React์˜ {children} ํŒจํ„ด์€ ์ปดํฌ๋„ŒํŠธ๋ฅผ '๋ Œ๋”๋ง ๋œ ๊ฒฐ๊ณผ๋ฌผ(ReactNode)' ํ˜•ํƒœ์˜ ํƒ๋ฐฐ ์ƒ์ž๋กœ ์ „๋‹ฌํ•˜๋Š” ๋งˆ๋ฒ•์ด์•ผ. ๋ถ€๋ชจ๊ฐ€ use client์—ฌ๋„, ์™ธ๋ถ€(์„œ๋ฒ„)์—์„œ ํ†ต์งธ๋กœ ํฌ์žฅ๋œ ์ƒ์ž๋ฅผ ๋ฐ›์•„ ์Šฌ์ฉ ๋ผ์›Œ ๋„ฃ๊ธฐ๋งŒ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— ์•ˆ์˜ ๋‚ด์šฉ๋ฌผ(์„œ๋ฒ„ ์†์„ฑ)์ด ์ „ํ˜€ ์˜ค์—ผ๋˜์ง€ ์•Š์•„.

// app/layout.tsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)
import ClientSidebarWrapper from './ClientSidebarWrapper'
import PureServerContent from './PureServerContent'
 
export default function RootLayout() {
  return (
    <body>
      {/* ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ถ€๋ฅด๋˜, ๊ทธ '๋ฐฐ ์•ˆ(children)'์— 
          ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํ™”๋ฌผ๋กœ ์‹ค์–ด์„œ ๋ณด๋‚ธ๋‹ค! */}
      <ClientSidebarWrapper>
        <PureServerContent /> 
      </ClientSidebarWrapper>
    </body>
  )
}
// app/ClientSidebarWrapper.tsx (ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ)
'use client' 
import { useState } from 'react'
 
// ํ™”๋ฌผ์„ ์ˆ˜๋ นํ•ด์„œ ๊ตฌ๋ฉ์— ์™ ๋ผ์›Œ์ฃผ๊ธฐ๋งŒ ํ•œ๋‹ค. 
// ๋‚ด๊ฐ€ ํด๋ผ์ด์–ธํŠธ๋ผ๊ณ  ํ•ด์„œ, ๋‚จ์ด ํฌ์žฅํ•ด์ค€ ํ™”๋ฌผ ๋‚ด์šฉ๋ฌผ์„ ๊ฐ•์ œ๋กœ ๊น” ์ˆ˜๋Š” ์—†๋‹ค! (์˜ค์—ผ ๋ฐฉ์ง€)
export default function ClientSidebarWrapper({ children }: { children: React.ReactNode }) {
  const [isOpen, setIsOpen] = useState(true);
 
  return (
    <div className="flex">
      <button onClick={() => setIsOpen(!isOpen)}>ํ† ๊ธ€</button>
      <main className={isOpen ? 'w-full' : 'w-1/2'}>
        {children} {/* ๐ŸŒŸ ์ˆœ์ˆ˜ํ•œ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฌด์‚ฌํžˆ ๋ Œ๋”๋ง๋จ! */}
      </main>
    </div>
  )
}

๐Ÿ’ก ํ•œ ์ค„๋กœ ๊ธฐ์–ตํ•˜๊ธฐ
ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ ์•ˆ๋ฐฉ ๋ฌธ์„ ์—ด๊ณ  ์ง์ ‘ import ServerComponent ํ•˜๋ฉด ์˜ค์—ผ๋ผ์„œ ํ„ฐ์ง„๋‹ค.
ํ•˜์ง€๋งŒ ๊ฑฐ์‹ค ์ฐฝ๋ฌธ์„ ๋šซ์–ด๋‘๊ณ ({children}) ๋ถ€๋ชจ ์ชฝ์—์„œ ๋ฐ–์—์„œ ์ง‘์–ด๋„ฃ์–ด ์ฃผ๋ฉด ์„œ๋ฒ„ ํ–ฅ๊ธฐ๋ฅผ 100% ๋ณด์กดํ•  ์ˆ˜ ์žˆ๋‹ค.


๐Ÿ’ฅ ์—๋Ÿฌ ํ•ด๊ฒฐ ์นดํƒˆ๋กœ๊ทธ

์—๋Ÿฌ ๋ฉ”์‹œ์ง€๊ฐ€ ๋œจ๋ฉด Ctrl+F๋กœ ๊ฒ€์ƒ‰ํ•ด๋ด.

โŒ You're importing a component that needs useState...

์›์ธ: useState, useEffect ๋“ฑ ํ›…์„ ์‚ฌ์šฉํ•œ ์ปดํฌ๋„ŒํŠธ ๋งจ ์œ„์— 'use client' ์„ ์–ธ์„ ๋นผ๋จน์Œ.
ํ•ด๊ฒฐ์ฑ…: ํŒŒ์ผ ์ตœ์ƒ๋‹จ(import ์œ„)์— 'use client' ์ง€์‹œ์ž๋ฅผ ์‚ฝ์ž…ํ•œ๋‹ค.

โŒ Event handlers cannot be passed to Client Component props...

์›์ธ: ๋ถ€๋ชจ๊ฐ€ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์ธ๋ฐ, ์ž์‹ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—๊ฒŒ onClick={() => console.log('์•ˆ๋…•')} ์ฒ˜๋Ÿผ ํ•จ์ˆ˜(Function)๋ฅผ ์Œฉ์œผ๋กœ ํ”„๋กญ์Šค๋กœ ๋„˜๊ฒจ์ฃผ๋ ค ํ•  ๋•Œ. ํ•จ์ˆ˜๋Š” ์„œ๋ฒ„์—์„œ ์ง๋ ฌํ™”(Serialization = ํ…์ŠคํŠธ๋กœ ๋ฐ”๊พธ๊ธฐ) ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒ๋ช…์ฒด๋ผ ํ†ต์‹ ๋ง(๋„คํŠธ์›Œํฌ)์„ ํƒ€๊ณ  ๋‚ด๋ ค๊ฐˆ ์ˆ˜ ์—†์Œ.
ํ•ด๊ฒฐ์ฑ…: ์ด๋ฒคํŠธ ํ•จ์ˆ˜ ๋กœ์ง ์ž์ฒด๋ฅผ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ง์ ‘ ๋„ฃ๊ฑฐ๋‚˜, ์–ด์ฉ” ์ˆ˜ ์—†๋‹ค๋ฉด ์ถ”ํ›„์— ๋ฐฐ์šธ [์„œ๋ฒ„ ์•ก์…˜(Server Actions)] ๊ธฐ๋ฒ•์„ ํ†ตํ•ด ํฌ์›Œ๋”ฉํ•ด์•ผ ํ•œ๋‹ค.


๐Ÿ ์ด๋ฒˆ์— ๋ฐฐ์šด ๋‚ด์šฉ ์ด์ •๋ฆฌ

์ƒํ™ฉ๋‚˜์œ ์„ค๊ณ„ โŒ (์ˆœ์ง„ํ•œ ์ ‘๊ทผ)์šฐ์•„ํ•œ ์„ค๊ณ„ โœ… (ํ”„๋กœ ํŒจํ„ด)
ํŽ˜์ด์ง€์— ๋ฒ„ํŠผ ํ•˜๋‚˜ ํ•„์š”ํ•  ๋•Œpage.tsx ํ†ต์งธ๋กœ use client ์„ ์–ธ๋ฒ„ํŠผ ๋ถ€๋ถ„๋งŒ ๋ณ„๋„ ์ปดํฌ๋„ŒํŠธ๋กœ ๋ถ„๋ฆฌ(Leaf) ํ›„ use client ์„ ์–ธ
๋ ˆ์ด์•„์›ƒ์ด ์ƒํƒœ ๊ด€๋ฆฌ(Open/Close) ํ•„์š”ํ•  ๋•Œlayout.tsx ํ†ต์งธ๋กœ use client ์„ ์–ธํ•˜์—ฌ ์ „์ฒด ์ž์‹ ์˜ค์—ผ ์œ ๋ฐœ์ƒํƒœ๋ฅผ ๊ฐ–๋Š” ๊ป๋ฐ๊ธฐ๋งŒ {children} Wrapper๋กœ ๋ถ„๋ฆฌ ํ›„ ์„œ๋ฒ„ ๋ผ์šฐํŒ…์— ์ ์šฉ
์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ํด๋ผ ์ž์‹์œผ๋กœํด๋ผ์ด์–ธํŠธ ํŒŒ์ผ ๋‚ด์—์„œ ์ง์ ‘ import ServerApp ์‹œ๋„์„œ๋ฒ„ ๋ถ€๋ชจ ์ปดํฌ๋„ŒํŠธ์—์„œ {children} ํ”„๋กญ์Šค๋กœ ์ฃผ์ž…

๐Ÿ“ ๋งˆ๋ฌด๋ฆฌ ํ€ด์ฆˆ

๋ฐฐ์› ์œผ๋ฉด ํ•œ ๋ฒˆ ๋น„ํ‹€์–ด์„œ ํ™•์ธํ•ด๋ด์•ผ ํ•ด.

Q1. ์˜์ฒ ์ด๊ฐ€ ํšŒ์›๊ฐ€์ž… ํผ์„ ์กฐ๋ฆฝ ์ค‘์ด์•ผ. app/signup/page.tsx (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ) ์•ˆ์—์„œ, ๋น„๋ฐ€๋ฒˆํ˜ธ ์œ ํšจ์„ฑ ๊ฒ€์‚ฌ๋ฅผ ์œ„ํ•ด ์ƒํƒœ ๊ด€๋ฆฌ๊ฐ€ ํ•„์š”ํ•œ PasswordInput (ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ)์„ ๋ถˆ๋Ÿฌ์™”๊ณ , ๊ทธ ๋ฐ‘์— ์•ฝ๊ด€ ๋‚ด์šฉ์ด ๋‹ด๊ธด ์ •์  ํ…์ŠคํŠธ ๋ฉ์–ด๋ฆฌ TermsText (์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)๋ฅผ ๋ถˆ๋Ÿฌ์™”์–ด. ์ด ๋ Œ๋”๋ง ํŠธ๋ฆฌ ๊ตฌ์กฐ๋Š” ์˜ฌ๋ฐ”๋ฅธ๊ฐ€? ์˜ค์—ผ์ด ๋ฐœ์ƒํ–ˆ๋Š”๊ฐ€?

โœ… ์ •๋‹ต: ์™„๋ฒฝํ•˜๊ฒŒ ์˜ฌ๋ฐ”๋ฅด๋ฉฐ ์–ด๋–ค ์˜ค์—ผ๋„ ๋ฐœ์ƒํ•˜์ง€ ์•Š๋Š”๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: PasswordInput ํŒŒ์ผ ๋‚ด๋ถ€์— 'use client'๊ฐ€ ์„ ์–ธ๋˜์–ด ์žˆ๋”๋ผ๋„, ๊ทธ ์ž‰ํฌ๋Š” ์˜ค์ง ํ•ด๋‹น ํŒŒ์ผ ๋‚ด๋ถ€์™€ ๊ฑฐ๊ธฐ์„œ ํŒŒ์ƒ๋˜๋Š” import ๊ณ„๋ณด์—๋งŒ ํ๋ฅธ๋‹ค. ๊ฐ™์€ ํ˜•์ œ๋กœ ํ˜ธ์ถœ๋œ TermsText๋Š” ์„œ๋กœ ๋…๋ฆฝ๋œ ๊ฐ์ฒด์ด๋ฏ€๋กœ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์˜ ์˜ค์—ผ(์ „์—ผ)์„ ๋ฐ›์ง€ ์•Š๊ณ  ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋กœ์„œ 0KB ๋ฒˆ๋“ค ์šฉ๋Ÿ‰์„ ๊น”๋”ํ•˜๊ฒŒ ์œ ์ง€ํ•œ๋‹ค.

Q2. ์˜ํ˜ธ(FE ๋ฆฌ๋“œ)๊ฐ€ ์˜์ฒ ์ด์—๊ฒŒ ์ค€ ๊ณผ์ œ์ด๋‹ค. "์šฐ๋ฆฌ ๋ธ”๋กœ๊ทธ ๋ณธ๋ฌธ์— ํ•ด๋‹นํ•˜๋Š” <PostContent> ์ปดํฌ๋„ŒํŠธ๋Š” ๋ฌด์กฐ๊ฑด ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ๋ฅผ ์œ ์ง€ํ•ด์„œ ์†๋„๋ฅผ ๋Œ์–ด์˜ฌ๋ ค์•ผ ํ•ด. ๊ทธ๋Ÿฐ๋ฐ ๋…์ž๊ฐ€ ๊ธ€์„ ์Šคํฌ๋กคํ•œ ํผ์„ผํŠธ(%)๋ฅผ ๊ณ„์‚ฐํ•ด์„œ ์ƒ๋‹จ์— ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ”(์ƒํƒœ ๊ด€๋ฆฌ ํ•„์ˆ˜)๋กœ ๊ทธ๋ ค์ฃผ๋Š” <ScrollProgressBar> ๊ธฐ๋Šฅ๋„ ๋„ฃ์–ด์•ผ ํ•œ๋‹จ ๋ง์ด์ง€? ์ž๊ธฐ๋ฅผ ๊ฐ์‹ธ๋Š” ๋ถ€๋ชจ๊ฐ€ ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ์—ฌ์•ผ ํ•˜๋Š”๋ฐ, ์ด ๋ชจ์ˆœ์„ ์–ด๋–ป๊ฒŒ ํ•ด๊ฒฐํ• ๋ž˜?"

โœ… ์ •๋‹ต: {children} ๊ตฌ๋ฉ ๋šซ๊ธฐ ํŒจํ„ด(Composition)์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹จ๋ฒˆ์— ํ•ด๊ฒฐ๋œ๋‹ค.

๐Ÿ’ก ์ƒ์„ธ ํ•ด์„ค: {children} ๊ตฌ๋ฉ ๋šซ๊ธฐ ํŒจํ„ด(Composition) ์„ ์‚ฌ์šฉํ•˜๋ฉด ๋‹จ๋ฒˆ์— ํ•ด๊ฒฐ๋œ๋‹ค! ์ƒํƒœ ์ถ”์ ์ด ํ•„์š”ํ•œ ํ”„๋กœ๊ทธ๋ ˆ์Šค ๋ฐ” ๋กœ์ง์€ ScrollWrapper.tsx ๋ผ๋Š” ํด๋ผ์ด์–ธํŠธ ์ปดํฌ๋„ŒํŠธ ํŒŒ์ผ๋กœ ๋งŒ๋“ค์–ด ์ƒ๋‹จ์— 'use client'๋ฅผ ์„ ์–ธํ•œ๋‹ค. ์ด Wrapper ํŒŒ์ผ ๋‚ด๋ถ€์— ์Šคํฌ๋กค ํ›… ๋กœ์ง๊ณผ <ProgressBar />๋ฅผ ํƒ‘์žฌํ•˜๋˜, ๋ธ”๋กœ๊ทธ ๋ณธ๋ฌธ์ด ๋“ค์–ด๊ฐˆ ์ž๋ฆฌ๋Š” <div className="content">{children}</div> ๋กœ ๋ปฅ ๋šซ์–ด ๊ตฌ๋ฉ(Slot)์„ ๋‚ด์–ด๋†“๋Š”๋‹ค. ๊ทธ๋ฆฌ๊ณ  page.tsx (๋ฃจํŠธ ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ)์—์„œ ์•„๊นŒ ๋งŒ๋“  ํด๋ผ์ด์–ธํŠธ ๋ž˜ํผ๋ฅผ ์ˆ˜์ž…ํ•ด์˜จ ๋’ค, <ScrollWrapper><PostContent /></ScrollWrapper> ํ˜•ํƒœ๋กœ ํ™”๋ฌผ(๋ณธ๋ฌธ)์„ ์‹ค์–ด์„œ ๋ณด๋‚ด์ฃผ๋ฉด ์™„๋ฒฝํ•œ ์ƒ์ƒ ์•„ํ‚คํ…์ฒ˜๊ฐ€ ์™„์„ฑ๋œ๋‹ค.


๐Ÿฃ ์˜์ฒ ์ด์˜ ํ‡ด๊ทผ ์ผ๊ธฐ

์˜ค๋Š˜์€ 'use client' ์ง€์‹œ์ž๊ฐ€ ์–ผ๋งˆ๋‚˜ ๋ฌด์„œ์šด ๋…€์„์ธ์ง€ ๊นจ๋‹ฌ์•˜์–ด. ๊ทธ๋ƒฅ "์ด ํŒŒ์ผ์€ ํด๋ผ์ด์–ธํŠธ์—์„œ ๋Œ์•„๊ฐ€" ๋ผ๋Š” ๋œป์ธ ์ค„ ์•Œ์•˜๋Š”๋ฐ, ๊ฐ•๋ฌผ ํ•˜๋ฅ˜๋ฅผ ๋ชฝ๋•… ์˜ค์—ผ์‹œํ‚ค๋Š” ํŒŒ๋ž€ ์ž‰ํฌ์˜€๋‹ค๋‹ˆ!

๐Ÿ’ก ์˜ค๋Š˜์˜ ๊ตํ›ˆ: "์ž‰ํฌ(use client)๋Š” ์ตœ๋Œ€ํ•œ ํ•˜๋ฅ˜ ๋์ž๋ฝ์— ๋˜‘! ๋–จ์–ด๋œจ๋ฆฌ์ž. ๋ถ€๋“์ดํ•˜๊ฒŒ ์ƒ๋ฅ˜์— ์จ์•ผ ํ•œ๋‹ค๋ฉด ๋ฐ˜๋“œ์‹œ ์ฐฝ๋ฌธ์„ ๋šซ์–ด๋‘์ž."

์˜ํ˜ธ ๋ฆฌ๋“œ ๋‹˜์ด ๊ฐ€๋ฅด์ณ์ฃผ์‹  'Leaf Component' ์™€ 'Children Hole' ํŒจํ„ด์€ ์ •๋ง ๋งˆ๋ฒ• ๊ฐ™์•˜์–ด. ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ์˜ ๊ฐ€๋ฒผ์›€์€ ์œ ์ง€ํ•˜๋ฉด์„œ ์ƒํ˜ธ์ž‘์šฉ๋งŒ ์™์™ ๊ณจ๋ผ๋„ฃ๋Š” ์ด ์พŒ๊ฐ! ์˜ค๋Š˜์€ ํ‡ด๊ทผํ•˜๊ณ  ์ง‘ ๊ฐ€์„œ ์—„๋งˆ๊ฐ€ ๋ณด๋‚ด์ฃผ์‹  ๋ฐ‘๋ฐ˜์ฐฌ์— ๋”ฐ๋œปํ•œ ๋ฐฅ ํ•œ ๋ผ ๋จน์œผ๋ฉฐ ํž๋งํ•ด์•ผ์ง€. ๋‚ด์ผ์€ ์˜ค๋Š˜ ๋ฐฐ์šด '์ˆœ์ˆ˜ํ•œ' ์„œ๋ฒ„ ์ปดํฌ๋„ŒํŠธ ์„ค๊ณ„๋ฒ•์„ ์‹ค๋ฌด์— ์ œ๋Œ€๋กœ ์ ์šฉํ•ด๋ณผ ๊ฑฐ์•ผ! ๐Ÿฃ


๐Ÿ”— ๋” ์•Œ์•„๋ณด๊ธฐ