이번 편의 결과물: 코드는 바꾸지 않습니다. 03편부터 컴포넌트를 만들 때마다 적용할 판단 기준을 정합니다. · 다루는 개념: 서버/클라이언트 컴포넌트 렌더링 모델, use client가 만드는 경계, 데이터가 흐르는 방향, 상호작용이 필요한 지점 판단 기준
이 편에서 만드는 파일
개념 편입니다. 새로 만들거나 고치는 파일이 없습니다. 04편에서 헤더 컴포넌트를 만들 때 이 편의 기준으로 서버 컴포넌트와 클라이언트 컴포넌트를 나눕니다.
개념 정리
react_3와 다른 점 — “전부 클라이언트”에서 “필요한 곳만 클라이언트”로
react_3의 bookshelf는 모든 컴포넌트가 브라우저에서 실행됐습니다. 컴포넌트마다 서버냐 클라이언트냐를 고민할 필요가 없었습니다. Next.js App Router는 다릅니다. layout과 page를 포함해 모든 컴포넌트는 기본이 서버 컴포넌트이고, 상호작용이 필요한 부분만 파일 맨 위에 use client를 적어 예외로 선언합니다.
'use client'
import { useState } from 'react'
export default function LikeButton() {
const [liked, setLiked] = useState(false)
return <button onClick={() => setLiked(!liked)}>{liked ? '좋아요 취소' : '좋아요'}</button>
}use client는 파일 맨 위, 어떤 import보다도 앞에 옵니다.
클라이언트 컴포넌트가 필요한 경우
| 필요한 기능 | 예시 |
|---|---|
| 상태 | useState, useReducer |
| 이벤트 핸들러 | onClick, onChange, onSubmit |
| 생명주기 훅 | useEffect |
| 브라우저 전용 API | window, localStorage, navigator |
| 커스텀 훅 | 위 기능을 내부에서 쓰는 훅이라면 그 훅을 쓰는 컴포넌트도 클라이언트가 되어야 한다 |
서버 컴포넌트가 유리한 경우
| 이유 | 설명 |
|---|---|
| 데이터에 직접 접근 | 파일 시스템·DB를 fetch 없이 바로 읽을 수 있다(07편에서 실제로 다룸) |
| 비밀 값 보호 | API 키·토큰을 클라이언트 번들에 절대 포함시키지 않는다 |
| 번들 크기 절감 | 서버 컴포넌트의 코드는 브라우저로 전송되지 않는다 |
| 초기 로딩 개선 | 자바스크립트 실행을 기다리지 않고 완성된 HTML을 먼저 보여준다 |
use client가 만드는 경계
한 파일에 use client를 붙이면, 그 파일이 직접 import하는 것과 그 파일이 직접 렌더링하는 컴포넌트까지 클라이언트 번들에 포함됩니다. 반대로 서버 컴포넌트를 클라이언트 컴포넌트의 children으로 전달하면, 그 서버 컴포넌트는 서버에서 미리 렌더링된 결과만 전달되고 클라이언트 번들에 포함되지 않습니다. 그래서 되도록 use client는 상호작용이 필요한 가장 작은 단위(버튼 하나, 검색창 하나)에만 붙이고, 레이아웃 전체를 클라이언트 컴포넌트로 만들지 않습니다.
데이터가 흐르는 방향
서버 컴포넌트에서 클라이언트 컴포넌트로는 props로 값을 넘깁니다. 단, 넘기는 값은 함수나 클래스 인스턴스가 아니라 문자열·숫자·객체·배열처럼 직렬화할 수 있는 값이어야 합니다.
// 서버 컴포넌트
import RatingButton from './rating-button'
export default async function BookDetail({ bookId }) {
const book = await getBook(bookId)
return <RatingButton bookId={book.id} initialRating={book.rating} />
}// 클라이언트 컴포넌트
'use client'
export default function RatingButton({ bookId, initialRating }) {
// ...
}클라이언트에서 서버로 값을 보내 데이터를 바꾸는 방법(Server Actions)은 09편에서 다룹니다. 지금은 서버에서 클라이언트로 가는 방향만 다룹니다.
판단 절차
컴포넌트를 새로 만들 때마다 다음 순서로 판단합니다.
- 이 컴포넌트가 상태나 이벤트 핸들러, 브라우저 API가 필요한가. 아니라면 서버 컴포넌트로 그냥 둔다.
- 필요하다면, 그 부분만 따로 떼어 작은 컴포넌트로 만들고 그 파일에만
use client를 붙인다. - 나머지 화면(데이터를 보여주기만 하는 부분)은 서버 컴포넌트로 남긴다.
직접 해보기
다음 세 컴포넌트가 서버·클라이언트 중 무엇이어야 하는지 판단해보세요. (1) 책 목록을 서버에서 읽어 나열만 하는 컴포넌트, (2) 별점을 클릭해 바꾸는 버튼, (3) 검색어 입력창(타이핑마다 목록을 필터링).
- 서버 컴포넌트입니다. 데이터를 읽어 나열만 하고 상호작용이 없습니다.
- 클라이언트 컴포넌트입니다. 클릭 이벤트와 상태(현재 별점)가 필요합니다.
- 클라이언트 컴포넌트입니다. 입력 이벤트마다 상태가 바뀌어야 합니다.
셋을 한 파일에 몰아넣지 않고, (2)와 (3)만 별도 파일로 분리해 use client를 붙이는 것이 04편부터 이 과목이 따르는 방식입니다.
자주 하는 실수
| 증상 | 원인 | 고치는 법 |
|---|---|---|
layout.js 맨 위에 use client부터 붙이고 시작한다 | SPA 시절 습관, 모든 컴포넌트가 클라이언트라는 가정 | 실제로 상태나 이벤트 핸들러가 필요한 부분만 따로 떼어 그 파일에만 붙인다 |
서버 컴포넌트 안에서 useState를 쓰다 에러가 난다 | 서버 컴포넌트는 훅을 지원하지 않음 | 상태가 필요한 부분을 클라이언트 컴포넌트로 분리한다 |
| 클라이언트 컴포넌트에 큰 데이터 조회 로직을 그대로 둔다 | 서버·클라이언트 구분 없이 짜던 습관 | 데이터 조회는 부모 서버 컴포넌트에서 하고 결과만 props로 내려준다 |