외부 원문 링크
리액트에서 SPA의 현재와 미래
SPA가 끝났다는 분위기와 달리, React 생태계는 render-as-you-fetch·RSC·Activity·ViewTransition·프리로드를 조합해 SPA UX를 다음 단계로 끌어올리고 있다.
핵심 요약
전통 SPA의 fetch-on-render 는 컴포넌트 렌더 후 useEffect 로 fetch를 시작해 워터폴이 발생한다. render-as-you-fetch 는 라우트 로더가 페치를 먼저 시작하고 Suspense fallback 을 즉시 보여주는 패턴이다(React Router v7 / TanStack Router). 추가로 RSC를 빌드 타임에 정적 컴포넌트로 활용, useTransition/useDeferredValue 로 우선순위 분리, Link hover 시 라우트 로더 프리로드, Activity 로 첫 렌더 결과 캐싱, ViewTransition 으로 전환 애니메이션 통합 등이 결합되어 "네이티브 앱 수준 전환"을 노린다.
"SPA vs SSR"이라는 이분법은 낡았다. 실제로 사용자는 (1) 첫 화면이 빨리 보이는가, (2) 인터랙션이 끊기지 않는가, (3) 페이지 전환이 부드러운가만 본다. 현재 React 가 보여주는 방향은 데이터 페칭 시점·렌더 우선순위·전환 애니메이션을 라우터/프레임워크가 협력해서 자동화하는 흐름이다.
면접에서 "SSR vs CSR vs RSC"를 물으면 대부분 정의를 외워서 답한다. 이 글은 "왜 이 패턴이 등장했는가"를 사용자 경험 관점에서 정리해 준다. fetch-on-render 의 워터폴 → render-as-you-fetch 의 로더 우선화 → Activity 로 컴포넌트 사전 렌더 같은 흐름을 답할 수 있어야 깊이가 생긴다.
학습 포인트
면접 답변으로 연결할 학습 포인트입니다.
fetch-on-render vs render-as-you-fetch
두 패턴의 핵심 차이는 **"페치 시작 시점"**이다.
| 패턴 | 페치 시작 | 워터폴 | 대표 도구 |
|---|---|---|---|
| fetch-on-render | 컴포넌트 마운트 후 useEffect | 발생 | 일반 SPA |
| render-as-you-fetch | 라우트 매칭 직후 loader | 줄어듦 | React Router v7, TanStack Router |
로더 패턴은 fallback을 빨리 보여주면서도 데이터를 미리 시작해 체감 지연을 수백 ms 줄인다.
useEffect 안에서 fetch 시작하면서 "우리도 SSR/Suspense 쓴다"고 답하는 것. fetch가 렌더 이후에 시작되면 핵심 이점을 얻지 못한다.
SPA에서도 RSC를 "정적 컴포넌트"로 쓸 수 있다
RSC는 서버 전용이라는 인식이 있지만, 빌드 타임에 RSC 페이로드를 만들어 SPA가 lazy-load 하는 방식도 가능하다. 세션·상호작용이 없는 정적 컨텐츠(문서/마케팅 페이지)는 이 패턴이 맞을 수 있다.
"RSC = Next.js 전용"이라고 단정. 프레임워크 채택이 가장 흔할 뿐, 모델 자체는 빌드 타임에도 적용 가능하다.
동시성 기능은 대형 앱 전용이 아니다
useTransition / useDeferredValue 는 "규모 큰 앱용"이라는 오해가 있다. 실제로는 모든 인터랙션은 임의 순서로 들어오고 메인 스레드는 하나라는 사실 자체가 동시성 도구를 정당화한다. 검색 입력처럼 빈번한 상태 변경에서 우선순위 낮은 업데이트를 표시해 주면 프레임 드롭이 줄어든다.
성능 문제 = React.memo 라는 도식. 자주 변하는 입력은 리렌더 자체가 비싼 거지 props 안정성 문제가 아닐 때가 많다.
`Activity` + 프리로드 + `ViewTransition` 의 조합
Link hover 시 라우트 로더 프리로드 → Activity 로 컴포넌트 첫 렌더 결과 보관 → 전환 시 ViewTransition API 로 부드럽게 교체. 이 셋이 합쳐지면 라우트 전환 지연을 거의 0에 가깝게 만들 수 있다.
이 기능들을 "개별 신기능"으로 외우는 것. 각각의 효과는 작아도, 셋이 결합될 때 UX가 결정적으로 바뀐다.
읽는 순서
- 1이론
라우트 로더 /
Suspense/useTransition/Activity/ViewTransition의 정의와 도입 시점, 호환 브라우저를 표로 정리. - 2구현
TanStack Router 또는 React Router v7 로 데이터 의존 페이지를 두 가지 방식(fetch-on-render vs route loader)으로 구현해 네트워크 워터폴을 비교.
- 3실무
사내 SPA에서 자주 쓰는 라우트에 hover 프리로드 +
Suspensefallback 정책을 적용해 LCP/INP 개선치를 측정. - 4설명
"왜 SPA는 끝났다고 말하기 어렵고, 어떤 부분이 RSC로 이동하는가"를 한 다이어그램으로 설명할 수 있게 정리.
면접 연결 질문
[감점 답변] "React.lazy 쓰면 됩니다". [좋은 답변] 원인을 분리: (1) JS 청크 다운로드 지연 → 라우트 hover 프리로드, (2) 데이터 페치 지연 → 라우트 로더로 페치 선행 + Suspense fallback, (3) 첫 페인트가 비어 있는 문제 → Activity 또는 RSC 정적 컨텐츠로 즉시 표시할 부분 분리, (4) 전환 끊김 → ViewTransition 으로 자연스럽게.
[감점 답변] "로딩 빨리 끝나면 된다". [좋은 답변] (1) useTransition 으로 업데이트를 "전환"으로 표시해 이전 UI 유지, (2) 데이터 캐싱(React Query/TanStack 등)으로 동일 키 재진입 시 fallback 자체를 안 보여주기, (3) Activity 로 첫 렌더 결과를 보관해 재방문 시 즉시 표시.
[감점 답변] "네/아니오" 단정. [좋은 답변] 상호 보완으로 보는 게 맞다. 세션·상호작용 없는 영역은 RSC로 즉시 표시(서버/빌드 타임), 인터랙션이 풍부한 영역은 SPA 모델 유지. 핵심은 "페이지 단위 선택"이 아니라 "컴포넌트 단위 분리"가 가능하다는 점.
자기 점검
워터폴이 "네트워크 단계 문제"라고만 생각하는 것. 실제로는 "렌더 이후 페치 시작"이라는 논리적 순서가 핵심 원인이다.
"성능 = memo" 단일 도구. 동시성과 메모이제이션은 다른 문제(우선순위 vs 참조 동등성)를 푼다.