실제 커머스 프론트엔드의 성능 개선 과정에서 나온 질문을 바탕으로 썼다. 조직과 저장소를 식별할 수 있는 이름, 주소, 티켓 번호는 제외했고 수치는 맥락을 설명하는 범위에서 근삿값으로 표현했다.

느린 화면의 렌더링 전략을 검토하다 보면 자연스럽게 이런 질문을 만나게 된다.

SSR로 전환하면 초기 화면을 더 빠르게 보여줄 수 있지 않을까?

서버에서 HTML을 만들어 보내면 브라우저가 빈 화면에서 시작하지 않아도 된다. 네트워크 폭포를 서버 가까이에서 줄일 수도 있고, 검색 엔진과 링크 미리보기에도 유리하다. 분명 SSR로 빨라지는 화면이 있다.

하지만 SSR은 브라우저에서 하던 모든 일을 서버로 순간 이동시키는 스위치가 아니다. 어떤 데이터는 서버 요청 시점에 존재하지 않는다. 어떤 요청은 이미 이전 화면에서 시작하는 편이 빠르다. 서버가 HTML을 만들었더라도 브라우저는 자바스크립트를 실행하고 상태를 연결해야 한다. 앱 WebView에서는 그 과정에 브라우저와 네이티브 사이의 비용까지 들어올 수 있다.

따라서 질문은 “SSR이 빠른가?”가 아니라 다음과 같아야 한다.

이 화면의 임계 경로에서 서버가 실제로 선행할 수 있는 일은 무엇이고, 브라우저에서 다시 해야 하는 일은 무엇인가?

이 글은 그 경계를 판단하기 위한 학습 기록이다.

TL;DR

  • SSR은 서버가 요청 시점에 필요한 입력을 알고, 결과를 HTML에 반영할 수 있을 때 초기 표시를 앞당길 수 있다.
  • SSR의 “render”는 보통 서버가 화면 픽셀을 그린다는 뜻이 아니다. 서버는 컴포넌트와 데이터를 계산해 초기 HTML과 상태를 만들고, 실제 DOM 구성·레이아웃(layout)·페인트(paint)·합성(composite)은 브라우저가 수행한다.
  • localStorage 사용 여부만으로 SSR 가능성을 판단하면 부족하다. 메모리 상태, 사용자 행동, DOM 측정, WebView 브리지, 클라이언트 캐시처럼 서버가 알 수 없는 입력은 더 많다.
  • 이전 화면에서 다음 화면의 API를 이미 시작한다면 SSR은 요청 시간을 없애는 것이 아니라, 요청 시작점을 내비게이션 뒤로 늦출 수도 있다.
  • 개인화된 POST 응답은 공개 목록 데이터와 다르다. 캐시, 인증, 입력 전달, 오류 복구와 데이터 정합성까지 함께 설계해야 한다.
  • 느린 개인화 API를 SSR의 필수 경로에 넣으면 API 지연이 Next 서버의 동시 요청 수와 페이지 전체의 가용성 문제로 확대될 수 있다.
  • SSR 이후에도 하이드레이션(hydration), 이벤트 연결, 클라이언트 상태 복원, DOM 계산은 남는다.
  • 도입 전에는 “SSR 적용”이 아니라 현재 방식과 같은 사용자 경로에서 tap → usable content를 비교하는 실험이 필요하다.

“서버에서 그린다”는 말부터 나눠보자

SSR은 Server-Side Rendering의 약자라서 흔히 “서버에서 화면을 그린다”고 설명한다. 팀 안에서 빠르게 대화할 때는 편한 표현이다. 그러나 성능과 실행 경계를 논의할 때 이 표현을 그대로 사용하면 서로 다른 작업이 하나로 뭉친다.

일반적인 웹 SSR에서 서버가 사용자 화면의 픽셀을 그려 이미지로 내려주는 것은 아니다. 서버가 하는 일은 대체로 다음과 같다.

  1. 요청에서 URL, 헤더, 쿠키 같은 입력을 읽는다.
  2. 필요한 데이터를 조회하고 애플리케이션 코드를 실행한다.
  3. 컴포넌트 트리를 계산한다.
  4. 그 결과를 HTML 문자열 또는 스트림으로 직렬화한다.
  5. 클라이언트가 이어받을 상태와 필요한 CSS·JavaScript 참조를 응답에 포함한다.

그 응답을 실제 화면으로 만드는 주체는 여전히 브라우저다.

  1. HTML을 파싱해 DOM을 만든다.
  2. CSS를 파싱해 CSSOM을 만든다.
  3. DOM과 CSSOM으로 렌더 트리(render tree)를 구성한다.
  4. 요소의 크기와 위치를 계산하는 레이아웃(layout)을 수행한다.
  5. 픽셀을 만드는 페인트(paint)와 레이어를 합치는 합성(composite)을 수행한다.
  6. JavaScript를 실행하고 하이드레이션(hydration)으로 이벤트와 상태를 연결한다.

서버도 컴포넌트를 “렌더한다”고 부르고, 브라우저도 렌더링 파이프라인을 수행한다고 부른다. 같은 단어를 쓰지만 결과물이 다르다.

실행 주체 주로 계산하는 것 결과
서버 요청 데이터, 컴포넌트 트리, 초기 상태 HTML·직렬화된 데이터·리소스 참조
브라우저의 JavaScript 클라이언트 상태, 이벤트, 후속 UI DOM 변경
브라우저 렌더링 엔진 스타일, 크기, 위치, 레이어 사용자가 보는 픽셀

물론 서버가 이미지 자체를 생성하는 서비스나 원격 화면 스트리밍도 있다. 하지만 일반적인 React·Next.js SSR을 말할 때는 그 경우가 아니다.

그러므로 “서버에서 그리니 빠르다”는 문장은 인과관계를 너무 많이 생략한다. 좀 더 정확한 설명은 다음에 가깝다.

서버가 브라우저보다 먼저 계산할 수 있는 초기 UI를 HTML로 만들어 보내면, 브라우저가 자바스크립트로 같은 DOM을 처음부터 구성하기 전에 콘텐츠를 표시할 기회를 얻는다.

여기서 핵심은 서버가 먼저 계산할 수 있는가다.

CSR과 SSR은 픽셀을 누가 그리느냐의 차이가 아니다

단순화한 CSR은 다음과 같다.

서버
  └─ 비어 있거나 최소한의 HTML + JavaScript 참조

브라우저
  ├─ HTML 파싱
  ├─ JavaScript 다운로드·실행
  ├─ 데이터 조회와 컴포넌트 계산
  ├─ DOM 생성
  └─ style → layout → paint → composite

SSR은 일부 애플리케이션 연산과 초기 DOM의 재료를 서버에서 먼저 수행한다.

서버
  ├─ 데이터 조회와 컴포넌트 계산
  └─ 내용이 들어 있는 HTML + 상태 + 리소스 참조

브라우저
  ├─ HTML 파싱과 DOM 생성
  ├─ style → layout → paint → composite
  ├─ JavaScript 다운로드·실행
  └─ hydration + 클라이언트 전용 디테일 반영

두 방식 모두 최종 픽셀은 브라우저가 만든다. 차이는 초기 DOM을 만들기 위한 애플리케이션 연산을 어디서, 언제 수행했는가에 있다.

SSR에서도 브라우저가 할 일은 남는다. CSS가 없으면 원하는 모양으로 그릴 수 없고, 레이아웃과 페인트를 피할 수도 없다. 이벤트가 필요한 화면은 JavaScript와 하이드레이션을 기다려야 한다. 브라우저에서만 알 수 있는 값은 하이드레이션 이후 다시 계산하고 화면을 보정해야 한다.

반대로 CSR도 언제나 완전히 빈 HTML만 받는 것은 아니다. 정적 셸이나 로딩 UI가 들어 있을 수 있고, 브라우저 캐시에 코드와 데이터가 이미 준비돼 있을 수도 있다. 그래서 SSR과 CSR이라는 이름만 보고 실제 시작점을 추측하지 말고 응답 HTML, 네트워크, 실행 순서를 확인해야 한다.

서버가 할 수 있는 연산과 할 수 없는 연산

SSR 후보를 볼 때는 컴포넌트 파일을 서버로 옮길 수 있는지만 보면 안 된다. 그 컴포넌트가 결과를 계산하는 데 필요한 입력이 서버에 있는지를 봐야 한다.

서버 요청 시점에 존재하는 입력
  → 서버가 계산 가능
  → 초기 HTML에 반영 가능

브라우저 실행 뒤에 생기는 입력
  → 서버가 그대로 계산할 수 없음
  → 기본값·별도 전달 계약·클라이언트 후속 계산 필요

예를 들어 URL의 상품 번호와 공개 API만 필요한 제목은 서버가 계산하기 쉽다. 반면 실제 DOM 너비에 따라 썸네일 개수를 정하거나, 앱이 주입한 네이티브 브리지로 현재 환경을 확인하거나, 직전 화면의 메모리 상태로 혜택을 계산하는 일은 서버가 같은 방식으로 수행할 수 없다.

이때 “클라이언트 전용 코드를 조건문으로 감싸면 SSR이 된다”는 말도 절반만 맞다. 서버 오류를 피할 수는 있지만, 그 연산 결과가 초기 HTML에 들어가는 것은 아니다. 브라우저가 실행된 뒤 다시 계산하고 DOM을 바꾼다면 해당 부분의 사용자 경험은 여전히 클라이언트 작업에 달려 있다.

SSR로 빨라지는 전형적인 화면

SSR이 잘 맞는 조건부터 분명히 하자.

서버가 초기 화면의 입력을 모두 알고 있다

URL, 경로 매개변수, 검색 조건, 요청 쿠키와 헤더만으로 첫 화면을 계산할 수 있다면 서버가 브라우저보다 먼저 일을 시작할 수 있다.

예를 들어 공개 상품 상세의 이름, 가격, 대표 이미지나 공개 검색 결과는 서버가 요청을 받는 순간 조회할 수 있다. 브라우저가 자바스크립트를 내려받고 API를 다시 호출할 때까지 기다리지 않고, 데이터가 들어간 HTML을 보낼 수 있다.

문서 요청
  ├─ 서버에서 공개 데이터 조회
  ├─ HTML 생성
  └─ 콘텐츠가 포함된 응답
       └─ 브라우저 hydration

특히 서버와 API가 가까워 브라우저의 추가 왕복보다 빠르거나, 여러 요청을 서버에서 병렬화할 수 있다면 이점이 커진다.

첫 표시가 상호작용보다 중요하다

콘텐츠 문서, 공개 상세, 랜딩 페이지처럼 사용자가 먼저 읽는 화면도 잘 맞는다. 서버가 의미 있는 HTML을 보내면 자바스크립트 준비 전에도 텍스트와 이미지의 자리를 보여줄 수 있다.

다만 “HTML이 보인다”와 “버튼이 동작한다”는 같은 시점이 아니다. 상호작용이 핵심인 화면이라면 hydration 비용도 같이 측정해야 한다.

개인화가 적고 캐시 전략이 분명하다

사용자마다 응답이 달라지지 않는 공개 데이터는 CDN이나 서버 캐시와 결합하기 쉽다. 요청마다 사용자 세션을 확인하고 완전히 다른 HTML을 만드는 화면보다 SSR 비용을 통제하기도 쉽다.

정리하면 SSR이 잘 맞는 질문은 이렇다.

서버가 지금 가진 정보만으로
사용자에게 의미 있는 첫 화면을
브라우저보다 먼저 완성할 수 있는가?

SSR로 바꿔도 자동으로 빨라지지 않는 화면

반대쪽에는 “CSR이라서 느리다”는 설명만으로는 부족한 화면이 있다.

이전 화면이 이미 다음 요청을 시작하고 있다

느린 API를 다음 화면이 열린 뒤 호출하던 구조라면 서버 측 데이터 조회가 이득일 수 있다. 하지만 이전 화면에서 요청을 미리 시작하도록 이미 최적화했다면 비교가 달라진다.

현재 방식

결제하기 선택
  ├─ 다음 화면의 데이터 요청 시작 ─────────┐
  └─ 라우트 전환과 코드 준비 ───────────────┤
                                            └─ 캐시를 이어받아 렌더

이 요청을 Pages Router의 getServerSideProps로 옮기면 다음처럼 될 수 있다.

SSR로 단순 이동

결제하기 선택
  └─ 라우트 요청
       └─ 서버의 데이터 요청
            └─ 응답
                 └─ hydration과 상태 복원

API의 500ms가 사라진 것이 아니다. 앞 화면의 라우트 전환과 겹치던 요청이 서버 내비게이션 뒤로 이동했을 뿐이다. 직접 진입에는 이득이 있어도, 실제 사용자의 주 경로인 소프트 내비게이션에서는 더 느릴 가능성도 있다.

그래서 SSR과 prefetch는 기술 이름으로 비교하면 안 된다. 사용자가 행동한 시각부터 요청이 언제 출발하는지를 비교해야 한다.

느린 API를 SSR에 넣으면 실패 범위도 서버로 이동한다

여기에는 사용자 한 명의 대기 시간 외에 서버의 동시성 문제도 있다.

Next 서버가 하위 API를 기다리는 동안 CPU를 계속 사용하는 것은 아니다. 그렇다고 비용이 없는 것도 아니다. 응답을 마치지 못한 요청의 컨텍스트와 렌더 상태가 메모리에 남고, 소켓과 연결 풀을 사용하며, 동시에 진행 중인 SSR 요청 수가 늘어난다.

안정된 상태에서 평균 동시 진행 요청 수는 대략 다음 관계로 생각할 수 있다.

평균 동시 진행 요청 수 ≈ 초당 요청 수 × 평균 처리 시간

예를 들어 SSR 요청이 초당 100개 들어오고 하위 API를 포함한 평균 처리 시간이 500ms라면 평균적으로 약 50개의 요청이 진행 중이다. 이 숫자는 CPU 사용량과 같지 않으며 곧바로 장애를 뜻하지도 않는다. 다만 하위 API의 지연이 길어지면 완료되지 않은 SSR 요청이 더 오래 남고, 필요한 메모리·소켓·연결 풀의 여유도 함께 줄어든다는 뜻이다.

특히 주문 데이터처럼 사용자마다 payload가 다른 요청은 공개 데이터와 달리 공유 캐시나 요청 병합으로 흡수하기 어렵다. 사용자 요청마다 하위 API 호출이 발생한다면 그 API의 지연 분포가 Next 서버 응답의 지연 분포로 전달된다.

하위 API 지연
  → SSR 응답 지연
  → 진행 중인 Next 요청 증가
  → 메모리·소켓·연결 풀 압박
  → 타임아웃과 재시도
  → 하위 API 부하 증가

실패 범위도 달라진다. CSR에서는 문서와 화면 셸을 먼저 전달하고, 데이터 요청의 실패를 로딩·재시도·부분 오류로 다룰 수 있다. 같은 API를 SSR의 필수 데이터로 묶으면 API 지연이나 장애 때문에 HTML 응답 자체를 완성하지 못할 수 있다. 데이터 한 영역의 실패가 페이지 진입 전체의 실패로 확대되는 것이다.

항상 CSR의 실패 격리가 더 낫다는 뜻은 아니다. 스트리밍, 시간 제한, fallback, circuit breaker, 별도 캐시로 서버 경로를 방어할 수 있다. 중요한 것은 SSR 전환이 실행 위치뿐 아니라 실패 격리 경계까지 바꾼다는 사실을 설계에 포함하는 것이다.

따라서 느린 개인화 API를 SSR로 옮기려면 화면의 전후 시간과 함께 다음 항목도 확인해야 한다.

  • 예상 RPS와 평균 동시 진행 SSR 요청 수
  • 하위 API의 p75·p95·p99와 타임아웃
  • 인스턴스당 메모리·연결 풀·동시 처리 한도
  • 재시도 횟수와 하위 API 부하 증폭 가능성
  • 하위 API 실패 시 fallback과 부분 응답 정책
  • SSR 경로가 포화될 때의 부하 차단과 복구 방법

SSR로 브라우저의 대기를 서버로 옮겼다면, 사용자 화면이 빨라졌는지만 볼 수 없다. 그 대기를 서버가 운영 트래픽에서 안전하게 감당할 수 있는지도 함께 증명해야 한다.

초기 데이터가 사용자 행동으로 완성된다

체크아웃 같은 화면의 입력은 URL 하나로 표현되지 않을 수 있다.

  • 앞 화면에서 선택한 상품과 옵션
  • 적용하거나 해제한 쿠폰
  • 배송지와 우편번호
  • 마일리지와 결제 수단
  • 선물 여부
  • 직전에 계산한 최적 혜택
  • 실험군과 기능 플래그

이 중 일부가 브라우저 메모리의 전달(handoff) 객체에만 있다면 서버는 다음 문서 요청에서 그 상태를 모른다. SSR을 하려면 먼저 별도의 전달 계약이 필요하다.

가능한 방법은 있다. URL에 넣거나, 서버 세션에 저장하거나, 요청 본문 또는 쿠키로 전달할 수 있다. 그러나 그것은 “SSR을 켠다”가 아니라 새로운 데이터 계약을 설계하는 작업이다.

그때는 다음 문제까지 생긴다.

  • 입력이 변조되지 않았는가?
  • 서버 세션과 브라우저 상태 중 무엇이 기준인가?
  • 오래된 전달 상태를 재사용하지 않는가?
  • 뒤로 가기와 재진입에서는 어떤 값을 쓰는가?
  • prefetch와 SSR이 같은 요청을 중복 실행하지 않는가?
  • 실패하면 어느 경로로 복구하는가?

브라우저에 있는 값을 서버도 알게 만들 수 있다는 것과, 그렇게 만드는 편이 더 빠르고 단순하다는 것은 다른 주장이다.

서버가 HTML을 만든 뒤에도 큰 클라이언트 작업이 남는다

SSR로 HTML을 만들었다고 페이지 구성이 끝나는 것은 아니다. React는 서버가 만든 HTML과 클라이언트 트리를 연결하고, 이벤트 핸들러와 상태를 준비한다.

서버 HTML 도착
  → 자바스크립트 다운로드·파싱·실행
  → hydration
  → 클라이언트 저장소와 캐시 복원
  → 사용자·실험·환경 상태 확인
  → DOM 측정과 후속 렌더
  → 상호작용 가능

서버가 마크업을 만들 수 있어도 다음 작업은 브라우저에 남는다.

  • window, document, History API에 의존하는 로직
  • 실제 viewport와 DOM 크기 측정
  • ResizeObserver, IntersectionObserver, 미디어 쿼리 결과
  • 포커스, 스크롤 위치와 입력 중인 값
  • TanStack Query 같은 인메모리 캐시
  • Zustand 등 클라이언트 상태 저장소
  • 앱 WebView가 주입한 객체와 네이티브 브리지
  • 브라우저 쿠키 접근과 클라이언트 인증 갱신
  • 사용자의 직전 행동에서 만들어진 임시 상태

이것들은 localStorage보다 넓은 범주의 문제다. 공통점은 서버 요청 시점에 동일한 실행 환경과 상태가 존재하지 않는다는 것이다.

localStorage만 찾으면 경계를 놓친다

SSR 검토에서 자주 나오는 확인은 “이 페이지가 localStorage를 쓰나요?”다. 필요한 질문이지만 충분하지 않다.

localStorage는 서버에 없기 때문에 접근 코드를 렌더 뒤로 옮기거나 값을 다른 저장소로 이전해야 한다. 그런데 코드를 고칠 수 있다는 이유만으로 SSR의 성능 이득이 증명되지는 않는다.

예를 들어 다음 코드가 있다고 하자.

const checkoutState = checkoutHandoff.peek();
const cachedOrderForm = queryClient.getQueryData(orderFormQueryKey(checkoutState));

여기에는 localStorage가 없다. 그래도 서버는 두 값을 모른다.

  • checkoutHandoff는 이전 화면의 사용자 행동으로 생성된 프로세스 메모리다.
  • queryClient에는 이전 화면에서 미리 시작한 요청과 그 결과가 있다.

SSR로 전환하려면 이를 제거하는 것에서 끝나지 않는다. 현재 병렬 실행의 이점을 유지하면서 서버가 같은 입력을 받아 같은 결과를 만들고, 클라이언트가 그 결과를 중복 요청 없이 이어받도록 설계해야 한다.

또 다른 예를 보자.

const width = element.getBoundingClientRect().width;
const isInsideApp = Boolean(window.NativeBridge);
const cookies = document.cookie;

세 값 모두 브라우저에서만 정확히 알 수 있다.

  • DOM 크기는 실제 CSS, viewport, font와 레이아웃이 적용된 뒤 결정된다.
  • 네이티브 브리지는 앱이 WebView에 주입한다.
  • document.cookie는 서버가 받은 Cookie 헤더와 비슷해 보여도 접근 시점, 쓰기 직후 상태, JavaScript 접근 가능 범위가 다를 수 있다.

서버용 대체 값을 만들 수는 있다. 하지만 대체 값이 hydration 뒤 클라이언트 결과와 다르면 화면이 바뀌거나 추가 렌더가 생긴다. SSR 가능 여부보다 서버와 클라이언트가 같은 첫 결과를 만들 수 있는지를 먼저 봐야 한다.

실기기에서 발견한 것은 서버 대기가 아니었다

체크아웃 성능을 조사할 때 처음에는 느린 API를 가장 큰 원인으로 봤다. 요청 시점을 앞당기는 prefetch도 실제로 효과가 있었다. 이후에는 데스크톱 CPU 제한 환경에서 큰 reflow를 발견해 약 100ms 규모의 개선을 확인했다.

그런데 실제 Android WebView 프로파일은 다시 다른 병목을 보여줬다. 긴 렌더 작업 안에서 레이아웃보다 반복적인 document.cookie 읽기가 훨씬 큰 비중을 차지했다.

겉으로는 문자열 프로퍼티 하나를 읽는 코드다.

const value = document.cookie;

하지만 document.cookie는 동기 API다. 특정 Android WebView에서는 브라우저 프로세스와 네이티브 쿠키 저장소 사이의 작업이 메인 스레드를 오래 막을 수 있었다. 여러 호환 계층과 상태 훅이 같은 렌더 작업 안에서 이 값을 반복해서 읽자 누적 비용이 커졌다.

SSR이 서버에서 초기 HTML을 만들더라도 이 비용이 자동으로 없어지는 것은 아니다. hydration 과정에서 클라이언트 코드가 동일한 쿠키 서비스와 사용자 상태를 다시 읽으면 병목은 남는다.

이 사례가 보여준 것은 “SSR이 나쁘다”가 아니다.

서버 응답을 최적화하는 기술로 브라우저 메인 스레드의 병목까지 해결했다고 가정하면 안 된다.

성능 문제는 실행 층을 맞춰서 봐야 한다.

느린 구간 먼저 검토할 방향
문서 TTFB 서버 처리, 캐시, CDN, SSR 데이터 조회
API 왕복 API 처리, payload, BFF, prefetch, 병렬화
JS 다운로드·파싱 번들 축소, route chunk, code splitting
하이드레이션 클라이언트 경계 축소, 초기 상태와 effect 점검
레이아웃·페인트 초기 DOM 양, CSS, geometry read
WebView 메인 스레드 동기 브라우저 API, 네이티브 브리지, 실기기 프로파일

SSR은 이 표의 일부 구간에 강한 도구다. 모든 행을 한 번에 지우는 도구는 아니다.

SSR 후보를 나누는 질문

다음 질문으로 화면을 분류하면 “SSR로 바꾸자”라는 추상적인 논의를 조금 더 구체화할 수 있다.

1. 실제 사용자의 주 진입 경로는 무엇인가?

  • 주소 직접 진입 또는 새로고침인가?
  • 앱 내부의 소프트 내비게이션인가?
  • 이전 화면에서 다음 요청을 미리 시작할 수 있는가?

직접 진입과 소프트 내비게이션의 최적 전략은 다를 수 있다. 한 화면에 반드시 하나의 렌더링 전략만 있어야 하는 것도 아니다.

2. 서버가 초기 렌더 입력을 모두 알고 있는가?

  • URL, 헤더, 요청 쿠키만으로 충분한가?
  • 이전 화면의 메모리 상태가 필요한가?
  • 사용자 행동이나 WebView 브리지의 결과가 필요한가?
  • 클라이언트와 서버가 동일한 기본값을 만들 수 있는가?

입력이 없다면 SSR 불가라고 끝낼 것이 아니라, 입력 전달 계약을 새로 만드는 비용과 이득을 함께 적어야 한다.

3. 데이터는 공개적인가, 개인화됐는가?

  • CDN 또는 공유 캐시가 가능한가?
  • 요청마다 인증과 사용자별 계산이 필요한가?
  • 조회라고 해도 POST이며 입력 조합이 많은가?
  • 서버가 대신 호출할 때 인증과 추적 정보가 보존되는가?

공개 상품 데이터와 체크아웃의 주문 가능 상태는 같은 “API 데이터”가 아니다.

4. SSR이 제거하는 실제 구간은 무엇인가?

아래 문장을 수치와 함께 완성할 수 있어야 한다.

현재 방식에서는 ______ 때문에 ____ms를 기다린다.
SSR에서는 ______를 서버에서 먼저 수행해 그중 ____ms를 줄일 것으로 예상한다.
대신 TTFB·payload·hydration에는 각각 ______의 비용이 추가된다.

이 문장을 쓸 수 없다면 아직 해결책이 아니라 가설 단계다.

5. HTML 표시와 상호작용 가능 시점을 모두 측정했는가?

서버 HTML이 빨리 보이더라도 결제 버튼이 오랫동안 동작하지 않는다면 사용자가 느끼는 개선은 제한적이다. 다음 지표를 나눠 봐야 한다.

  • TTFB
  • 첫 의미 있는 콘텐츠 표시
  • tap → content
  • hydration long task
  • 상호작용 가능 시점
  • API 호출 횟수와 중복 여부
  • 서버 p75·p95
  • 기기와 WebView별 p75·p95

평균 하나와 데스크톱 한 번의 측정으로는 특정 Android WebView의 꼬리 지연을 설명할 수 없다.

체크아웃이라면 이렇게 실험한다

논쟁으로 결정하지 않고 세 가지 구현을 같은 조건에서 비교할 수 있다.

A. 현재 소프트 내비게이션 + prefetch

기준선이다. 이전 화면에서 개인화 API를 시작하고 동일한 query key와 QueryClient로 이어받는다.

B. 서버 prefetch + dehydrated state

서버가 필요한 입력을 받을 수 있다는 전제에서 API를 호출하고, 결과를 클라이언트 캐시의 초기 상태로 전달한다. 중복 요청과 직렬화 비용을 확인한다.

C. ATF를 포함한 SSR

서버가 첫 화면의 주요 콘텐츠까지 만든다. HTML 표시뿐 아니라 hydration 이후 사용자 상태가 정확한지 확인한다.

그리고 최소한 다음을 같은 실제 경로에서 비교한다.

장바구니에서 결제하기 선택
  → 헤더 표시
  → 주문 정보 표시
  → 결제 CTA 상호작용 가능

직접 진입도 별도 시나리오로 측정한다. B나 C가 직접 진입에서는 빠르고 소프트 내비게이션에서는 느릴 수 있기 때문이다. 그런 결과라면 실패가 아니라 경로별 전략을 나눌 근거가 된다.

결론: 렌더링 전략보다 먼저 실행 경로를 그린다

SSR은 좋은 도구다. 서버가 아는 공개 데이터를 먼저 가져오고, 의미 있는 HTML을 보내며, 브라우저의 추가 왕복을 줄이는 데 강하다.

문제는 “느린 CSR 화면”이라는 이유만으로 SSR을 처방할 때 생긴다.

클라이언트에서만 가능한 연산은 localStorage 접근보다 훨씬 많다. 이전 화면의 메모리와 캐시, 사용자의 직전 선택, 실제 DOM과 viewport, WebView의 네이티브 환경, 동기 브라우저 API, hydration 뒤 연결되는 상태가 모두 서버와 클라이언트의 경계를 만든다.

이 경계를 무시하고 SSR로 옮기면 일이 사라지지 않는다. 다른 위치로 이동하거나, 서버와 클라이언트에서 두 번 실행되거나, 새로운 상태 전달 계약이 된다.

그래서 다음부터 “SSR이면 빠르지 않을까?”라는 제안을 받으면 반대부터 하지 않으려고 한다. 대신 실행 경로를 함께 그릴 것이다.

  1. 사용자는 어디에서 이 화면으로 들어오는가?
  2. 가장 느린 구간은 서버, 네트워크, JS, hydration, layout 중 어디인가?
  3. 서버는 그 구간에 필요한 입력을 언제 알 수 있는가?
  4. 현재 병렬인 일을 SSR이 직렬로 만들지는 않는가?
  5. 브라우저에서 결국 다시 해야 하는 일은 무엇인가?
  6. 같은 실제 경로의 측정으로 얼마나 빨라지는가?

SSR을 채택하는 결론도, 채택하지 않는 결론도 이 질문 뒤에 나와야 한다.

렌더링 전략은 믿음으로 고르는 기술 이름이 아니라, 실제 임계 경로에서 어느 실행 주체가 일을 가장 먼저 끝낼 수 있는지에 대한 설계다.

함께 읽기