FEInterview Prep

Tistory

리액트 서버 컴포넌트는 정말 성능을 개선할까요?

CSR/SSR/RSC 를 같은 앱에서 *직접 측정* 하면 — RSC 가 마법처럼 빠른 게 아니라 Streaming + Suspense 경계 가 있을 때만 의미가 있다는 사실이 드러난다.

2025-11-20·11분 읽기
ReactNext.js성능
원문 보기 ↗

핵심 요약

측정 메트릭은 (LCP, 사이드바 표시, 메시지 표시, 토글 상호작용, 상호작용 불가능 간극) 다섯 가지. CSR 은 모든 게 늦지만 보이는 즉시 인터랙티브. SSR(서버 패칭) 은 LCP 는 좋지만 JS 다운로드 동안 인터랙션 불가능 한 간극이 생긴다. 본 글의 핵심 결과: (1) Pages Router → App Router 그대로 옮기기 는 LCP 약 500ms 개선되지만 상호작용 가능 시간은 700ms 악화 — 자바스크립트 로드가 CSS 뒤로 밀리는 부수효과. (2) 서버 컴포넌트만 데이터 패칭 그대로 도입하면 번들이 거의 안 줄고 성능 변화 없음. (3) 데이터 패칭을 서버로 옮기되 Suspense 없이 하면 LCP 가 SSR 수준으로 악화. (4) 데이터 패칭을 서버 컴포넌트로 + Suspense 경계로 감싸기 — 이때만 사이드바/메시지가 동시에 빠르게 나타난다. RSC 의 진짜 이득은 Streaming + Suspense 와 한 묶음.

RSC 의 성능 이득을 '서버 컴포넌트 = 자동으로 빠름' 으로 보지 말고 '(1) 자바스크립트 번들에서 코드 빼기, (2) 데이터 패칭을 서버로 옮기기, (3) Suspense 로 청크 경계 그리기 — 이 셋을 동시에 했을 때만 의미 있는 이득이 생기는 도구' 로 본다. 상호작용 불가능 간극(uninteractive gap) 이라는 별도 메트릭으로 SSR 의 진짜 비용을 본다.

'App Router 로 옮기면 빨라진다' 는 흔한 오해를 본 글의 측정 결과는 정면으로 반박한다.

  • 단순 lift-and-shift 는 LCP 만 약간 좋아지고 상호작용 가능 시간 은 오히려 악화
  • 서버 컴포넌트로 일부 컴포넌트를 옮겨도 데이터 패칭 이 클라이언트에 남으면 번들이 거의 줄지 않음
  • Suspense 경계 없이 비동기 서버 컴포넌트만 쓰면 모든 데이터를 await 한 뒤 HTML 을 보내 LCP 가 더 나빠짐
  • 진짜 이득은 데이터 패칭을 서버 컴포넌트로 옮기고 + Suspense 로 점진 stream 했을 때

면접에서 'RSC 의 성능 이점' 을 묻는 질문에 'JS 번들이 줄어든다' 만 말하면 표면 답변이다. 언제 이득이 안 생기는지 까지 말할 수 있어야 한다.

학습 포인트

면접 답변으로 연결할 학습 포인트입니다.

*상호작용 불가능 간극* 은 SSR 의 진짜 비용

SSR 은 LCP 를 빠르게 만들지만, HTML 은 보이는데 JS 가 아직 로드 안 된 시간이 생긴다. 이 동안 버튼 클릭이 무반응이라 고장난 페이지처럼 보인다.

시간:  HTML 파싱 → CSS → LCP → JS 다운로드 → JS 실행 → hydrate → 인터랙티브
        ↑                  ↑                                          ↑
        TTFB              LCP 기록                                 토글 작동
                                  ←———— 상호작용 불가능 간극 ————→

이 간극을 줄이는 유일한 방법은 초기 로드에 필요한 JS 를 줄이는 것 — 코드 스플리팅, 서버 컴포넌트, 또는 더 작은 진입점.

LCPTime to Interactivehydrationuninteractive gap
자주 하는 오해

LCP 만 보고 'SSR 도입했더니 빨라졌다' 고 끝내는 것. 사용자 체감은 클릭이 되는 시점 에 더 가깝다.

Suspense 경계가 없으면 streaming 도 없다

RSC 의 streaming 은 컴포넌트 단위 가 아니라 Suspense 경계 단위 로 청크가 나눠진다. Suspense 없이 비동기 컴포넌트만 쓰면 React 는 전체 트리 await 후 한 번에 응답한다.

// ❌ Suspense 없음 → 전체가 한 청크
export default async function Page() {
  const fast = await fetchFast();  // 100ms
  const slow = await fetchSlow();  // 1000ms
  return <Layout><Sidebar data={fast}/><Messages data={slow}/></Layout>;
}

// ✅ Suspense 경계 → 빠른 부분 먼저 stream
export default function Page() {
  return (
    <Layout>
      <Suspense fallback={<SidebarSkeleton/>}><Sidebar/></Suspense>
      <Suspense fallback={<MessagesSkeleton/>}><Messages/></Suspense>
    </Layout>
  );
}

첫 청크가 즉시 나가고, 사이드바가 100ms 에 도착, 메시지는 1초 뒤 도착 — 체감 속도는 동일하게 빠름.

SuspenserenderToPipeableStream청크 경계loading.tsx
자주 하는 오해

Page 컴포넌트에 loading.tsx 만 두고 모든 데이터 패칭이 자동으로 stream 된다 고 오해. 내부 컴포넌트마다 Suspense 가 따로 필요.

lift-and-shift 마이그레이션은 *느려질 수도* 있다

Pages Router → App Router 로 데이터 패칭은 그대로 두고 옮기기만 하면 본 글의 측정에서 결과는 다음과 같다.

메트릭PagesApp Router (lift-shift)변화
LCP (캐시 X)1.76s1.28s🟢 -480ms
사이드바 표시3.7s4.4s🔴 +700ms
메시지 표시4.2s4.9s🔴 +700ms
토글 상호작용3.1s3.8s🔴 +700ms

App Router 가 CSS 로드 후로 JS 를 지연시켜 LCP 는 좋아지지만, 그 대가로 모든 인터랙션이 늦어진다. 진짜 이득은 데이터 패칭 재작성 + Suspense 까지 했을 때만 나온다.

App Router마이그레이션code splittingJS 지연
자주 하는 오해

'프레임워크만 바꾸면 빨라질 것'. 측정 없는 마이그레이션은 체감 속도를 악화 시킬 수 있다.

읽는 순서

  1. 1이론

    React 공식의 renderToPipeableStream 과 Next.js loading.tsx 문서를 정독하고, Suspense 경계가 청크를 어떻게 만드는지 한 페이지로 정리.

  2. 2구현

    본 글의 GitHub 저장소 를 클론해 본문의 7개 시나리오를 직접 측정한다.

  3. 3실무

    현재 프로젝트에서 가장 느린 데이터 패칭 을 가진 페이지에 Suspense 경계를 추가한 후 LCP/INP 변화를 RUM 으로 비교.

  4. 4설명

    팀에 'App Router 마이그레이션이 자동으로 빠르지 않은 이유' 10분 발표. 축: lift-shift 측정, 서버 패칭 + Suspense, 인터랙션 메트릭.

면접 연결 질문

hard*'서버 컴포넌트만 도입했는데 번들이 거의 안 줄었다'* 는 동료에게 원인을 어떻게 설명하시겠어요?
힌트

[감점 답변] '설정이 잘못됐다'. [좋은 답변] 대부분의 실제 앱은 상태/이벤트 핸들러 가 페이지 상단까지 거슬러 올라가 'use client' 가 트리 대부분을 덮는다. 이 경우 서버 컴포넌트의 코드는 클라이언트 번들에 그대로 포함 되므로 번들이 줄지 않는다. 데이터 패칭을 서버로 옮기는 것 + Suspense 경계 까지 함께 해야 진짜 이득이 시작된다.

medium비동기 서버 컴포넌트와 `<Suspense>` 의 관계를 설명하고, Suspense 가 없으면 stream 이 어떻게 되는지 말해보세요.
힌트

[감점 답변] 'fallback 을 보여준다' 만. [좋은 답변] React 의 renderToPipeableStreamSuspense 경계마다 청크를 끊는다. 경계가 없으면 전체 트리를 await 한 뒤 하나의 청크로 보내므로 가장 느린 데이터를 끝까지 기다려야 HTML 이 시작된다. 즉 Suspense 가 없으면 기존 SSR 과 동일하게 동작.

mediumLCP 는 좋아졌는데 사용자가 *느려졌다고* 항의한다. 어떤 메트릭으로 검증하시겠어요?
힌트

[감점 답변] '다른 메트릭은 신경 안 써도 된다'. [좋은 답변] INP(Interaction to Next Paint) 와 상호작용 가능 시간 을 봐야 한다. LCP 는 시각적 도착, INP 는 클릭/입력 반응성. App Router lift-shift 처럼 LCP 가 좋아지면서 인터랙션이 느려지는 케이스가 흔하다.

자기 점검

본인 프로젝트의 한 페이지에 Chrome Performance 로 *상호작용 불가능 간극* 을 측정해보세요.
LCPTTI인터랙션JS 다운로드
자주 하는 오해

'LCP 만 좋으면 끝'. 사용자는 클릭이 통하는 순간 에 가까운 무언가를 체감한다.

App Router 로 옮긴 한 라우트에서 *Suspense 가 어디에* 있어야 가장 좋을지 그려보세요.
Suspense데이터 의존성공통 레이아웃병렬 패칭
자주 하는 오해

'Suspense 는 page 최상위에 하나만'. 데이터 의존성이 다른 컴포넌트마다 따로 둬야 빠른 부분을 먼저 보낼 수 있다.