Server Components와 Client Components 경계 설계: 'use client' 위치를 정하는 기준
번들 크기를 줄이는 경계 배치, children 슬롯 패턴, server-only로 비밀값 보호
핵심 요약
- 'use client'가 붙은 파일이 import하는 모듈과 직접 렌더링하는 컴포넌트는 모두 클라이언트 번들에 들어간다.
- 경계는 상태·이벤트가 필요한 가장 작은 잎 컴포넌트에 둔다.
- 서버 데이터를 클라이언트 컨테이너 안에 보여 줄 때는 children 슬롯으로 넘긴다.
- 비밀값을 쓰는 모듈에는 server-only를 import해 잘못된 import를 빌드 오류로 막는다.
AI 코딩 도구로 Next.js 화면을 만들면 오류를 피하려고 파일 맨 위에 'use client'를 붙이는 경우가 많다. 당장은 동작하지만, 페이지 전체가 클라이언트 번들로 들어가 자바스크립트 크기가 커지고 서버에서만 다뤄야 할 코드가 브라우저 쪽으로 넘어갈 위험이 생긴다. Next.js 공식 문서(16.3 기준)는 레이아웃과 페이지가 기본적으로 Server Component이며, 상호작용이나 브라우저 API가 필요할 때만 Client Component를 덧붙이라고 안내한다. 이 글은 경계를 어디에 둘지 판단하는 기준을 정리한다. 16.3의 라우팅 변화는 Instant Navigations 글에서 다뤘다.
어느 쪽 컴포넌트가 필요한지 판단하는 표
| 필요한 것 | 컴포넌트 | 이유 |
|---|---|---|
useState, onClick, onChange | Client | 상태와 이벤트 핸들러는 브라우저에서 동작 |
useEffect 등 생명주기 로직 | Client | 렌더링 이후 브라우저에서 실행 |
localStorage, window, 위치 정보 | Client | 브라우저 전용 API |
| DB·API 조회 | Server | 데이터 원본 가까이에서 조회 |
| API 키·토큰 사용 | Server | 비밀값을 브라우저에 노출하지 않음 |
| 정적인 본문·목록 표시 | Server | 클라이언트로 보내는 자바스크립트 감소 |
'use client'는 파일 하나가 아니라 경계를 만든다
문서에 따르면 'use client'가 붙은 파일은 서버와 클라이언트 모듈 그래프 사이의 경계가 되고, 그 파일이 가져오는(import) 모듈과 직접 렌더링하는 컴포넌트는 모두 클라이언트 번들에 포함된다. 그래서 하위 컴포넌트마다 지시어를 붙일 필요는 없지만, 반대로 경계를 위쪽에 두면 아래쪽 전체가 클라이언트로 넘어간다.
// 피할 구조: 레이아웃 전체가 클라이언트가 된다
'use client'
import Logo from './logo'
import Search from './search'
export default function Layout({ children }) { /* ... */ }
// 권장 구조: 상호작용이 필요한 Search만 클라이언트
// app/layout.tsx (Server Component)
import Logo from './logo' // Server
import Search from './search' // 'use client'가 붙은 파일
export default function Layout({ children }: { children: React.ReactNode }) {
return (
<>
<nav><Logo /><Search /></nav>
<main>{children}</main>
</>
)
}판단 기준은 단순하다. 경계는 상호작용이 실제로 필요한 가장 작은 컴포넌트에 둔다. 검색창, 좋아요 버튼, 모달 토글처럼 잎(leaf)에 가까운 컴포넌트가 대상이다.
children 슬롯으로 서버 렌더링 결과를 클라이언트 안에 넣기
모달처럼 클라이언트 상태가 필요한 컨테이너 안에 서버에서 조회한 데이터를 보여 줘야 할 때가 있다. 이때 컨테이너가 서버 컴포넌트를 import하면 그 컴포넌트까지 클라이언트로 끌려간다. 문서가 권하는 방법은 children이나 다른 prop으로 넘기는 것이다. 이렇게 전달된 Server Component는 클라이언트 모듈 그래프에 포함되지 않고 서버에서 미리 렌더링된 결과로 전달된다.
// app/ui/modal.tsx
'use client'
export default function Modal({ children }: { children: React.ReactNode }) {
const [open, setOpen] = useState(false)
return open ? <div role="dialog">{children}</div> : <button onClick={() => setOpen(true)}>열기</button>
}
// app/page.tsx (Server Component)
import Modal from './ui/modal'
import Cart from './ui/cart' // 서버에서 장바구니를 조회하는 Server Component
export default function Page() {
return <Modal><Cart /></Modal>
}서버에서 클라이언트로 넘기는 props의 제약
- 직렬화 가능해야 한다: 문서는 Client Component에 넘기는 props가 React가 직렬화할 수 있는 값이어야 한다고 명시한다. 일반 함수, 클래스 인스턴스는 넘길 수 없다.
- 필요한 필드만 넘긴다: DB 레코드 전체를 넘기면 화면에 쓰지 않는 필드까지 RSC Payload에 실려 브라우저로 간다. 표시할 값만 골라 넘긴다.
- Context는 클라이언트에서만: React context는 Server Component에서 지원되지 않으므로, Provider를 Client Component로 만들고
children만 감싸도록 트리 깊은 곳에 둔다.
server-only로 비밀값이 섞이는 사고 막기
서버와 클라이언트가 같은 모듈을 공유할 수 있어서, API 키를 쓰는 함수를 실수로 Client Component에서 import할 수 있다. Next.js는 NEXT_PUBLIC_ 접두사가 없는 환경 변수를 클라이언트 번들에서 빈 문자열로 바꾸므로 키 자체가 노출되지는 않지만, 기능이 조용히 깨진다. 문서는 server-only 패키지를 import해 이런 실수를 빌드 시점 오류로 바꾸라고 권한다.
// lib/data.ts
import 'server-only'
export async function getData() {
const res = await fetch('https://external-service.com/data', {
headers: { authorization: process.env.API_KEY! },
})
return res.json()
}경계를 점검하는 순서
- 프로젝트에서
'use client'가 붙은 파일 목록을 뽑아 레이아웃·페이지 파일이 포함돼 있는지 확인한다. - 포함돼 있다면 실제로 상태나 이벤트를 쓰는 부분만 별도 파일로 분리한다.
- 클라이언트 파일이 서버 전용 모듈(DB 클라이언트, 비밀값 사용 함수)을 import하지 않는지 확인하고, 해당 모듈에
server-only를 추가한다. - 서드파티 컴포넌트가 지시어 없이 클라이언트 기능을 쓰면 자체 Client Component 파일로 감싸 다시 내보낸다.
- 빌드 후 번들 분석으로 클라이언트 자바스크립트 크기 변화를 비교한다.
AI 도구에 화면 작성을 맡길 때는 규칙 파일에 "'use client'는 상호작용 컴포넌트에만, 데이터 조회는 Server Component에서"를 적어 두면 같은 실수가 반복되지 않는다. 규칙 파일 작성은 Cursor Rules 작성법 글을, 서버 액션의 오류 처리는 서버 액션 에러 핸들링 글을 참고한다.
초보자가 자주 실수하는 포인트
- 오류를 피하려고 layout.tsx나 page.tsx에 'use client'를 붙이는 경우
- 클라이언트 컨테이너에서 Server Component를 직접 import하는 경우
- DB 레코드 전체를 Client Component props로 넘기는 경우
- Context Provider로 html 문서 전체를 감싸는 경우
체크리스트
- 레이아웃·페이지 파일에 'use client'가 없는가
- 클라이언트 컴포넌트가 상호작용 부분만 담당하는가
- props로 넘기는 값이 직렬화 가능하고 필요한 필드만 포함하는가
- 비밀값 사용 모듈에 server-only가 있는가
- Provider가 children만 감싸도록 깊은 곳에 있는가
자주 묻는 질문
Client Component 안의 하위 컴포넌트에도 'use client'를 붙여야 하나요?
필요 없다. 경계 파일이 import하고 렌더링하는 컴포넌트는 자동으로 클라이언트 번들에 포함된다.
Client Component도 서버에서 렌더링되나요?
첫 로드에서는 Client Component와 RSC Payload로 HTML을 미리 렌더링하고, 브라우저에서 하이드레이션해 상호작용을 붙인다.
server-only 패키지를 꼭 설치해야 하나요?
Next.js에서는 설치가 선택 사항이며 내부적으로 처리된다. 린트 규칙이 외부 의존성을 검사한다면 설치해 둔다.
참고 자료 · 검증 기준
위 자료와 내용을 대조한 날짜: . 도구·서비스 정책은 이후 바뀔 수 있으므로 적용 전 공식 문서를 다시 확인하세요.
이 글은 위 참고 자료를 바탕으로 정리했으며, 내용은 운영 과정에서 순차적으로 보완될 수 있습니다.