2단계
2단계 — Server vs Client 컴포넌트
0회 조회
2단계 — Server vs Client 컴포넌트
App Router 의 핵심 개념. 기본이 Server Component, 필요할 때만 'use client'.
1. 왜 분리?
- 많은 코드는 브라우저에 내려갈 필요가 없음
- 서버에서 DB · API 호출 후 HTML 만 전송
- 브라우저 번들 크기 감소 · 초기 로딩 빠름
- 민감 정보 (API 키) 서버에 머무름
2. Server Component (기본)
// src/app/posts/page.tsx
async function getPosts() {
const res = await fetch("https://api.example.com/posts", {
next: { revalidate: 60 }, // 60초 캐시
});
return res.json();
}
export default async function PostsPage() {
const posts = await getPosts(); // 서버에서 실행
return (
<ul className="space-y-2 p-6">
{posts.map((p: any) => (
<li key={p.id}>{p.title}</li>
))}
</ul>
);
}
특징:
- 컴포넌트가
async가능 fetch결과 자동 캐시 · 재검증 (revalidate)useState·onClick사용 불가 (서버에 이벤트 핸들러 없음)document·window접근 불가
3. Client Component
// src/app/counter.tsx
"use client";
import { useState } from "react";
export default function Counter() {
const [n, setN] = useState(0);
return (
<button
onClick={() => setN(n + 1)}
className="rounded bg-blue-600 px-4 py-2 text-white"
>
{n}
</button>
);
}
파일 최상단 'use client' 지시어. 이후 이 파일 및 import 한 컴포넌트 모두 클라이언트 모드.
4. 언제 'use client'?
useState·useEffect·useRefonClick·onChange등 이벤트 핸들러window·localStorage·IntersectionObserver- 브라우저 전용 라이브러리 (Mapbox · chart.js 등)
5. 두 컴포넌트 함께
// src/app/page.tsx (Server)
import Counter from "./counter";
async function getUser() {
return { name: "홍길동" };
}
export default async function Home() {
const user = await getUser();
return (
<main className="p-6 space-y-4">
<h1 className="text-2xl">안녕하세요, {user.name} 님</h1>
<Counter /> {/* Client Component */}
</main>
);
}
Server 가 Client 를 import · 조합 가능. 반대 (Client → Server) 는 직접 불가.
6. 데이터 흐름 — props
// Server
<Counter initial={42} />
// Client
"use client";
export default function Counter({ initial }: { initial: number }) {
const [n, setN] = useState(initial);
...
}
Server 가 가져온 데이터를 Client 에 props 로 전달. 단 직렬화 가능한 값만 (함수 · class 인스턴스 불가).
7. 경계를 얕게 유지
나쁨:
"use client"; // 루트에 선언
// 전체 앱이 클라이언트 모드
좋음:
// app/layout.tsx — Server (기본)
// app/components/ThemeToggle.tsx — Client (이 컴포넌트만)
인터랙션 부분만 Client. 나머지는 Server.
8. cookies() · headers()
Server Component · Server Action 에서만 사용 가능.
import { cookies, headers } from "next/headers";
export default async function Page() {
const cookieStore = await cookies();
const theme = cookieStore.get("theme")?.value ?? "light";
return <div>theme: {theme}</div>;
}
Next 15+ 부터 cookies() · headers() 가 Promise 반환 (async).
9. Server Action — 폼 서브밋
클라이언트에서 서버 함수 호출. API 라우트 작성 없이.
// src/app/new-post/page.tsx
async function createPost(formData: FormData) {
"use server";
const title = formData.get("title") as string;
// DB 저장
console.log("saving:", title);
}
export default function NewPostPage() {
return (
<form action={createPost} className="p-6 space-y-2">
<input name="title" className="border p-2" />
<button type="submit" className="bg-blue-600 text-white px-4 py-2">
저장
</button>
</form>
);
}
form 이 제출되면 createPost 가 서버에서 실행.
10. Streaming + Suspense
비동기 Server Component 를 <Suspense> 로 감싸 부분 로딩.
import { Suspense } from "react";
import PostList from "./post-list"; // async Server Component
export default function Page() {
return (
<main>
<h1>게시글</h1>
<Suspense fallback={<p>불러오는 중...</p>}>
<PostList />
</Suspense>
</main>
);
}
전체 페이지 완성을 기다리지 않고 준비된 부분부터 표시.
11. 캐시 · revalidate
// 1시간 캐시
fetch(url, { next: { revalidate: 3600 } });
// 캐시 안 함
fetch(url, { cache: "no-store" });
// 특정 태그로 무효화 가능
fetch(url, { next: { tags: ["posts"] } });
mutation 후 revalidateTag("posts") 로 즉시 무효화.
12. force-dynamic 회피
export const dynamic = "force-dynamic";
매 요청 재렌더. DB 가 자주 변하고 실시간성이 중요한 페이지에 사용. 남발하면 성능 손해.
13. 자주 걸리는 자리
'use client'없이useState— 빌드 에러- Server 에서
window— 런타임 에러 - 직렬화 불가 props — 함수 · Date 객체 전달 시 경고
- client 에서 DB 직접 — Server Action 또는 API route 경유
cookies()를 Client 에서 — Server 전용
하고픈 말
Server 가 기본 · Client 는 옵트인. 이 한 문장이 App Router 전체를 꿰뚫습니다. 처음에는 모든 걸 Client 로 할 수도 있지만 점점 Server 비중을 올리세요.
Next
- 03-api-drizzle
Next.js — Server and Client Components · React 19 Server Components 를 참고합니다.