본문 바로가기

SPA 이해

MPA·SPA 탐색 구조와 CSR·SSR·SSG 렌더링 전략을 분리하고, 문서 진입·클라이언트 이동·라우팅 책임을 비교합니다.

6장에서는 하나의 앱 안에서 여러 화면을 오가는 라우팅을 다룹니다. 그 바탕이 되는 개념부터 정리해 봅니다. MPA(Multi Page Application)와 SPA(Single Page Application)는 다른 URL로 옮겨 갈 때 새 문서를 여는지, 실행 중인 문서 안에서 화면만 바꾸는지로 나뉘는 탐색 구조입니다. 반면 CSR(Client-Side Rendering), SSR(Server-Side Rendering), SSG(Static Site Generation)는 첫 화면의 HTML을 어디서 언제 만드는지를 설명합니다. 두 분류는 같은 기준이 아니므로, SPA가 반드시 CSR이고 MPA가 반드시 SSR인 것은 아닙니다.

또 React는 UI를 렌더링하는 라이브러리일 뿐이라서, 라우터와 렌더링 전략, 서버 설정, 캐시와 배포 방식은 React 하나로 정해지지 않습니다. 이 글에서는 이 차이들을 하나씩 나눠 봅니다.


탐색 구조와 렌더링 전략은 서로 다른 축입니다

탐색 방식과 HTML 생성 시점은 독립적인 두 축입니다

REACT ROUTING · URL & RESPONSIBILITY

탐색 방식과 HTML 생성 시점은 독립적인 두 축입니다

MPA·SPA는 URL 이동 때 새 문서를 여는지, CSR·SSR·SSG는 초기 HTML을 어디서 언제 만드는지 설명합니다. 한 축의 이름으로 다른 축이나 성능을 단정하지 않습니다.

하나의 조합 · SSR로 진입한 SPA

GET /products/42
→ 서버가 초기 HTML 생성
hydrateRoot → React 앱 시작
내부 Link → 같은 문서에서 탐색

SSR은 첫 HTML 생성 전략이고 SPA는 실행 뒤 내부 탐색 구조입니다. MPA의 개별 문서에서 CSR로 UI를 완성하는 조합도 가능합니다.

탐색 구조와 렌더링 전략을 섞어 읽지 않기 위한 비교
축 · 선택 무엇을 결정하나 함께 쓸 수 있는 조합
MPA

탐색축

URL 이동 때 서버·CDN에 문서를 요청하고 새 문서를 활성화합니다. SSR·SSG가 흔하지만, 각 문서가 CSR로 UI를 완성하는 구성도 가능합니다.
SPA

탐색축

최초·직접 진입은 문서를 요청하고, 실행 뒤 적합한 내부 이동은 현재 문서에서 라우터가 처리합니다. CSR뿐 아니라 SSR·SSG HTML을 hydrate한 뒤 client navigation을 이어 갈 수 있습니다.
CSR

렌더링축

브라우저가 JavaScript 실행 뒤 초기 UI를 만듭니다. SPA 진입 문서 또는 MPA의 개별 문서 안에서 사용할 수 있습니다.
SSR

렌더링축

요청 시점에 서버가 URL의 초기 HTML을 만듭니다. 그대로 문서 탐색을 이어 가거나, hydrate 후 같은 문서 탐색으로 전환할 수 있습니다.
SSG

렌더링축

빌드 시점에 경로의 HTML을 만들어 정적 호스트·CDN에서 제공합니다. 각 정적 문서로 이동하거나, hydrate 후 client navigation을 사용할 수 있습니다.
탐색축 · MPA
URL 이동마다 새 문서를 요청하고 활성화합니다. 물리적인 HTML 파일이나 요청 시점 SSR을 뜻하지는 않습니다.
탐색축 · SPA
최초·직접 진입은 문서 요청입니다. 앱이 실행된 뒤 적합한 내부 이동만 현재 문서에서 history와 client router로 처리합니다.
렌더링축 · CSR
브라우저 JavaScript가 초기 UI를 만듭니다. SPA뿐 아니라 MPA의 개별 문서에도 사용할 수 있습니다.
렌더링축 · SSR
서버가 요청 시점에 초기 HTML을 만듭니다. hydrate 뒤 client navigation을 계속할 수도 있습니다.
렌더링축 · SSG
빌드 때 경로 HTML을 만듭니다. 정적 문서 탐색과 hydrate된 SPA 모두 구성할 수 있습니다.

SEO와 성능은 구조 이름이 아니라 URL·링크·상태 코드·초기 콘텐츠, JavaScript·hydration 비용, route chunk·data 요청과 캐시를 함께 측정해 판단합니다.

위 그림은 탐색 방식과 HTML을 만드는 시점이 서로 독립된 두 축이라는 점을 표로 보여 줍니다. 탐색 축에서 MPA는 URL을 옮길 때마다 서버나 CDN에 문서를 요청해 새 문서를 활성화하며, SSR이나 SSG와 흔히 함께 쓰지만 각 문서가 CSR로 UI를 완성하는 구성도 가능합니다. SPA는 처음 진입하거나 주소를 직접 입력할 때는 문서를 요청하지만, 앱이 실행된 뒤의 알맞은 내부 이동은 현재 문서에서 라우터가 처리합니다. 또 CSR뿐 아니라 SSR이나 SSG로 만든 HTML에서 시작할 수도 있는데, 이때는 미리 만든 HTML에 React가 이벤트와 상태를 이어 붙이는 하이드레이션(hydration)을 거친 뒤 클라이언트 탐색을 이어 갑니다. 하이드레이션은 아래의 CSR·SSR·SSG 절에서 자세히 다룹니다.

렌더링 축에서 CSR은 자바스크립트가 실행된 뒤 브라우저가 초기 UI를 만들며, SPA의 진입 문서나 MPA의 개별 문서 안에서 쓸 수 있습니다. SSR은 요청할 때 서버가 그 URL의 초기 HTML을 만들고, SSG는 빌드할 때 경로의 HTML을 만들어 정적 호스트나 CDN에서 제공합니다. 둘 다 그대로 문서 탐색을 이어 가거나, 하이드레이션한 뒤 같은 문서 안의 탐색으로 바꿀 수 있습니다.

그림 위쪽의 예는 SSR로 진입한 SPA라는 하나의 조합입니다. GET /products/42에 서버가 초기 HTML을 만들어 보내면 hydrateRoot로 React 앱이 시작되고, 그 뒤의 내부 Link는 같은 문서 안에서 탐색합니다. 그림의 설명처럼 SSR은 첫 HTML을 만드는 전략이고 SPA는 실행된 뒤의 내부 탐색 구조이므로, 한 축의 이름으로 다른 축이나 성능을 단정하지 않습니다. 검색 노출과 성능은 구조의 이름이 아니라 URL과 링크, 상태 코드, 초기 콘텐츠, 자바스크립트와 하이드레이션의 비용, 라우트 청크와 데이터 요청, 캐시를 함께 측정해 판단합니다.

MPA: 이동할 때 새 문서를 여는 구조

MPA에서는 일반적인 링크 이동이나 주소 입력이 브라우저의 문서 탐색(document navigation)을 일으킵니다. 브라우저는 목적지 URL을 서버나 CDN에 요청하고, 응답받은 HTML을 새 문서로 파싱합니다. 이전 문서의 자바스크립트 실행 환경은 보통 끝나지만, 브라우저가 문서를 뒤로·앞으로 캐시(bfcache)에 보관했다가 뒤로나 앞으로 이동할 때 되살릴 수도 있습니다.

여기서 “여러 페이지”는 URL마다 정적인 HTML 파일이 꼭 하나씩 있다는 뜻이 아닙니다. 서버가 요청할 때 HTML을 만들 수도 있고(SSR), 빌드할 때 만든 파일을 제공할 수도 있으며(SSG), 응답받은 문서가 브라우저에서 나머지 UI를 그릴 수도 있습니다(CSR).

MPA의 성질도 구현과 배포에 따라 달라집니다. 새 문서로 탐색하면 브라우저가 제목과 포커스, 스크롤, 문서의 생명주기를 기본 동작으로 처리해 준다는 장점이 있습니다. 공통 CSS와 자바스크립트, 이미지는 HTTP 캐시에서 다시 쓸 수 있으므로 페이지마다 모든 리소스를 늘 다시 내려받는 것도 아닙니다. 서버 렌더링의 비용은 캐시와 CDN, 정적 생성 여부에 따라 달라지므로 “MPA이면 서버 부하가 크다”고 단정할 수 없고, 문서를 바꾸는 비용은 있지만 작은 문서와 캐시, 미리 가져오기(prefetch), bfcache를 잘 쓰는 사이트가 SPA보다 반드시 느린 것도 아닙니다.

SPA: 실행 중인 문서 안에서 탐색하는 구조

SPA도 처음 진입할 때, 주소창에 입력할 때, 새로고침할 때, 외부 링크로 들어올 때는 그 URL의 HTML 문서를 요청합니다. 차이는 앱이 실행된 뒤의 클라이언트 내부 탐색에 있습니다.

라우터는 처리할 수 있는 같은 출처의 링크가 일으키는 기본 문서 이동을 대신 막고, History API로 주소 기록을 갱신한 뒤, 현재 URL의 경로(pathname)를 라우트 규칙에 맞춥니다. 경로 매개변수와 쿼리 문자열은 화면의 대상과 보기 조건에, 해시(hash)는 문서 안의 위치나 앱이 정한 상태에 쓸 수 있습니다. 이어서 필요한 컴포넌트와 데이터를 준비하면 React가 현재 문서의 UI를 갱신합니다. 뒤로 가기와 앞으로 가기에서도 라우터가 바뀐 history 항목을 다시 해석합니다.

이 과정에서 네트워크 요청이 사라지는 것은 아닙니다. 라우트별로 코드를 나누면 아직 받지 않은 자바스크립트 청크를 가져올 수 있고, 화면의 데이터도 새로 요청할 수 있습니다. 반대로 이미 받은 청크나 데이터는 캐시 정책에 따라 다시 쓸 수 있습니다. SPA의 대표적인 이점은 공통 레이아웃과 일부 메모리 상태를 유지한 채 화면을 바꿀 수 있다는 점이지만, 초기 자바스크립트와 하이드레이션, 데이터 요청이 줄줄이 이어지는 워터폴(waterfall), 지나치게 큰 번들 때문에 비용이 생길 수 있으므로 “SPA이면 언제나 더 빠르다”는 보장은 없습니다.


CSR·SSR·SSG와 hydration

렌더링 전략은 탐색 구조와 따로 고르며, 프레임워크는 라우트마다 전략을 섞기도 합니다. CSR에서는 서버가 앱의 진입 문서와 리소스를 보내고 브라우저의 자바스크립트가 초기 UI를 만들며, 미리 렌더링한 React 마크업이 없다면 보통 createRoot로 시작합니다. SSR에서는 서버가 요청할 때 그 URL의 초기 HTML을 만듭니다. 응답을 캐시하거나 스트리밍할 수도 있으므로, 요청마다 늘 같은 비용으로 전체 HTML을 한 번에 만든다는 뜻은 아닙니다. SSG에서는 빌드할 때 경로의 HTML을 만들어 정적 호스팅이나 CDN에서 제공하며, 콘텐츠를 언제 다시 만들지는 배포 시스템이 정합니다.

하이드레이션은 SSR이나 SSG로 미리 만든 React HTML에 이벤트와 상태를 이어 붙여, 상호작용할 수 있는 앱으로 만드는 과정입니다. hydrateRoot에 넘긴 첫 클라이언트 결과는 서버가 만든 마크업과 일치해야 합니다(React 문서의 hydrateRoot). 정적인 문서나 순수한 CSR에는 하이드레이션이 꼭 필요하지 않습니다.

검색 엔진 최적화도 MPA나 SPA라는 이름만으로 정해지지 않습니다. URL마다 고유한 제목과 메타데이터, 링크, 실제 콘텐츠가 담긴 초기 HTML, 올바른 HTTP 상태 코드, 그리고 크롤러가 필요한 리소스에 접근할 수 있는지를 함께 확인해야 합니다. CSR 콘텐츠를 처리하는 검색 엔진도 있지만 모든 크롤러가 같은 수준으로 자바스크립트를 실행하지는 않으므로, 검색 노출과 빠른 초기 콘텐츠가 중요하다면 SSR이나 SSG 같은 사전 렌더링을 검토합니다.

더 깊이: 하이드레이션 결과가 서버 HTML과 다를 때

React는 하이드레이션할 때 서버가 만든 마크업을 하나하나 다시 검증하지 않습니다. React 문서의 hydrateRoot에 따르면 개발 모드에서는 불일치를 경고하지만, 대부분의 앱에서 불일치는 드물고 모든 마크업을 검증하는 비용이 너무 크기 때문에 속성의 차이까지 고쳐 준다는 보장은 없습니다. React 19는 불일치를 발견하면 여러 경고 대신 차이를 담은 오류 하나를 남기며, 그 오류 문구가 알리듯 맞지 않는 트리는 클라이언트에서 다시 만들어집니다(React 19 발표 글). hydrateRoot 문서는 이런 불일치가 운이 좋으면 속도 저하로 끝나지만, 나쁘면 이벤트 처리 함수가 엉뚱한 요소에 붙을 수 있으니 다른 버그처럼 고치라고 강조합니다.

흔한 원인은 렌더하는 동안 typeof window !== 'undefined'를 검사하거나 window.matchMedia 같은 브라우저 전용 API를 읽는 것, 서버와 클라이언트가 서로 다른 데이터를 렌더링하는 것, 루트 노드 안의 React HTML 주위에 줄바꿈 같은 공백이 더 들어가는 것입니다. 시각처럼 피할 수 없는 차이는 그 요소에 suppressHydrationWarning을 붙여 경고를 끌 수 있지만, 한 단계 깊이에만 적용되고 React가 다른 글자를 고쳐 주지도 않습니다. 클라이언트에서만 다른 내용을 보여 줘야 한다면 첫 렌더는 서버와 같게 두고 Effect에서 isClient 같은 state를 바꿔 한 번 더 렌더링하는데, 그만큼 하이드레이션은 느려집니다.


첫 진입·내부 이동·직접 링크의 처리

SPA가 실행되는 방식을 이해하려면 “어떤 URL인가?”보다 먼저 “지금 앱이 이미 실행 중인가?”를 물어야 합니다.

직접 진입과 SPA 내부 이동의 책임 흐름 — 상태 추적

직접 /products/42 진입과 실행 중 내부 이동의 문서/host/router/필요 자원/UI 상태가 다르다. host 404와 app view를 구별하며 createRoot/hydrateRoot를 조건에 맞춰 추적한다.

직접 진입은 호스트부터, 내부 이동은 라우터부터 시작합니다

REACT ROUTING · URL & RESPONSIBILITY

직접 진입은 호스트부터, 내부 이동은 라우터부터 시작합니다

같은 URL도 주소창·새로고침에서는 문서 요청이고, 실행 중인 앱의 적합한 내부 링크에서는 현재 문서의 history 이동입니다. 두 성공 경로는 필요한 리소스를 준비하고 React UI를 갱신합니다.

본문 URL · /products/42의 두 시작점

주소창 / 새로고침 → GET /products/42
앱 내부 링크 → history · pathname 매칭
앱이 준비된 뒤 → chunk / CSS / data → UI

직접 요청의 문서·fallback·실제 404는 서버·CDN 책임입니다. 정적 catch-all이 200을 반환했다면 client NotFound 화면만으로 HTTP 404로 바뀌지 않습니다.

직접 진입과 클라이언트 내부 이동 흐름URL 이동이 실행 중인 앱의 내부 탐색인지 판정된다. 내부 이동은 라우터로, 최초 진입과 새로고침은 문서 요청과 호스트 경로 판정으로 간다. 유효한 경로의 초기 HTML은 React 시작으로, 잘못된 경로는 404로 이어지며, 두 성공 경로는 필요한 라우트 리소스를 확인한 뒤 React UI를 갱신한다. URL 이동실행 중인 앱의내부 탐색인가?브라우저 문서 요청GET /products/42클라이언트 라우터history · pathname match유효한 앱 경로를호스트가 처리하는가?초기 HTML + React 시작client-only: createRootpre-render + JS: hydrateRoot정확한 404 응답라우트 리소스 확인chunk · CSS · datacache · prefetch · networkReact UI 갱신title · focus · scroll · status아니요예유효없음
직접 진입과 클라이언트 내부 이동 흐름URL 이동이 실행 중인 앱의 내부 탐색인지 판정된다. 내부 이동은 라우터로, 최초 진입과 새로고침은 문서 요청과 호스트 경로 판정으로 간다. 유효한 경로의 초기 HTML은 React 시작으로, 잘못된 경로는 404로 이어지며, 두 성공 경로는 필요한 라우트 리소스를 확인한 뒤 React UI를 갱신한다.URL 이동실행 중인 앱의내부 탐색인가?브라우저 문서 요청GET /products/42유효한 앱 경로를호스트가 처리?정확한 404 응답초기 HTML + Reactclient-only:createRootpre-render + JS:hydrateRoot클라이언트 라우터history · pathnamematch라우트 리소스 확인chunk · CSS · datacache · prefetchnetworkReact UI 갱신title · focusscroll · status아니요예없음유효
1 · 최초 진입·직접 링크·새로고침
브라우저가 현재 URL을 문서로 요청합니다. 내부 client route보다 서버·CDN의 경로 처리가 먼저입니다.
2 · 호스트 경계
유효한 URL에는 route별 SSR·SSG HTML 또는 SPA 진입 문서를 반환합니다. 존재하지 않는 경로는 실제 404여야 합니다.
3 · React 시작
client-only shell은 createRoot로 렌더링합니다. 미리 렌더된 React HTML을 상호작용 가능하게 만들 때는 동일한 첫 결과로 hydrateRoot를 수행합니다.
4 · 실행 중인 앱의 내부 이동
적합한 링크를 라우터가 받아 history를 갱신하고 pathname을 라우트에 맞춥니다. params·search·hash는 화면 입력으로 쓰며, 필요한 chunk·CSS·data는 cache, prefetch 또는 network에서 옵니다.
5 · 화면과 경계 검증
React가 UI를 갱신한 뒤 title·focus·scroll·pending/error 안내를 확인합니다. 클라이언트 route guard와 별개로 API·서버가 권한을 강제합니다.

같은 문서의 history 이동은 라우터가 화면을 복원하고, 문서 간 뒤로·앞으로 이동은 브라우저 bfcache가 전체 문서를 복원할 수도 있습니다. 어느 경우든 URL 공유와 재진입 결과를 함께 시험합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.
관계도의 보충 설명
원도표의 전체 관계·조건·설명
구분내용
원도표 정의 내용유효한 URL에는 route별 SSR·SSG HTML 또는 SPA 진입 문서를 반환합니다. 존재하지 않는 경로는 실제 404여야 합니다.
원도표 정의 내용client-only shell은 createRoot로 렌더링합니다. 미리 렌더된 React HTML을 상호작용 가능하게 만들 때는 동일한 첫 결과로 hydrateRoot를 수행합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
직접 /products/42host: 문서 요청 판정 / router: 실행 전문서가 없으면 host에 GET /products/42를 요청한다직접 진입과 현재 앱 내부 이동은 출발점이 다르다
HTML과 필요 자원HTML: 전달 전략에 따른 초기 내용 / 자원: chunk·CSS·data허용한 문서 뒤 createRoot 또는 hydrateRoot와 route 자원을 준비한다404라면 이 성공 경로로 가지 않는다
현재 앱 내부 이동document: 유지 / UI: title/focus/scroll/status 정책이미 실행 중이면 router와 history에서 시작해 필요한 자원 뒤 UI를 갱신한다내부 이동도 코드나 data의 network 요청이 없다는 뜻은 아니다

직접 진입과 SPA 내부 이동의 책임 흐름 — 상태 추적

직접 /products/42 진입과 실행 중 내부 이동의 문서/host/router/필요 자원/UI 상태가 다르다. host 404와 app view를 구별하며 createRoot/hydrateRoot를 조건에 맞춰 추적한다.

직접 진입은 호스트부터, 내부 이동은 라우터부터 시작합니다

REACT ROUTING · URL & RESPONSIBILITY

직접 진입은 호스트부터, 내부 이동은 라우터부터 시작합니다

같은 URL도 주소창·새로고침에서는 문서 요청이고, 실행 중인 앱의 적합한 내부 링크에서는 현재 문서의 history 이동입니다. 두 성공 경로는 필요한 리소스를 준비하고 React UI를 갱신합니다.

본문 URL · /products/42의 두 시작점

주소창 / 새로고침 → GET /products/42
앱 내부 링크 → history · pathname 매칭
앱이 준비된 뒤 → chunk / CSS / data → UI

직접 요청의 문서·fallback·실제 404는 서버·CDN 책임입니다. 정적 catch-all이 200을 반환했다면 client NotFound 화면만으로 HTTP 404로 바뀌지 않습니다.

직접 진입과 클라이언트 내부 이동 흐름URL 이동이 실행 중인 앱의 내부 탐색인지 판정된다. 내부 이동은 라우터로, 최초 진입과 새로고침은 문서 요청과 호스트 경로 판정으로 간다. 유효한 경로의 초기 HTML은 React 시작으로, 잘못된 경로는 404로 이어지며, 두 성공 경로는 필요한 라우트 리소스를 확인한 뒤 React UI를 갱신한다. URL 이동실행 중인 앱의내부 탐색인가?브라우저 문서 요청GET /products/42클라이언트 라우터history · pathname match유효한 앱 경로를호스트가 처리하는가?초기 HTML + React 시작client-only: createRootpre-render + JS: hydrateRoot정확한 404 응답라우트 리소스 확인chunk · CSS · datacache · prefetch · networkReact UI 갱신title · focus · scroll · status아니요예유효없음
직접 진입과 클라이언트 내부 이동 흐름URL 이동이 실행 중인 앱의 내부 탐색인지 판정된다. 내부 이동은 라우터로, 최초 진입과 새로고침은 문서 요청과 호스트 경로 판정으로 간다. 유효한 경로의 초기 HTML은 React 시작으로, 잘못된 경로는 404로 이어지며, 두 성공 경로는 필요한 라우트 리소스를 확인한 뒤 React UI를 갱신한다.URL 이동실행 중인 앱의내부 탐색인가?브라우저 문서 요청GET /products/42유효한 앱 경로를호스트가 처리?정확한 404 응답초기 HTML + Reactclient-only:createRootpre-render + JS:hydrateRoot클라이언트 라우터history · pathnamematch라우트 리소스 확인chunk · CSS · datacache · prefetchnetworkReact UI 갱신title · focusscroll · status아니요예없음유효
1 · 최초 진입·직접 링크·새로고침
브라우저가 현재 URL을 문서로 요청합니다. 내부 client route보다 서버·CDN의 경로 처리가 먼저입니다.
2 · 호스트 경계
유효한 URL에는 route별 SSR·SSG HTML 또는 SPA 진입 문서를 반환합니다. 존재하지 않는 경로는 실제 404여야 합니다.
3 · React 시작
client-only shell은 createRoot로 렌더링합니다. 미리 렌더된 React HTML을 상호작용 가능하게 만들 때는 동일한 첫 결과로 hydrateRoot를 수행합니다.
4 · 실행 중인 앱의 내부 이동
적합한 링크를 라우터가 받아 history를 갱신하고 pathname을 라우트에 맞춥니다. params·search·hash는 화면 입력으로 쓰며, 필요한 chunk·CSS·data는 cache, prefetch 또는 network에서 옵니다.
5 · 화면과 경계 검증
React가 UI를 갱신한 뒤 title·focus·scroll·pending/error 안내를 확인합니다. 클라이언트 route guard와 별개로 API·서버가 권한을 강제합니다.

같은 문서의 history 이동은 라우터가 화면을 복원하고, 문서 간 뒤로·앞으로 이동은 브라우저 bfcache가 전체 문서를 복원할 수도 있습니다. 어느 경우든 URL 공유와 재진입 결과를 함께 시험합니다.

이 예의 조건

  • 원도표의 전체 구조와 설명은 위 관계도와 표에서 유지합니다. 아래 인쇄 표에도 원도표의 관계·조건·라벨·설명·주석을 보존합니다.
  • 두 관찰 칸의 위치는 고정입니다. 관찰 상태는 해당 순간의 값이며, 동작·이유는 그 조건에서의 판정과 변화 원인입니다. 독립 비교와 실패 분기는 별도 조건으로 읽고 모든 분기를 한 요청의 필수 실행 순서로 이어 읽지 않습니다.

관계도의 보충 설명

원도표의 전체 관계·조건·설명
구분내용
원도표 정의 내용유효한 URL에는 route별 SSR·SSG HTML 또는 SPA 진입 문서를 반환합니다. 존재하지 않는 경로는 실제 404여야 합니다.
원도표 정의 내용client-only shell은 createRoot로 렌더링합니다. 미리 렌더된 React HTML을 상호작용 가능하게 만들 때는 동일한 첫 결과로 hydrateRoot를 수행합니다.
전체 상태와 변화 근거
장면현재 관찰값설명변화 이유
직접 /products/42host: 문서 요청 판정 / router: 실행 전문서가 없으면 host에 GET /products/42를 요청한다직접 진입과 현재 앱 내부 이동은 출발점이 다르다
HTML과 필요 자원HTML: 전달 전략에 따른 초기 내용 / 자원: chunk·CSS·data허용한 문서 뒤 createRoot 또는 hydrateRoot와 route 자원을 준비한다404라면 이 성공 경로로 가지 않는다
현재 앱 내부 이동document: 유지 / UI: title/focus/scroll/status 정책이미 실행 중이면 router와 history에서 시작해 필요한 자원 뒤 UI를 갱신한다내부 이동도 코드나 data의 network 요청이 없다는 뜻은 아니다

위 그림은 직접 진입은 호스트에서, 내부 이동은 라우터에서 시작한다는 흐름을 보여 줍니다. URL을 옮길 때 먼저 그것이 실행 중인 앱의 내부 탐색인지를 판단합니다. 내부 탐색이라면 클라이언트 라우터가 history를 갱신하고 경로를 맞춥니다. 처음 진입하거나 새로고침하는 경우라면 브라우저가 GET /products/42처럼 문서를 요청하고, 호스트가 그 경로를 처리할 수 있는지에 따라 초기 HTML과 React의 시작으로 이어지거나 정확한 404 응답으로 끝납니다. React의 시작은 클라이언트 전용이면 createRoot로, 미리 렌더링한 HTML이 있으면 hydrateRoot로 합니다. 두 성공 경로는 모두 필요한 라우트 리소스, 곧 청크와 CSS, 데이터를 캐시나 미리 가져오기, 네트워크에서 확인한 뒤 React UI를 갱신하고 제목과 포커스, 스크롤, 상태 안내를 맞춥니다.

그림 아래의 다섯 칸은 각 단계에서 무엇을 확인할지 정리합니다. 처음 진입하거나 직접 링크로 들어오거나 새로고침할 때는 클라이언트의 라우트보다 서버나 CDN의 경로 처리가 먼저입니다. 호스트는 유효한 URL에 라우트별 SSR·SSG HTML이나 SPA의 진입 문서를 돌려주고, 없는 경로에는 실제 404를 돌려줘야 합니다. 실행 중인 앱의 내부 이동에서는 라우터가 알맞은 링크를 받아 history를 갱신하고 경로를 라우트에 맞추며, 경로 매개변수와 쿼리, 해시를 화면의 입력으로 씁니다. 마지막으로 화면이 바뀐 뒤 제목과 포커스, 스크롤, 대기·오류 안내를 확인하고, 클라이언트의 라우트 가드와 별개로 API와 서버가 권한을 검사해야 합니다. 그림의 마지막 설명처럼 같은 문서 안의 history 이동은 라우터가 화면을 되살리고, 문서 사이의 뒤로·앞으로 이동은 bfcache가 문서 전체를 되살릴 수도 있으므로, URL을 공유하고 다시 들어왔을 때의 결과를 함께 시험합니다.

직접 링크와 새로고침

사용자가 /products/42를 주소창에 입력하거나 그 주소에서 새로고침하면, 브라우저는 서버에 /products/42 문서를 요청합니다. 그러면 배포된 서버나 호스팅 쪽에서 셋 가운데 하나를 해야 합니다. SSR이나 SSG로 만든 경로라면 그 URL의 문서를 돌려줍니다. 하나의 진입 문서를 쓰는 클라이언트 라우팅 SPA라면 유효한 앱 경로를 진입 HTML로 이어 줘야 하며, 이 대체 응답(fallback)이 없으면 앱 안에서의 이동은 되지만 직접 링크로 들어올 때 404가 날 수 있습니다.

정확한 HTTP 상태가 필요한 배포라면 없는 경로에 실제 404를 돌려주는 규칙을 둡니다. 클라이언트 전용의 정적 catch-all이 진입 문서를 200으로 보내는 경우도 있으므로, 화면에 보이는 “찾을 수 없음” UI와 응답의 상태 코드는 따로 확인해야 합니다. API나 정적 자산, 잘못된 URL까지 모두 진입 HTML과 200으로 바꿔 버리면 오류 처리와 검색 색인이 왜곡됩니다.

React, 라우터, 서버가 맡는 범위

각 주체가 맡는 일은 다음과 같이 나뉩니다. 브라우저는 문서 탐색과 URL, history, HTTP 캐시, 문서의 생명주기를 맡고, 서버와 CDN은 URL에 맞는 문서나 대체 응답, 404, 캐시 헤더와 보안 헤더를 정합니다. 라우터는 실행 중인 앱에서 URL을 라우트에 맞추고 링크 탐색과 매개변수, 쿼리, 라우트 데이터, 코드 분할을 연결하며, React는 선택된 컴포넌트 트리를 렌더링하고 상태의 변화에 맞춰 DOM을 갱신하고 미리 렌더링한 HTML에는 하이드레이션으로 연결됩니다. API와 서버는 입력 검증과 인증·인가를 요청마다 강제합니다. 클라이언트의 라우트 가드나 숨긴 버튼은 사용자 경험을 돕는 보조 수단일 뿐 보안 장치가 아닙니다.

React 자체는 라우터도, SSR·SSG 배포 시스템도 아닙니다. 작은 기존 문서의 일부에만 React를 더할 수도 있고, MPA의 문서마다 독립된 React 루트를 둘 수도 있으며, 프레임워크로 서버 렌더링한 SPA를 만들 수도 있습니다.

한 URL을 두 가지 진입 방식으로 읽기

같은 /products/42?tab=spec#buy라도 이미 열린 앱의 링크를 누르는 경우와 새 탭의 주소창에 붙여 넣는 경우는 처음 일하는 쪽이 다릅니다. 앞의 경우에는 실행 중인 라우터가 경로를 읽고 화면을 바꿀 수 있습니다. 뒤의 경우에는 먼저 호스트가 /products/42?tab=spec의 문서 요청에 응답해야 하며, 해시인 #buy는 그 HTTP 요청에 포함되지 않습니다. 앱의 문서를 받은 뒤에야 라우터와 React가 실행됩니다.


성능·보안·접근성 확인표

구조의 이름보다 실제 사용자의 흐름을 측정하고 확인합니다. 성능은 초기 HTML에 쓸모 있는 콘텐츠가 있는지, 자바스크립트와 라우트 청크의 크기, 하이드레이션 비용, 데이터 요청의 워터폴, 캐시 적중과 다시 방문했을 때의 전환을 각각 확인합니다. 보안 면에서는 MPA와 SPA 모두 XSS(cross-site scripting)가 생길 수 있으므로, 믿을 수 없는 HTML을 넣지 말고 CSP(Content Security Policy) 같은 방어를 적용합니다. 두 개념은 웹 입문 교재의 웹 보안 기초 (HTTPS, XSS, CSRF)에서 다룹니다. 비밀 값과 최종 권한 판단은 클라이언트 번들에 맡기지 않습니다.

접근성 면에서는 실제 href가 있는 링크와 의미 있는 문서 구조를 유지합니다. 클라이언트 탐색은 새 문서를 열지 않으므로, 라우트가 바뀐 뒤의 문서 제목과 주 콘텐츠로의 포커스, 로딩과 완료의 안내, 스크롤 복원을 앱과 라우터가 직접 설계해야 합니다. 오류 처리에서는 처음 진입할 때와 내부 이동할 때 모두 로딩, 네트워크 실패, 권한 부족, 찾을 수 없는 라우트가 올바른 상태와 회복 방법을 보여야 합니다.


정리

  • MPA와 SPA는 URL을 옮길 때 새 문서를 여는지로 나뉘는 탐색 구조이고, CSR·SSR·SSG는 초기 HTML을 어디서 언제 만드는지로 나뉘는 렌더링 전략입니다.
  • SPA도 처음 진입하거나 새로고침할 때는 문서를 요청하며, 앱이 실행된 뒤의 내부 이동만 라우터가 같은 문서 안에서 처리합니다.
  • 하이드레이션은 미리 만든 React HTML을 상호작용하는 앱으로 잇는 과정이며, 첫 클라이언트 결과가 서버의 마크업과 일치해야 합니다.
  • 클라이언트 라우팅 SPA는 깊은 경로에도 진입 HTML을 돌려주는 대체 응답이 있어야 직접 링크와 새로고침이 동작합니다.
  • 화면의 “찾을 수 없음” UI와 HTTP 404 상태는 별개이며, 실제 404는 호스트나 서버가 돌려줘야 합니다.
  • 권한은 클라이언트의 라우트 가드가 아니라 API와 서버가 요청마다 검사합니다.

연습 문제

1. 앱 안의 링크로 이동하기

클라이언트 전용 앱의 홈은 이미 열리지만, 호스트에는 깊은 경로에 대한 대체 응답이 없다고 가정합니다. 홈의 앱 링크로 /products/42로 이동하면 어떻게 될까요?

해설 보기

앱 안에서 정상적인 라우트가 맞춰질 수 있습니다. 새 문서를 요청하지 않고 실행 중인 라우터가 화면을 고르므로 호스트의 대체 응답을 거치지 않습니다. 다만 그 화면에 필요한 데이터 요청까지 반드시 성공한다는 뜻은 아닙니다.

2. 그 화면에서 새로고침하기

1번의 /products/42 화면에서 브라우저의 새로고침을 누르면 어떻게 될까요? 무엇부터 고쳐야 할까요?

해설 보기

새로고침은 /products/42에 대한 새 문서 요청이므로, 호스트에 대체 응답이 없으면 404로 실패할 수 있습니다. 1번에서 이미 확인한 클라이언트 라우트를 다시 더하기보다, 먼저 호스트가 깊은 경로에 진입 HTML을 돌려주도록 설정을 고쳐야 합니다.

3. 200으로 보이는 찾을 수 없음 화면

호스트가 /unknown에도 진입 HTML을 200 OK로 보낸 뒤, 앱이 찾을 수 없음 화면을 보여 줍니다. 그 화면에 “404”라는 글자를 쓰면 HTTP 응답도 404로 바뀔까요?

해설 보기

바뀌지 않습니다. 앱이 찾을 수 없음 UI를 보여 줘도 진입 문서의 응답은 여전히 200 OK입니다. 실제 HTTP 404가 필요하다면 경로를 아는 호스트나 서버, 또는 사전 생성 규칙이 있어야 합니다. 화면의 메시지와 네트워크의 상태 코드를 각각 확인하세요.

다음 글 React Router 사용법에서는 React Router로 URL과 컴포넌트를 연결하는 방법을 배웁니다. 이때도 라우터가 React의 렌더링이나 서버의 보안·배포를 대신하지 않는다는 점을 기억해 두세요.