요즘 다시 SSR이 뜨고 있다
Next.js App Router, Remix, SvelteKit, Nuxt, 새로 등장하는 프레임워크들은 다 SSR을 기본으로 깔고 있다. 몇 년 전까지만 해도 "SPA가 미래다"라고 외치던 사람들이 이제 "RSC가 답이다"라고 한다.
이걸 보고 누군가는 농담처럼 말한다. "결국 PHP 시대로 돌아간 거 아니냐?" 그리고 그 농담은 꽤 자주 들린다. 일리가 있어 보이기도 한다. 서버에서 HTML을 만들어 보낸다는 점에서, 표면적으로는 분명 닮았다.
하지만 농담은 농담으로 두고, 잠깐만 1세대 SSR을 떠올려보면 어떨까.
1세대 SSR, 잊혀진 풍경
PHP나 JSP로 웹을 만들던 시절을 기억해보자. 페이지를 이동하면 전체 새로고침이 일어났다. 폼을 제출하면 페이지가 다시 로드되고, 그 사이에 사용자는 흰 화면을 잠깐 봐야 했다. 인터랙션이라고 해봐야 onclick="alert(...)" 정도였고, 동적인 UI를 원하면 jQuery로 DOM을 직접 조작해야 했다.
서버는 매 요청마다 템플릿에 데이터를 끼워 넣어 HTML을 통째로 그렸다. 컴포넌트라는 개념은 없었고, 있는 건 인클루드와 템플릿이었다. 데이터 흐름이라고 부를 만한 것도 없었다. 폼 → POST → 리다이렉트 → 새 페이지가 전부였다.
그래서 우리는 떠났다.
CSR이라는 탈출
SPA가 등장했을 때, 그건 해방처럼 느껴졌다. 페이지를 새로고침하지 않고 상태를 바꿀 수 있다니. 클릭 한 번에 부드러운 전환이 일어나고, 폼을 제출해도 사용자의 입력 위치가 그대로 유지되고, 로딩 인디케이터를 자유롭게 보여줄 수 있다니.
React, Vue, Angular가 차례로 등장했고, 우리는 모든 걸 클라이언트로 옮겼다. 서버는 그저 API만 제공하면 됐다. 백엔드와 프론트엔드가 깔끔하게 분리되었고, 프론트엔드 개발자는 자기만의 세계를 갖게 되었다.
몇 년간 잘 작동했다. 하지만 어느 순간부터 한계가 보이기 시작했다.
CSR의 한계, 우리가 외면해온 것들
SEO가 약했다. 검색 엔진이 JavaScript를 실행해서 렌더링한 결과를 인덱싱하는 데는 한계가 있었다. Google은 어느 정도 따라왔지만, 다른 검색 엔진과 SNS 크롤러는 여전히 JavaScript를 제대로 다루지 못했다. 블로그나 커머스 사이트에서 SEO는 생존의 문제였다.
첫 paint가 느렸다. 사용자가 URL을 입력하면 빈 HTML이 먼저 오고, 그 다음 JavaScript 번들이 다운로드되고, 파싱되고, 실행되어야 비로소 콘텐츠가 보였다. 모바일 환경에서는 이 시간이 몇 초씩 걸렸다.
Core Web Vitals가 나빴다. Google이 LCP, CLS, INP 같은 지표를 도입하면서 클라이언트 렌더링의 약점이 점수로 드러났다. 큰 번들의 다운로드와 파싱, JS 실행 전까지 비어있는 화면, 동적으로 채워지면서 발생하는 레이아웃 이동, 모두 SPA가 약한 영역이었다.
번들이 점점 비대해졌다. 기능을 추가할수록 클라이언트 코드가 늘어났고, 사용자는 자신이 쓰지도 않을 코드까지 다운로드해야 했다.
이 한계들이 모이자, 사람들은 다시 서버를 바라보기 시작했다.
그런데 왜 지금이야?
흥미로운 건 이거다. CSR의 한계는 사실 처음부터 알려져 있었다. SEO 문제는 SPA 등장 직후부터 논의되었고, 첫 paint 지연도 마찬가지였다. 그런데 왜 이 문제들이 몇 년 동안 그냥 감수되다가, 지금 와서 다시 SSR로 회귀하는 흐름이 생긴 걸까?
답은 인프라의 성숙에 있다.
엣지 컴퓨팅 이 보편화되면서 서버 응답 지연이라는 1세대 SSR의 가장 큰 약점이 해결되었다. Cloudflare Workers, Vercel Edge, Deno Deploy 같은 플랫폼은 사용자에게 가장 가까운 엣지에서 코드를 실행한다. 옛날 SSR은 서울 사용자가 미국 데이터센터의 응답을 기다려야 했지만, 지금은 사용자 근처에서 즉시 응답이 만들어진다.
Streaming SSR 이 등장하면서 "전체 페이지가 준비될 때까지 기다린다"는 제약도 사라졌다. 서버는 빠른 부분을 먼저 보내고, 느린 부분은 준비되는 대로 추가로 흘려보낸다. 사용자는 빈 화면이 아니라 점진적으로 채워지는 화면을 본다.
React Server Components, Server Actions 같은 새로운 모델은 서버와 클라이언트의 경계를 더 자연스럽게 만들었다. 어떤 컴포넌트는 서버에서, 어떤 컴포넌트는 클라이언트에서, 한 트리 안에 섞여 있어도 자연스럽게 동작한다.
이 모든 게 갖춰진 후에야 SSR은 다시 매력적인 선택지가 되었다.
1세대 vs 3세대, 같은 단어, 다른 세계
이제 본질적인 차이를 정리해보자.
렌더링 단위의 차이. 1세대 SSR은 페이지 단위였다. 한 페이지가 통째로 그려지고, 다음 페이지로 넘어갈 때도 통째로 다시 그려졌다. 3세대 SSR은 컴포넌트 단위다. 같은 페이지 안에서도 어떤 부분은 서버에서, 어떤 부분은 클라이언트에서 그려진다. 그리고 한 번 그려진 후에는 SPA처럼 부드럽게 전환된다.
인터랙션 모델의 차이. 1세대는 폼과 리다이렉트가 전부였다. 사용자가 뭔가를 하면 페이지가 다시 로드됐다. 3세대는 hydration 이후에 SPA처럼 동작한다. 클라이언트 상태를 유지하면서 부분 업데이트가 일어난다. 폼 제출도 페이지 새로고침 없이 처리된다.
컴포넌트 모델의 차이. 1세대에는 컴포넌트가 없었다. 있는 건 템플릿과 인클루드뿐. 3세대는 React, Vue, Svelte 같은 컴포넌트 모델 위에서 작동한다. 재사용 가능한 UI 단위가 있고, 상태와 props로 데이터가 흐른다.
데이터 흐름의 차이. 1세대는 폼 POST → 서버 처리 → 리다이렉트 → 새 페이지가 전부였다. 3세대는 Server Actions로 서버 함수를 클라이언트 컴포넌트가 직접 호출한다. API 라우트를 따로 만들지 않아도 된다. 데이터는 props로 컴포넌트에 흐른다.
표면적으로는 둘 다 "서버에서 HTML을 만든다"이지만, 그 안에서 일어나는 일은 완전히 다르다. 같은 단어, 다른 세계다.
나선형 진보
이 흐름을 보면 떠오르는 이미지가 있다. 평면 위에서 보면 같은 자리로 돌아온 것처럼 보이지만, 위에서 보면 한 단계 위에서 돌아온 것이다. 나선이다.
PHP의 SSR은 원시적이었다. 우리는 그 한계를 느끼고 CSR로 떠났다. CSR은 자유로웠지만 새로운 한계를 만났다. 이제 우리는 다시 서버로 돌아왔지만, 떠나기 전과는 완전히 다른 도구를 손에 쥐고 있다. 컴포넌트, 엣지, Streaming, RSC, 1세대 SSR이 꿈꿀 수 없었던 것들이다.
회귀가 아니라 진보다. 다만 직선으로 진보한 게 아니라, 한 바퀴 돌면서 진보했을 뿐이다.
그리고 이 또한 영원하지 않다
마지막으로 한 가지를 덧붙이고 싶다. 지금 SSR이 다시 주목받는다고 해서 SSR이 영원한 정답이라는 뜻은 아니다.
CSR이든, ISR이든, SSR이든, RSC든, 정답은 없다. 각 시대마다 사용 가능한 인프라와 도구가 다르고, 그에 맞는 최적의 답이 다를 뿐이다. 엣지 컴퓨팅이 없던 시절에 SSR을 고집했다면 오답이었을 것이고, 엣지가 보편화된 지금 무조건 CSR을 고집하는 것도 오답일 수 있다.
생태계는 계속 변한다. 5년 후에는 또 다른 인프라가 등장하고, 또 다른 패러다임이 자리잡을 것이다. 그때 우리는 또 한 바퀴를 돌게 될지도 모른다. 표면적으로는 같은 자리로 돌아온 것처럼 보이지만, 분명 한 단계 위에서 돌아오고 있을 것이다.
기술 선택의 본질은 이거다. 지금 이 순간의 생태계에 맞는 답을 골라낼 수 있는 시각. 그리고 그 답이 영원하지 않다는 걸 받아들이는 유연함.