서버가 데이터를 읽고 클라이언트를 그려
가장 흔한 구조는 서버 컴포넌트가 데이터를 읽은 뒤 작은 클라이언트 컴포넌트를 렌더링하는 방식이야. 클라이언트 껍데기는 이벤트와 상태를 맡고, 서버에서 만든 다른 하위 화면은 children으로 받아 그대로 유지해. 상호작용 때문에 콘텐츠 전체를 클라이언트 번들로 옮길 필요가 없어.
Context provider는 얇게 만들어
React Context를 읽는 hook은 클라이언트에서 실행해야 해. 작은 'use client' provider를 만들고 루트 레이아웃 안에서 필요한 트리만 감싸. provider가 클라이언트라는 이유로 그 안의 모든 서버 컴포넌트가 자동으로 클라이언트가 되는 건 아니야. 서버에서 만든 children을 슬롯으로 받을 수 있거든.
의미가 여러 개면 이름 있는 슬롯을 써
모달의 머리글, 본문, 바닥글처럼 자리가 분명하다면 children 하나보다 여러 ReactNode prop이 읽기 좋아. 각 슬롯은 서버에서 렌더링하고, 클라이언트 컴포넌트는 배치와 열고 닫는 행동만 맡아.
경계를 그리는 목적을 기억해
컴포넌트 수를 줄이는 게 목표가 아니라 브라우저에 보내는 실행 책임을 줄이는 게 목표야. 서버 콘텐츠와 클라이언트 행동을 조합하면 둘 중 하나를 포기하지 않아도 돼.
상호작용 껍데기의 API를 행동과 슬롯으로 나눠. 예를 들어 Modal은 onClose 같은 클라이언트 행동과 서버에서 렌더링한 header, body, footer 슬롯을 받아. 그러면 모달 자체를 재사용하면서 각 콘텐츠의 데이터 읽기와 번들은 서버에 남길 수 있어.
provider를 루트에 둔다고 그 안의 모든 것이 자동 클라이언트가 되지는 않지만, provider 값이 자주 바뀌면 소비하는 클라이언트 하위 트리는 다시 렌더링돼. 편하다는 이유로 거대한 context 하나를 만들지 말고 상태 수명과 갱신 빈도별로 범위를 나눠. 서버에서 만든 header·body·footer를 클라이언트 Modal 슬롯으로 넘기고, Modal 파일이 그 콘텐츠 모듈을 import하지 않는지 확인해. provider 범위를 루트와 하위 구역에 각각 두어 client chunk와 재렌더 범위가 어떻게 달라지는지도 측정해.
provider를 루트로 올리기 전에 실제 소비자를 표시해. 일부 라우트만 필요하다면 가장 가까운 공통 레이아웃에 두어 다른 화면까지 클라이언트 경계로 끌어들이지 마.