• 포스트
  • 도구
  • 작업
  • 포스트
  • 도구
  • 작업
로그인회원가입

© 2005 kaisa.co.kr. All Rights Reserved.

소개개인정보처리방침7083620@hanmail.net
← 목록으로
2024-04-15

Server Component와 Client Component

Server Component와 Client Component

요약: 기본은 서버 컴포넌트로 두고, 클릭·입력처럼 브라우저가 필요한 부분만 Client로 나눈다.


1. 왜 이 주제가 필요한가

App Router의 컴포넌트는 기본이 RSC(React Server Component, 서버에서만 실행되는 컴포넌트) 다.

서버에서 돌면 DB·파일·비밀키에 바로 접근할 수 있다. 그 코드는 브라우저 번들에 안 실려서 JS가 줄어든다.

모든 파일을 'use client'로 열면 이 이점이 사라진다. 반대로 서버 파일에 onClick을 붙이면 빌드가 실패한다. 경계를 아는 것이 App Router의 핵심이다.


2. 한 줄 규칙

데이터와 마크업은 서버에 둔다. useState·이벤트·브라우저 API가 필요한 잎만 Client로 만든다.

페이지 전체가 Client일 필요는 없다. Client가 Server를 import하면 그 트리도 클라이언트가 된다. 반대로 Server는 Client를 자식으로 둘 수 있다.


3. 예제

서버가 할 수 있는 것 / 없는 것

Server Component Client Component
기본 app/ 파일, 지시어 없음 파일 상단 'use client'
데이터 async/await, DB, fs fetch·훅. 시크릿·fs 금지
상태 불가 useState / useEffect
이벤트 불가 onClick 등
번들 클라이언트 JS에 안 실림 실림

Client는 잎에 가깝게

// app/posts/_components/counter.tsx
'use client';

import {useState} from 'react';

export function Counter() {
  const [n, setN] = useState(0);
  return (
    <button type="button" onClick={() => setN((v) => v + 1)}>
      {n}
    </button>
  );
}

'use client'는 그 파일과, 그 파일이 import하는 모듈을 클라이언트 경계로 만든다. 지시어는 파일 최상단, import보다 위다.

서버 페이지가 Client를 조합한다

// lib/posts.ts — 서버 전용
export type Post = {slug: string; title: string};

export async function getPosts(): Promise<Post[]> {
  return [
    {slug: 'hello', title: '첫 글'},
    {slug: 'world', title: '둘째 글'},
  ];
}
// app/posts/_components/post-filters.tsx
'use client';

import {useState} from 'react';

type Props = {
  titles: string[];
};

export function PostFilters({titles}: Props) {
  const [q, setQ] = useState('');
  const filtered = titles.filter((t) => t.includes(q));

  return (
    <div>
      <input
        value={q}
        onChange={(e) => setQ(e.target.value)}
        placeholder="제목 검색"
      />
      <ul>
        {filtered.map((t) => (
          <li key={t}>{t}</li>
        ))}
      </ul>
    </div>
  );
}
// app/posts/page.tsx — Server Component
import {getPosts} from '@/lib/posts';
import {PostFilters} from './_components/post-filters';
import {Counter} from './_components/counter';

export default async function PostsPage() {
  const posts = await getPosts();
  const titles = posts.map((p) => p.title);

  return (
    <main>
      <h1>글 목록</h1>
      <PostFilters titles={titles} />
      <Counter />
    </main>
  );
}

서버가 데이터를 가져와 직렬화 가능한 props(문자열, 숫자, 평범한 객체·배열)만 Client에 넘긴다.

넘기면 안 되는 것: 함수, Class 인스턴스, Date를 제외한 특수 객체. 함수를 props로 넘기면 직렬화 에러가 난다.

공유 모듈이 경계를 오염시킨다

Client 파일이 lib/format.ts를 import하면, 그 모듈은 클라이언트 번들에 들어간다. 그 lib/format.ts가 fs를 import하면 브라우저 빌드가 실패한다.

서버 전용 코드는 server-only 패키지로 표시할 수 있다.

import 'server-only';

export function loadSecret() {
  return process.env.API_SECRET;
}

Client가 이 파일을 import하면 빌드 단계에서 막힌다.

큰 라이브러리

차트·에디터처럼 무거운 패키지는 그 UI를 쓰는 Client 파일에서만 import한다. 루트 레이아웃에 두면 모든 페이지 번들이 커진다.

브라우저 전용 라이브러리는 동적 import로 나눈다.

'use client';

import dynamic from 'next/dynamic';

const Chart = dynamic(() => import('./chart'), {
  ssr: false,
  loading: () => <p>차트 로딩</p>,
});

ssr: false는 정말 브라우저에서만 돌아야 할 때만 쓴다. 남용하면 첫 HTML이 비어 검색·미리보기에 불리하다.


4. 흔한 실수

실수 결과 대안
page.tsx 전체에 'use client' 서버 페칭·시크릿 불가 버튼·폼만 분리
Server에서 window / document 빌드·렌더 에러 Client로 이동
Client에서 fs / DB 클라이언트 번들 실패 또는 시크릿 유출 서버에서 읽고 props
이벤트 핸들러를 Server에서 작성 컴파일 에러 Client 자식에 위임
Client가 Server 컴포넌트를 import 그 파일도 클라이언트화 Server가 Client를 자식으로 둔다
훅이 있는 유틸을 서버가 import 경계가 무너진다 훅 파일에 'use client'

정리

기본은 Server다. Client는 상호작용이 있는 잎이다.

서버가 데이터를 가져오고, 직렬화 가능한 값만 Client에 넘긴다. 페이지를 Client로 여는 것은 최후의 수단이다.


연습

  1. 서버 페이지에서 글 목록을 읽고, Client 검색 입력으로 제목을 거른다.
  2. 'use client'가 붙은 파일을 찾아, 상태·이벤트가 없는 파일은 지시어를 제거한다.
  3. npm run build 출력에서 큰 Client 번들이 들어간 라우트를 확인하고, 무거운 import를 해당 페이지로 옮긴다.

댓글

0

아직 댓글이 없습니다. 첫 댓글을 남겨보세요.

이전 글라우팅·레이아웃·동적 세그먼트다음 글데이터 페칭과 캐시 전략
← 목록으로