[Next.js + TS] 5. Server Component vs Client Component
4편까지는 목록·상세·링크만 있었습니다. 전부 읽기에 가깝습니다.
이번 편은 App Router에서 제일 헷갈리는 부분,
서버 컴포넌트 / 클라이언트 컴포넌트 경계를 나눕니다.
이 편에서 할 일
- 기본값이 서버 컴포넌트인 이유 이해하기
'use client'가 필요한 순간 정리하기- 좋아요 버튼을 작은 클라이언트 컴포넌트로 분리하기
MemoCard(서버)가 그 버튼을 조립하는 패턴 익히기
목표는 “전부 'use client'로 만들기”가 아닙니다.
꼭 필요한 곳만 클라이언트로 내리는 감각을 잡는 겁니다.
한 줄 정리
| Server Component | Client Component | |
|---|---|---|
| 기본? | App Router 기본값 | 파일 맨 위에 'use client' |
| 실행 위치 | 서버 | 브라우저(+필요 시 서버에서 사전 렌더) |
hooks (useState 등) |
불가 | 가능 |
| 클릭/입력 이벤트 | 불가 | 가능 |
| 데이터 fetch | 자연스러움 | 가능하지만 목적이 다름 |
React(Vite/CRA)에 익숙하면 전부 클라이언트처럼 느껴집니다.
Next App Router는 기본을 서버에 두고, 인터랙션만 브라우저로 보냅니다.
왜 기본이 서버인가
우리가 만든 MemosPage, MemoCard에는 아직:
useState없음onClick없음- 브라우저 API 없음
그래서 서버 컴포넌트로 충분합니다.
서버에서 HTML을 만들어 보내면:
- 첫 화면을 빨리 그릴 수 있고
- 불필요한 JS를 클라이언트로 덜 보내고
- (나중에) DB/시크릿에 가까운 작업을 서버에 두기 쉽습니다
“React 컴포넌트 = 무조건 브라우저”가 아니라는 점이 핵심입니다.
언제 'use client'가 필요한가
아래가 필요하면 클라이언트 컴포넌트입니다.
useState,useEffect,useRef같은 hooksonClick,onChange같은 이벤트window,localStorage같은 브라우저 API- 일부 브라우저 전용 라이브러리
반대로, 데이터 보여주고 링크로 이동만 하면 서버로 남겨 두세요.
잘못된 습관:
"use client"; // 습관적으로 파일 맨 위에 붙임
export default function MemosPage() {
// 사실 hooks도 이벤트도 없음
}
페이지 전체를 클라이언트로 올리면,
서버 컴포넌트의 이점을 스스로 버리는 셈입니다.
실습 아이디어: 좋아요 버튼
메모 카드에 “좋아요”를 눌러 숫자가 바뀌게 해 봅시다.
이건 useState가 필요하므로 클라이언트가 맞습니다.
하지만 MemoCard 전체를 'use client'로 바꾸지 않습니다.
MemoCard ← 서버 (제목, 내용, 링크)
└─ LikeButton ← 클라이언트 (클릭, state)
큰 덩어리는 서버, 인터랙션 조각만 클라이언트.
이게 App Router에서 자주 쓰는 패턴입니다.
1) LikeButton 만들기
경로: components/LikeButton.tsx
"use client";
import { useState } from "react";
export default function LikeButton() {
const [likes, setLikes] = useState(0);
return (
<button type="button" onClick={() => setLikes((prev) => prev + 1)}>
좋아요 {likes}
</button>
);
}
반드시 파일 맨 위에 "use client" (또는 'use client')가 있어야 합니다.
이 한 줄이 “이 파일은 클라이언트 컴포넌트 경계”라는 표시입니다.
2) 서버 MemoCard에 조립
components/MemoCard.tsx는 'use client'를 붙이지 않습니다.
import Link from "next/link";
import type { Memo } from "@/types/memo";
import LikeButton from "@/components/LikeButton";
type MemoCardProps = {
memo: Memo;
};
export default function MemoCard({ memo }: MemoCardProps) {
return (
<article>
<h2>
<Link href={`/memos/${memo.id}`}>{memo.title}</Link>
</h2>
<p>{memo.content}</p>
<time dateTime={memo.createdAt}>{memo.createdAt}</time>
<div>
<LikeButton />
</div>
</article>
);
}
서버 컴포넌트가 클라이언트 컴포넌트를 자식으로 import하는 것은 OK입니다.
반대로, 클라이언트 컴포넌트가 서버 컴포넌트를 같은 방식으로 막 import해서 서버 전용 이점을 유지하긴 어렵습니다.
(필요하면 children으로 서버 결과를 넘기는 패턴을 씁니다. 지금은 몰라도 됩니다.)
동작 확인
npm run dev/memos에서 좋아요 클릭- 숫자만 올라가면 성공
페이지를 새로고침하면 숫자는 0으로 돌아갑니다.
지금은 메모리 state만 쓰기 때문입니다. (저장은 7편 쪽에서 다룹니다.)
경계를 가르는 기준 (실전용)
고민될 때 이 순서로 물어보세요.
- hooks / 이벤트가 필요한가?
→ 없으면 서버 - 파일 전체가 아니라 버튼/입력만 필요한가?
→ 작은 클라이언트 컴포넌트로 분리 - 데이터 읽기가 주목적인가?
→ 페이지/카드는 서버에 두는 쪽을 먼저 생각
좋은 방향
─────────
page (Server)
└─ MemoCard (Server)
└─ LikeButton (Client)
피하고 싶은 방향
─────────
page (Client) ← 페이지 통째로
└─ 모든 것
자주 하는 실수
1. "use client"를 함수 안에 씀
→ 반드시 파일 최상단 (import보다 위)
2. 서버 컴포넌트에서 useState 사용
→ 에러 납니다. state가 필요하면 그 부분만 클라이언트로 분리
3. Link가 있다고 무조건 클라이언트
→ next/link는 서버 컴포넌트에서도 사용 가능
4. 샘플이라서 전부 클라이언트로 통일
→ 나중에 습관이 됩니다. 처음부터 경계를 나누는 연습을 하세요
이 편에서 가져갈 것
- App Router 기본은 Server Component다
- hooks/이벤트가 필요할 때만 Client Component다
- 페이지 전체가 아니라 인터랙션 조각에
'use client'를 붙인다 - 서버 카드 + 클라이언트 버튼 조합이 기본 패턴이다
다음 편 예고
[Next.js + TS] 6. 데이터 가져오기
- 서버 컴포넌트에서
fetch하기 loading.tsx/error.tsx맛보기lib/memos.ts하드코딩에서 한 걸음 나아가기
지금은 로컬 배열을 읽고 있습니다.
다음 편에서 데이터를 가져오는 방식을 정리합니다.
시리즈 목차
- 왜 Next.js + TypeScript인가
- 프로젝트 만들고 폴더 구조 이해하기
- TypeScript로 컴포넌트 타입 잡기
- 라우팅과 레이아웃
- Server Component vs Client Component ← 지금
- 데이터 가져오기
- 폼과 서버 액션 입문
- 정리 + 다음 학습 로드맵
'frontend > NextJs' 카테고리의 다른 글
| # [Next.js + TS] 7. 폼과 서버 액션 입문 (0) | 2026.07.29 |
|---|---|
| # [Next.js + TS] 6. 데이터 가져오기 (0) | 2026.07.29 |
| # [Next.js + TS] 4. 라우팅과 레이아웃 (0) | 2026.07.29 |
| # [Next.js + TS] 3. TypeScript로 컴포넌트 타입 잡기 (0) | 2026.07.29 |
| # [Next.js + TS] 2. 프로젝트 만들고 폴더 구조 이해하기 (0) | 2026.07.29 |
