dev.syw

두 컴포넌트의 경계와 합성 규칙을 익힌다.

서버 컴포넌트와 클라이언트 컴포넌트

App Router의 가장 큰 개념 변화는 컴포넌트가 기본적으로 서버에서 실행된다는 점입니다. 필요할 때만 일부를 클라이언트로 떼어 냅니다. 이 경계를 잘 그으면 빠르고 안전한 앱을 만들 수 있습니다.

학습 목표

  • 서버 컴포넌트가 기본값인 이유를 이해한다.
  • 'use client' 경계가 어디까지 전파되는지 안다.
  • 언제 서버를, 언제 클라이언트를 쓸지 판단한다.
  • 서버 컴포넌트가 클라이언트 컴포넌트를 감싸는 합성 패턴을 익힌다.

서버 컴포넌트 (기본)

지시어가 없으면 모두 서버 컴포넌트입니다. 서버에서만 실행되므로 DB 접근, 파일 읽기, 비밀 키 사용이 안전합니다. 결과 HTML만 브라우저로 보내고, 컴포넌트 코드 자체는 번들에 포함되지 않습니다.

// app/users/page.tsx — 서버 컴포넌트
import { db } from '@/lib/db';

export default async function UsersPage() {
  const users = await db.user.findMany(); // 서버에서만 실행
  return (
    <ul>
      {users.map((u) => (
        <li key={u.id}>{u.name}</li>
      ))}
    </ul>
  );
}

서버 컴포넌트에서 할 수 없는 것: useState/useEffect 같은 훅, onClick 같은 이벤트 핸들러, window·localStorage 같은 브라우저 API.

클라이언트 컴포넌트

상호작용이 필요하면 파일 맨 위에 'use client' 를 붙입니다. 이 컴포넌트는 서버에서 HTML로 한 번 그려진 뒤, 브라우저에서 다시 살아나(hydration) 상호작용이 가능해집니다.

'use client';
import { useState } from 'react';

export default function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>
      {liked ? '좋아요 취소' : '좋아요'}
    </button>
  );
}

💡 TIP'use client' 를 한 파일에 붙이면, 그 파일이 import 하는 모든 하위 컴포넌트도 클라이언트 경계 안으로 들어옵니다. 그래서 클라이언트 컴포넌트는 가능한 한 트리의 잎(leaf)에 두는 게 좋습니다.

언제 무엇을 쓸까

상황선택
데이터 패칭, DB/파일 접근서버 컴포넌트
비밀 키·토큰 사용서버 컴포넌트
useState/useEffect 등 훅클라이언트 컴포넌트
onClick/onChange 이벤트클라이언트 컴포넌트
localStorage/window 등 브라우저 API클라이언트 컴포넌트

기본은 서버, 상호작용이 필요한 작은 조각만 클라이언트로 — 이 원칙만 기억하세요.

props 직렬화 규칙

서버 컴포넌트가 클라이언트 컴포넌트에 props를 넘길 때, 그 값은 직렬화 가능한 것이어야 합니다. 네트워크를 건너 전달되기 때문입니다.

// 가능: 문자열, 숫자, 불리언, 배열, 일반 객체, Date
<Chart data={[1, 2, 3]} title="매출" />

// 불가능: 함수, 클래스 인스턴스
<Widget onLoad={() => {}} /> // ❌ 함수는 못 넘김

⚠️ 주의 — 서버 → 클라이언트로 함수를 props로 넘길 수 없습니다. 이벤트 핸들러는 클라이언트 컴포넌트 내부에서 정의하세요.

서버 → 클라이언트 합성

흔한 패턴은 서버 컴포넌트가 데이터를 읽어 클라이언트 컴포넌트에 children으로 끼워 넣는 것입니다. 클라이언트 경계 안에서도 서버에서 렌더된 내용을 그대로 보여줄 수 있습니다.

// app/page.tsx — 서버 컴포넌트
import Tabs from './tabs'; // 'use client'
import { getNews } from '@/lib/news';

export default async function Home() {
  const news = await getNews(); // 서버에서 데이터
  return (
    <Tabs>
      {/* children 으로 끼워 넣으면 클라이언트 안에서도 서버 결과 표시 */}
      <ul>
        {news.map((n) => (
          <li key={n.id}>{n.title}</li>
        ))}
      </ul>
    </Tabs>
  );
}

이 강좌 사이트도 마크다운을 서버에서 읽어 본문으로 만들고, 사이드바의 펼침/접힘만 클라이언트 컴포넌트로 처리합니다.

요약

  • 지시어가 없으면 서버 컴포넌트, 'use client' 가 있으면 클라이언트 컴포넌트다.
  • 'use client' 경계는 import 하는 하위 컴포넌트까지 전파된다.
  • 데이터·비밀은 서버에서, 상호작용은 클라이언트에서.
  • 서버 → 클라이언트 props는 직렬화 가능해야 하고, 함수는 넘길 수 없다.

연습문제

  1. 서버 컴포넌트로 글 목록을 그리고, 각 글에 'use client' 좋아요 버튼을 붙여 보세요.
  2. 'use client' 가 트리 위쪽에 있을 때와 잎에 있을 때 번들 크기에 어떤 차이가 생길지 설명해 보세요.
  3. 서버 컴포넌트에서 클라이언트 컴포넌트로 함수를 넘기려다 막혔습니다. 어떻게 해결할까요?

힌트 — 클라이언트 컴포넌트는 가능한 한 작게, 잎에 두세요. 함수가 필요하면 그 동작을 클라이언트 컴포넌트 내부로 옮기거나 서버 액션(다음 레슨들에서 다룸)을 고려합니다.

💡 연습문제 풀이

불러오는 중…

함께 보면 좋은 자료

댓글 0

Next.js” 강좌에 대한 댓글입니다.

댓글을 작성하려면 로그인이 필요합니다.