~/web-app-dev.md
WEB_APP_DEV

React 19.2의 Partial Pre-rendering은 SSR 속도 옵션이 아니라 셸과 동적 데이터의 경계 계약이다

10분 읽기·2026.07.31·출처 6·00
오늘의 질문

우리 화면에서 먼저 만들어 둘 수 있는 HTML과 요청 시점에만 채울 수 있는 데이터는 어디서 갈라질까?

web-app-dev.md
PPR boundary contract

React 19.2의 Partial Pre-rendering은 SSR 속도 옵션이 아니라 셸과 동적 데이터의 경계 계약이다

  • 카테고리: web_app_dev
  • 예상 읽기 시간: 10분
  • 오늘의 질문: 우리 화면에서 “먼저 만들어 둘 수 있는 HTML”과 “요청 시점에만 채울 수 있는 데이터”는 어디서 갈라질까?
  • 핵심 출처:

1. 왜 지금 봐야 하나

React 19.2는 Activity, useEffectEvent, cacheSignal, React Performance tracks 같은 기능과 함께 React DOM의 Partial Pre-rendering API를 공개했다. 공식 블로그의 설명은 명확하다. 앱의 정적인 부분을 미리 렌더링해 CDN에서 제공하고, 나중에 동적 콘텐츠를 채우기 위해 렌더링을 “resume”할 수 있게 하는 기능이다.

겉으로 보면 “SSR이 더 빨라지는 기능”처럼 들린다. 하지만 웹앱 개발자가 가져가야 할 핵심은 속도보다 경계다. React 19.2의 흐름은 화면을 세 덩어리로 나눠 생각하게 만든다.

  1. 빌드 또는 사전 작업 시점에 만들어도 안전한 정적 셸
  2. 사전 렌더링 중 일부러 멈춰 두는 postponed 상태
  3. 실제 요청, 세션, 권한, 데이터 신선도에 맞춰 이어서 그리는 동적 부분

이 구분은 Next.js 같은 프레임워크에도 바로 연결된다. Next.js 16.3 preview 릴리스 노트에는 Partial Prefetching 관련 변경, runtime data 접근 추적, static prefetch 우선 시도, 충분한 응답을 받을 때까지 prefetch task를 막는 변경이 계속 들어가고 있다. 프레임워크가 알아서 빨라지는 것처럼 보이지만, 실제 제품에서는 “이 route의 셸은 캐시해도 되는가?”, “이 데이터는 prefetch 중 읽어도 되는가?”, “사용자별 권한이 섞인 HTML이 CDN에 남지 않는가?”를 개발자가 판단해야 한다.

또 하나의 최신 신호는 React v19.2.8이다. 2026년 7월 21일 GitHub release에는 React Server Components decoding 성능 개선이 포함됐다. 즉 React의 서버 렌더링·서버 컴포넌트 경로는 계속 성능 개선이 들어오는 현재 진행형 영역이다. 그래서 지금 필요한 공부는 새 API 이름을 외우는 것이 아니라, 서버 렌더링을 정적 셸, postponed state, resume stream, 사용자별 동적 데이터로 분해하는 사고방식이다.

2. 핵심 개념

React의 prerender 문서는 이 API가 React tree를 static HTML로 렌더링하고 Web Stream을 사용한다고 설명한다. 중요한 반환값은 단순 HTML 문자열 하나가 아니라 preludepostponed다.

  • prelude: 클라이언트 또는 CDN에 먼저 보낼 수 있는 HTML stream
  • postponed: 나중에 렌더링을 이어가기 위해 저장해야 하는 상태

그다음 resume 문서는 resume(reactNode, postponedState, options?)가 사전 렌더링된 React tree를 Readable Web Stream으로 이어서 렌더링한다고 설명한다. Node.js 환경에서는 Web Streams 대신 Node Stream용 API를 쓰라는 주의도 붙어 있다.

즉 Partial Pre-rendering의 핵심은 “HTML을 미리 만든다”가 아니다. 더 정확히는 다음 계약이다.

같은 React tree를 두 번 그리되, 첫 번째 렌더링은 안전한 셸까지만 만들고, 두 번째 렌더링은 저장된 postponed state를 기준으로 요청 시점의 동적 부분을 이어 붙인다.

이 계약에서 개발자가 반드시 확인해야 하는 질문이 생긴다.

  • 이 컴포넌트는 사용자 세션 없이 그려도 되는가?
  • 이 fetch는 빌드/사전 렌더링 시점에 실행되어도 되는가?
  • 이 Suspense boundary는 사용자가 “로딩 중”으로 봐도 괜찮은 단위인가?
  • resume 단계가 실패하면 셸은 어떤 fallback을 보여줘야 하는가?
  • postponed state를 어디에 저장하고 어떤 버전의 React tree와 함께 써야 하는가?

여기서 셸(shell)은 단순 레이아웃이 아니다. 제품 관점에서 셸은 “모든 사용자에게 같아도 되는 약속”이다. 반대로 동적 영역은 “요청자, 권한, 시간, 실험군, 데이터 신선도에 따라 달라질 수 있는 약속”이다.

3. 최신 이슈와 연결

React 19.2 블로그는 Partial Pre-rendering 외에도 SSR 관련 변화를 함께 언급한다. 예를 들어 Suspense boundary reveal batching, Node의 SSR Web Streams 지원, cacheSignal 같은 서버 렌더링 주변 기능이 같이 등장한다. 이 조합은 React가 단순 클라이언트 UI 라이브러리를 넘어 렌더링 파이프라인의 수명주기를 더 명시적으로 다루고 있다는 신호다.

특히 cacheSignal은 React Server Components에서 cache()의 수명이 끝나는 시점을 알 수 있게 하는 기능으로 소개된다. 이것은 PPR과 직접 같은 API는 아니지만 같은 방향을 가리킨다. 서버 렌더링에서는 “캐시할 수 있는 계산”과 “더 이상 유효하지 않은 계산”을 런타임이 알아야 한다.

Next.js preview 릴리스의 흐름도 비슷하다. 릴리스 노트에는 Partial Prefetching, route cache LRU, runtime data 접근 추적, static prefetch 우선 시도 같은 변경이 보인다. 이 말은 프레임워크가 다음 질문을 계속 자동화하려 한다는 뜻이다.

  • 이 route는 정적으로 먼저 가져올 수 있는가?
  • prefetch 중 runtime data를 읽었는가?
  • 캐시 키와 LRU 예산은 충분한가?
  • 셸을 먼저 보여주고 나머지를 stream으로 채울 수 있는가?

하지만 자동화가 늘어날수록 개발자의 책임도 사라지지 않는다. 오히려 코드 리뷰에서 “왜 이 컴포넌트가 static shell 안에 들어가도 되는가?”를 설명할 수 있어야 한다.

4. 개발자 관점 해석

작은 웹앱 팀은 PPR을 성능 기능으로만 보면 실수하기 쉽다. “미리 렌더링하면 빠르다”라는 결론은 맞지만 불완전하다. 다음 네 가지 실패 모드를 같이 봐야 한다.

1) 권한이 섞인 셸

사용자 이름, 조직명, 플랜, A/B 실험군, 비공개 카운트가 정적 셸에 들어가면 안 된다. 셸은 CDN에 오래 남을 수 있고, 여러 사용자에게 재사용될 수 있다. 따라서 셸 후보 컴포넌트는 “익명 사용자에게 보여줘도 같은가?”로 먼저 검사한다.

2) Suspense boundary가 제품 언어와 맞지 않음

기술적으로 Suspense를 나눌 수 있어도 사용자가 이해하는 단위와 다르면 UX가 이상해진다. 예를 들어 결제 화면에서 가격은 늦게 뜨고 결제 버튼만 먼저 뜨면 안 된다. boundary는 컴포넌트 트리가 아니라 사용자 과업 단위로 나눠야 한다.

3) resume 실패 경로가 없음

사전 렌더링된 셸은 떴는데 resume stream이 실패하면 사용자는 반쪽짜리 화면을 본다. 그래서 동적 영역마다 “다시 시도”, “로그인 필요”, “권한 없음”, “데이터를 불러올 수 없음”을 구분해야 한다. 로딩 skeleton 하나로 모든 실패를 덮으면 운영 중 원인 파악도 어려워진다.

4) 캐시 키가 제품 경계보다 넓음

정적 셸, route cache, CDN cache, RSC cache가 서로 다른 계층에 존재할 수 있다. 캐시 키가 path만 보고 결정되는데 실제 화면은 locale, tenant, feature flag, auth 상태에 따라 달라진다면 잘못된 셸을 재사용할 수 있다. PPR 설계는 결국 캐시 키 설계와 붙어 있다.

5. 내 프로젝트에 적용할 체크포인트

아래 체크리스트를 route 하나에만 적용해도 충분하다.

  • 셸 후보 표시: header, navigation, marketing copy, 빈 card frame처럼 모든 사용자에게 같은 영역을 표시한다.
  • 동적 데이터 표시: auth session, tenant, permission, user preference, realtime count, 결제 상태처럼 요청 시점 정보가 필요한 영역을 표시한다.
  • Suspense boundary 이름 붙이기: ProfilePanelLoading보다 권한 확인 중, 주문 목록 불러오는 중처럼 사용자 과업 단위로 이름을 붙인다.
  • 캐시 키 적기: path, locale, tenant, experiment, auth 여부 중 셸을 바꾸는 값이 무엇인지 적는다.
  • resume 실패 UI 정하기: 네트워크 실패, 권한 없음, 세션 만료, 데이터 없음, 서버 오류를 최소 두 가지 이상으로 구분한다.
  • 관측 지표 정하기: shell TTFB, dynamic section complete time, resume error rate, Suspense fallback 노출 시간, cache hit rate를 분리해 본다.

AI 코딩 도구를 쓴다면 프롬프트에도 이 경계를 넣어야 한다. 예를 들어 “이 페이지를 PPR 친화적으로 바꿔줘”가 아니라 “정적 셸에 사용자별 데이터가 들어가지 않게 하고, 동적 영역은 Suspense boundary와 실패 UI를 분리해줘”라고 요청해야 한다.

6. 오늘 10분 액션

오늘은 새 API를 설치하지 않아도 된다. 현재 프로젝트의 화면 하나를 골라 10분만 표시해보자.

  1. route 하나를 고른다. 예: /dashboard, /pricing, /projects/[id]
  2. 화면을 세 색으로 나눈다.
    • 초록: 모든 사용자에게 같은 정적 셸
    • 노랑: 요청 시점에 이어서 그려야 하는 동적 데이터
    • 빨강: 절대 캐시된 셸에 들어가면 안 되는 권한/개인정보
  3. 노랑 영역마다 Suspense boundary 이름과 실패 메시지를 쓴다.
  4. 빨강 영역이 초록 컴포넌트의 props로 흘러 들어가는지 확인한다.
  5. “이 route의 셸 캐시 키는 무엇인가?”를 한 문장으로 적는다.

예시 문장:

/dashboard의 정적 셸은 locale과 앱 버전에만 의존한다. 프로젝트 목록, 사용자 이름, 초대 알림 수, 플랜 상태는 resume 단계의 동적 영역으로 분리한다.

이 한 문장을 쓸 수 있으면 PPR을 도입할 준비의 절반은 끝난 것이다.

7. 더 볼 자료

중복 회피 메모

로컬 content/generated와 Supabase 최근 web_app_dev 글을 확인했다. 최근 글은 Supabase 서버 권한 계약, Custom OIDC 신원 경계, TanStack DB 클라이언트 데이터 그래프, RLS 권한 행렬 테스트, Realtime binary payload, Hono 입력 객체 경계, Next.js Partial Prefetching/App Shell, Supabase Pipelines/CDC, Next.js 보안 릴리스와 빌드 도구 계약을 다뤘다. 이번 글은 Next.js Partial Prefetching이나 Supabase 인증/데이터 계층을 반복하지 않고, React 19.2의 prerender/resume/postponed 원시 API를 기준으로 정적 셸과 요청 시점 동적 데이터의 경계 계약을 설계하는 관점에 집중했다.

오늘 10분 액션+5%

댓글 0

최신순 ▾
한 줄 남기기

혹시 이 글을 읽는 동료 개발자가 있다면, GitHub으로 로그인하고 한 줄 흔적을 남겨줘요. (스팸 방지용 로그인이에요)