본문 바로가기
C.W.K.
Stream
Lesson 08 of 08 · published

컴포넌트 경계에서 자주 하는 실수

~20 min · pitfalls, anti-patterns

Level 0호기심
0 XP0/68 lessons0/11 achievements
0/120 XP to next level120 XP to go0% complete

'use client'를 트리 꼭대기에 붙이지 마

레이아웃이나 페이지에 선언하면 그 파일 아래의 import 그래프가 한꺼번에 클라이언트 번들로 들어가. 상태가 필요한 가장 작은 잎만 승격해. 서버 컴포넌트가 클라이언트 잎을 그리는 방향은 자연스럽지만, 클라이언트 파일이 서버 전용 모듈을 import하는 방향은 성립하지 않아.

서버 전용 모듈은 스스로 경계를 밝혀

DB, 비밀 값, Node 전용 API를 다루는 모듈에는 import 'server-only'를 넣어. 클라이언트 컴포넌트가 실수로 불러오면 빌드가 즉시 실패하니, 배포 뒤 비밀이 새는 것보다 훨씬 싸게 고칠 수 있어.

내부 데이터 흐름마다 API를 만들 필요는 없어

서버 컴포넌트는 데이터를 직접 읽고, 서버 액션은 내부 변경을 직접 처리할 수 있어. 라우트 핸들러는 webhook, 외부 연동, 공개 API처럼 실제 HTTP 계약이 필요한 곳에 남겨. 앱 안의 CRUD를 관성적으로 /api/...로 우회하면 왕복과 인증 경계만 하나 더 생겨.

클래스 인스턴스는 평범한 데이터로 바꿔

ORM 모델, 사용자 정의 Date 하위 클래스, 메서드를 가진 Result 객체는 직렬화 경계를 그대로 넘지 못해. 필요한 필드만 평범한 객체로 뽑아 보내. 메서드가 사라져도 우연히 작동하는 것처럼 보이는 상태를 허용하지 마.

빌드에서 client가 server-only 모듈을 가져오는 실패를 의도적으로 한 번 만들어 경계 검사가 살아 있는지 확인해. 서버 액션과 라우트 핸들러가 겹치는 내부 API도 목록으로 만들고 외부 소비자가 없는 엔드포인트는 직접 서버 호출로 단순화해. 직렬화 경고는 데이터 모양부터 고쳐.

경계를 엄격히 지키겠다고 모든 파일에 얇은 wrapper를 만들면 탐색 비용이 커져. 서버 전용 데이터 모듈, 상호작용 잎, 공용 순수 모듈이라는 몇 가지 방향만 명확히 하고 컴파일러로 지켜. 폴더 이름보다 import 가능 방향이 진짜 아키텍처야.

client 파일에서 server-only 모듈을 import해 빌드가 즉시 실패하는지 확인하고, ORM 객체를 prop으로 넘겨 직렬화 오류를 재현해. 내부 CRUD API 하나를 서버 컴포넌트·액션으로 바꾼 뒤 네트워크 왕복과 인증 검사가 줄거나 빠지지 않았는지 비교해. 경계를 내린 뒤에도 이벤트, 접근성, 초기 HTML이 그대로인지 확인해. 번들만 줄고 기능이 깨졌다면 책임을 분리한 게 아니라 코드를 잘라낸 셈이야.

Code

나쁜 예: 페이지 전체를 클라이언트로 내리기·tsx
'use client'; // 나쁨 — 모든 하위 컴포넌트가 클라이언트 코드가 돼
import { useState } from 'react';

export default function Page() {
  const [open, setOpen] = useState(false);
  return (
    <>
      <HeavyTable />     {/* 표 전체를 JavaScript로 전송 */}
      <MarkdownRenderer />{/* parser도 함께 전송 */}
      <button onClick={() => setOpen(!open)}>Toggle</button>
    </>
  );
}
좋은 예: Toggle만 클라이언트로 두기·tsx
// app/page.tsx — 서버
export default async function Page() {
  const data = await loadAll();
  return (
    <>
      <HeavyTable data={data} />     {/* Server */}
      <MarkdownRenderer />            {/* Server */}
      <ToggleButton />                 {/* 클라이언트 경계 */}
    </>
  );
}

// components/ToggleButton.tsx
'use client';
import { useState } from 'react';
export function ToggleButton() {
  const [open, setOpen] = useState(false);
  return <button onClick={() => setOpen(!open)}>{open ? 'Open' : 'Closed'}</button>;
}
좋은 예: 서버 전용 모듈 경계·ts
// lib/db.ts
import 'server-only';
import { PrismaClient } from '@prisma/client';
export const db = new PrismaClient();
// 'use client' 파일에서 import하면 빌드가 실패해.

External links

Exercise

저장소의 'use client' 파일을 검토해. 파일 전체가 브라우저 실행을 필요로 하는지 확인하고 적어도 두 곳의 선언을 더 작은 잎으로 내려.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인 — 댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.