"use client"를 도대체 어디에 써야 할까? (Next.js 13+ 가이드)
1. "에러가 나서 그냥 다 붙였어요"
Next.js 13 App Router를 처음 도입했을 때, 저는 좀 억울했습니다.
기존 React 코드(useState, useEffect)를 그대로 복사해서 넣었는데, 화면이 빨갛게 물들었거든요.
Error: useState only works in Client Components. Add the "use client" directive at the top of the file.
"아니, 내가 쓰던 React가 왜 안 돼?"
급한 마음에 저는 모든 파일의 맨 윗줄에 "use client"를 붙이기 시작했습니다.
그러자 에러가 사라지고 앱이 잘 돌아갔습니다. 저는 천재인 줄 알았죠.
하지만 며칠 뒤, 빌드 결과물을 보고 경악했습니다. 자바스크립트 번들 사이즈가 이전보다 2배나 커진 것입니다. Next.js의 강력한 기능인 "서버 컴포넌트"를 제 손으로 모조리 꺼버린 덕분이었죠.
2. 서버 컴포넌트 vs 클라이언트 컴포넌트
이 문제를 해결하려면 먼저 "렌더링이 어디서 일어나는가"를 이해해야 합니다.
기본값은 "서버"다
Next.js App Router(app/ 폴더)의 모든 컴포넌트는 기본적으로 서버 컴포넌트(Server Component)입니다.
즉, 브라우저가 아니라 Node.js 서버에서 실행되고 HTML로 변환되어 전송됩니다.
- 장점:
- JS 번들이 0KB입니 (브라우저는 HTML만 받음).
- DB에 직접 접속할 수 있습니다 (비밀번호 노출 걱정 없음).
- 초기 로딩이 엄청 빠릅니다.
"use client"는 "브라우저로 보낼 티켓"이다
특정 파일에 "use client"를 적는다는 건, Next.js에게 이렇게 말하는 것과 같습니다.
"이 파일은 브라우저에서 실행돼야 해. 자바스크립트 번들에 포함시켜서 사용자의 컴퓨터로 보내줘."
브라우저로 보내야 하는 경우는 딱 2가지뿐입니다.
- 상호작용(Interactivity):
onClick,onChange같은 이벤트가 필요할 때. - 브라우저 API:
useState,useEffect,window,document를 써야 할 때.
이 외에는? 무조건 서버에 남겨두는 게 이득입니다.
3. 실제 패턴 - "말단 직원에게만 권한을 줘라"
가장 중요한 규칙은 "최대한 트리의 끝부분(Leaf Node)으로 미루라"는 것입니다.
❌ 나쁜 예 - 상사가 모든 일을 다 함
페이지 전체를 클라이언트 컴포넌트로 만들면, 그 안의 모든 자식들도 강제로 클라이언트 컴포넌트가 됩니다.
// app/page.tsx
'use client'; // 😱 최악의 선택
import Header from './Header';
import PostList from './PostList'; // 거대한 라이브러리 포함
import Footer from './Footer';
export default function Page() {
const [count, setCount] = useState(0); // 고작 이거 때문에...
return (
<div>
<Header />
<PostList /> {/* 얘도 덩달아 클라이언트 번들에 포함됨 */}
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Footer />
</div>
);
}
고작 버튼 하나 때문에 PostList 같은 무거운 컴포넌트까지 전부 브라우저로 전송됩니다. 성능 낭비죠.
✅ 좋은 예 - 버튼만 따로 빼기
인터랙션이 필요한 부분만 별도 파일로 분리하고, 거기에만 "use client"를 붙이세요.
// app/Counter.tsx
'use client'; // 👈 여기만!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// app/page.tsx
// (기본적으로 Server Component)
import Header from './Header';
import PostList from './PostList'; // ✅ 서버에 남아서 JS 0KB!
import Counter from './Counter';
export default function Page() {
return (
<div>
<Header />
<PostList />
<Counter /> {/* 인터랙션이 필요한 곳만 쏙 끼워넣기 */}
<Footer />
</div>
);
}
이제 PostList는 서버에서 HTML로만 렌더링되므로, 사용자는 Counter의 작은 JS 코드만 다운로드하면 됩니다.
4. 깊이 파고들기 - 경계(Boundary) 넘나들기
하지만 실제로는 이렇게 단순하지 않습니다. 서버 컴포넌트 안에 클라이언트 컴포넌트가 있고, 그 안에 다시 서버 컴포넌트를 넣고 싶다면요?
Composition (children) 패턴
클라이언트 컴포넌트 안에서 서버 컴포넌트를 직접 import하면 에러가 납니다 (또는 서버 컴포넌트가 클라이언트로 오염됩니다).
이럴 때는 children prop을 이용해 "구멍"을 뚫어주세요.
// ClientComponent.tsx
'use client';
export default function Card({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="card">
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children} {/* 서버 컴포넌트가 들어올 자리 */}
</div>
);
}
// ServerPage.tsx
import Card from './ClientComponent';
import ServerData from './ServerData';
export default function Page() {
// 서버에서 렌더링 된 HTML을 Card의 children으로 전달
return (
<Card>
<ServerData />
</Card>
);
}
이렇게 하면 ServerData는 여전히 서버에 머물 수 있습니다. 이것이 Next.js 최적화의 핵심입니다.
6. 팁 - 서버 액션(Server Actions)과의 관계
"클라이언트 컴포넌트에서 폼(Form)을 처리하려면 useState를 써야 하잖아요?"
옛날(Next.js 12)에는 그랬습니다. API Route를 만들고 fetch를 날려야 했죠.
하지만 이제는 Server Actions가 있습니다.
함수를 props로 넘기세요
서버에서 실행되는 함수를 만들고, 클라이언트 컴포넌트에 prop으로 넘기거나 import 할 수 있습니다.
// actions.ts
'use server'; // 이 함수는 서버에서만 실행됨
export async function saveData(formData) {
await db.users.create(formData);
}
// Form.tsx
'use client';
import { saveData } from './actions';
export default function Form() {
return (
// useState 없이도 데이터 전송 가능!
<form action={saveData}>
<input name="email" />
<button>가입</button>
</form>
);
}
이렇게 하면, Form 컴포넌트는 브라우저에서 돌지만, 핵심 로직은 서버 액션이 담당하므로 JS 코드를 획기적으로 줄일 수 있습니다.
"use client"를 쓴다고 해서 무조건 useState로 범범이 될 필요는 없다는 뜻입니다.
7. 마무리 - 게으른 개발자가 승리한다
Next.js App Router의 성능 최적화 비결은 "게으름"입니다. 브라우저에게 일을 시키지 마세요. 최대한 서버에서 미리 해서 보내주세요.
"use client"를 타이핑하기 전에, 3초만 고민해 보세요.
- 이게 정말 클릭이 필요한가?
- 이게 정말 브라우저 API(
window)가 필요한가? - 아니라면, 그냥 서버 컴포넌트로 두세요.
여러분의 사용자는 가벼운 앱을 원합니다.
Where Exactly Should I Put "use client"? (Next.js 13+ Guide)
1. "I Just Paste It Everywhere"
When I first migrated to Next.js 13 App Router, I felt attacked.
I copied my existing React code (useState, useEffect), and the screen turned red.
Error: useState only works in Client Components. Add the "use client" directive at the top of the file.
"Why isn't my React working?"
In a panic, I started adding "use client" to the top of every single file.
The errors vanished, and the app worked. I thought I was a genius.
But a few days later, checking the build output, I was horrified. The JavaScript bundle size had doubled. I had single-handedly turned off Next.js's most powerful feature: "Server Components."
2. Server Components vs. Client Components
To fix this, we must understand "Where does rendering happen?"
The Default is "Server"
In Next.js App Router (app/ folder), every component is a Server Component by default.
Basically, they run on the Node.js server, render to HTML, and are sent to the browser.
- Pros:
- JS Bundle size is 0KB (Browser receives pure HTML).
- Direct database access (No API keys exposed).
- Lightning-fast initial load (FCP).
"use client" is a "Ticket to Browser"
Writing "use client" at the top of a file is like telling Next.js:
"This file needs to run in the browser. Bundle it into JavaScript and ship it to the user's computer."
We only need to ship code to the browser in 2 specific cases:
- Interactivity: When you need
onClick,onChange,onSubmit. - Browser APIs: When you need
useState,useEffect,window,localStorage.
For everything else? Keep it on the server. It's free performance.
3. Real-World Pattern: "Delegate to the Intern"
The golden rule is "Push it to the Leaf Nodes (tips of the tree)."
❌ Bad Example: The Boss Does Everything
If you make the entire Page a Client Component, all its children become Client Components too.
// app/page.tsx
'use client'; // 😱 Worst choice
import Header from './Header';
import PostList from './PostList'; // Huge library included
import Footer from './Footer';
export default function Page() {
const [count, setCount] = useState(0); // Just for this button...
return (
<div>
<Header />
<PostList /> {/* Forced to be bundled into Client JS */}
<button onClick={() => setCount(c => c + 1)}>{count}</button>
<Footer />
</div>
);
}
Because of one button, heavy components like PostList are sent to the browser unnecessarily. This is a waste of bandwidth.
✅ Good Example: Extract the Button
Isolate the interactive part into its own file and add "use client" only there.
// app/Counter.tsx
'use client'; // 👈 Only here!
import { useState } from 'react';
export default function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
// app/page.tsx
// (Server Component by default)
import Header from './Header';
import PostList from './PostList'; // ✅ Stays on Server (0KB JS)
import Counter from './Counter';
export default function Page() {
return (
<div>
<Header />
<PostList />
<Counter /> {/* Slot in the interactive part */}
<Footer />
</div>
);
}
Now PostList renders as static HTML on the server, and the user only downloads the tiny JS code for Counter.
4. Deep Dive: Crossing the Boundary
In reality, apps are complex. What if you have a Server Component inside a Client Component, inside another Server Component?
The Composition (children) Pattern
You cannot import a Server Component directly into a Client Component. It causes an error (or worse, silent pollution).
Instead, pass it as a children prop.
// ClientComponent.tsx
'use client';
export default function Card({ children }) {
const [isOpen, setIsOpen] = useState(false);
return (
<div className="card">
<button onClick={() => setIsOpen(!isOpen)}>Toggle</button>
{isOpen && children} {/* Slot for Server Component */}
</div>
);
}
// ServerPage.tsx
import Card from './ClientComponent';
import ServerData from './ServerData';
export default function Page() {
// Server renders ServerData to HTML and passes it to Card
return (
<Card>
<ServerData />
</Card>
);
}
By doing this, ServerData stays on the server. This is the secret to Next.js optimization.
6. Bonus: What About Server Actions?
"Wait, if I use use client for a form, how do I send data to the server?"
In the old days (Next.js 12), you had to create an API Route (pages/api/...) and fetch() it.
But now, we have Server Actions.
server actions are functions, not components
You can define a function that runs on the server, and pass it directly to a Client Component.
// actions.ts
'use server'; // This function runs on the server
export async function submitData(formData) {
await db.create(formData);
}
// Form.tsx
'use client'; // This component runs in the browser
import { submitData } from './actions';
export default function Form() {
return (
<form action={submitData}>
<input name="email" />
<button type="submit">Sign Up</button>
</form>
);
}
This is the beauty of the Boundary.
The Form captures user input in the browser, and hands it over to submitData which executes securely on the server.
You don't need useState or onSubmit handlers for simple forms anymore.
This further reduces the amount of Client-Side JavaScript you need to write.
7. Common Pitfall: "use client" pollution
Be careful when exporting huge utility files.
If you have a utils.ts that imports a heavy library (like moment.js or lodash), and you import a tiny function from it into a Client Component...
The entire utils.ts and all its dependencies might get bundled into the client chunk.
Tree Shaking isn't Magic
Webpack and Turbopack are smart, but they aren't perfect. If you have a file that mixes Server-only logic (DB connection) and Client-safe logic (Date formatting), split them!
utils/db.ts(Server only - installserver-onlypackage to enforce this)utils/format.ts(Shared)
This discipline prevents accidental bundle bloating.