๐ Next.js 5์ฅ: Client Boundary ์ค๊ณ โ ์ด๋์ ์ ์ ๊ทธ์ ๊ฒ์ธ๊ฐ?
๐ ๊ฐ์
Client Component ๊ฒฝ๊ณ๋ฅผ ์ด๋์ ๊ทธ์ด์ผ ํ๋์ง โ ์ค๊ณ ์์น๊ณผ ํํ ์ค์๋ฅผ ์ ๋ฆฌํฉ๋๋ค.
๐ ๋ชฉ์ฐจ
- ๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
- ๐ค ์ ์์์ผ ํ๋๊ฐ
- ๐๏ธ ๋น์ ๋ก ๋จผ์ ์ดํดํ๊ธฐ
- ๐งฉ
use client์ ์ง์ง ์๋ฏธ (๊ฒฝ๊ณ์ ์ ์ธ) ๐ข - ๐ฑ Leaf Component Pattern โ ์ค์ผ์ ์ต์ํํ๋ผ ๐ก
- ๐ก๏ธ ์๋ฒ ์ปดํฌ๋ํธ๋ฅผ ํด๋ผ์ด์ธํธ๋ก ๋๊ธฐ๋ ๋ง๋ฒ (
{children}) ๐ด - ๐ฅ ์๋ฌ ํด๊ฒฐ ์นดํ๋ก๊ทธ
- ๐ ์ด๋ฒ์ ๋ฐฐ์ด ๋ด์ฉ ์ด์ ๋ฆฌ
- ๐ ๋ง๋ฌด๋ฆฌ ํด์ฆ
- ๐ ๋ ์์๋ณด๊ธฐ
๐ ์ด ๋ฌธ์๋ฅผ ์ฝ๊ธฐ ์ ์
โฑ๏ธ ์์ ์ฝ๊ธฐ ์๊ฐ: 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' ํจํด์ ์ ๋ง ๋ง๋ฒ ๊ฐ์์ด. ์๋ฒ ์ปดํฌ๋ํธ์ ๊ฐ๋ฒผ์์ ์ ์งํ๋ฉด์ ์ํธ์์ฉ๋ง ์์ ๊ณจ๋ผ๋ฃ๋ ์ด ์พ๊ฐ! ์ค๋์ ํด๊ทผํ๊ณ ์ง ๊ฐ์ ์๋ง๊ฐ ๋ณด๋ด์ฃผ์ ๋ฐ๋ฐ์ฐฌ์ ๋ฐ๋ปํ ๋ฐฅ ํ ๋ผ ๋จน์ผ๋ฉฐ ํ๋งํด์ผ์ง. ๋ด์ผ์ ์ค๋ ๋ฐฐ์ด '์์ํ' ์๋ฒ ์ปดํฌ๋ํธ ์ค๊ณ๋ฒ์ ์ค๋ฌด์ ์ ๋๋ก ์ ์ฉํด๋ณผ ๊ฑฐ์ผ! ๐ฃ