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

서버 컴포넌트가 줄여 주는 비용

~20 min · bundle, perf, security

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

한 번 변환할 코드를 모두에게 보내지 않아

전통적인 클라이언트 React 앱은 화면을 만드는 컴포넌트 코드와 그 의존성을 방문자의 브라우저로 전부 보냈어. 글 하나를 HTML로 바꾸기 위해 Markdown 파서와 구문 강조기를 사용한다면, 변환은 한 번뿐인데도 모든 방문자가 그 라이브러리를 내려받고 실행해야 했지.

서버에 둘 때 얻는 네 가지

  1. 번들이 작아져. 서버 컴포넌트와 데이터 가공 라이브러리는 클라이언트 JavaScript에 0바이트를 더해.
  2. 데이터에 바로 닿아. 같은 프로세스나 데이터센터의 DB와 내부 서비스를 공개 API 왕복 없이 호출할 수 있어.
  3. 비밀이 서버를 떠나지 않아. API 키, DB 자격 증명, 내부 URL이 브라우저 번들에 섞일 길을 차단해.
  4. 준비된 부분부터 보낼 수 있어. 싼 부분은 즉시 보내고 느린 데이터가 도착하면 뒤이어 스트리밍해.

숫자로 보면 차이가 선명해

marked가 약 200KB, highlight.js가 약 800KB라고 해 보자. 클라이언트에서 글을 변환하면 방문자마다 약 1MB의 JavaScript 비용을 내. 서버 컴포넌트에서 변환하면 두 라이브러리는 전송량에 0바이트만 더하고, 브라우저는 완성된 HTML만 받아.

bundle analyzer에서 큰 클라이언트 의존성을 찾으면 먼저 그 라이브러리가 사용자 이벤트 뒤에도 필요한지 물어봐. Markdown 변환이나 날짜 묶음처럼 렌더 결과만 필요하면 서버 컴포넌트로 옮기고 직렬화된 결과만 내려. 옮긴 뒤 번들 크기뿐 아니라 서버 계산 시간과 캐시도 함께 측정해.

클라이언트 JavaScript 0바이트라는 문구가 전체 비용 0을 뜻하지 않아. 서버는 라이브러리를 실행하고 결과 HTML을 전송해야 하며, 요청마다 반복하면 compute가 커질 수 있어. 재사용 가능한 결과를 캐시하고, 정말 상호작용하는 부분만 브라우저에 남기는 균형이 필요해.

무거운 Markdown 라이브러리를 클라이언트와 서버에 각각 둔 두 build를 만들어 전송 JavaScript, 서버 응답, hydration 시간을 비교해. 요청마다 변환하는 버전과 캐시한 버전도 재서 비용이 브라우저에서 서버로 단순 이동한 건 아닌지 확인해. 같은 페이지를 서버 가공과 클라이언트 가공으로 각각 만들어 전송된 원자료와 JavaScript 크기를 비교해. 민감한 원본이 브라우저에 오지 않는지도 함께 확인해야 해.

Code

무거운 라이브러리를 서버에 남기기·tsx
import { marked } from 'marked';
import hljs from 'highlight.js';
import { format } from 'date-fns';
import { db } from '@/lib/db';

export default async function BlogPost({
  params,
}: {
  params: Promise<{ slug: string }>;
}) {
  const { slug } = await params;
  const post = await db.post.findUnique({ where: { slug } });
  if (!post) return null;

  const html = marked(post.content, {
    highlight: (code, lang) => hljs.highlight(code, { language: lang }).value,
  });

  return (
    <article>
      <time>{format(post.date, 'MMMM d, yyyy')}</time>
      <div dangerouslySetInnerHTML={{ __html: html }} />
    </article>
  );
}
// 클라이언트가 받는 것: 렌더링된 HTML. 번들 영향: 0 byte.
비밀값을 안전하게 읽기·tsx
// app/admin/page.tsx — 인증 뒤에서만도착하지만 비밀값 접근을 보여 주는 예
export default async function Admin() {
  const stripe = process.env.STRIPE_SECRET_KEY!;
  const { data } = await fetch('https://api.stripe.com/v1/customers', {
    headers: { Authorization: `Bearer ${stripe}` },
    cache: 'no-store',
  }).then(r => r.json());
  return <CustomersTable rows={data} />;
}

External links

Exercise

Markdown, 구문 강조, 날짜 가공처럼 표시만 위해 쓰는 무거운 의존성을 서버 컴포넌트로 옮겨. bundle analyzer에서 클라이언트 chunk가 얼마나 줄었는지 확인해.

Progress

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

댓글 0

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

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