- Published on
2025.04.05
[리액트 딥다이브] - 04. 서버 사이드 렌더링(1)
4.1. 서버 사이드 렌더링이란?
4.1.1. 싱글 페이지 애플리케이션의 세상
렌더링과 라우팅에 필요한 대부분의 기능을 브라우저의 자바스크립트에 의존하는 방식
첫 페이지를 불러온 이후엔 서버에서 HTML을 받지 않고 하나의 페이지에서 모든 작업을 처리해 싱글 페이지 애플리케이션이라고 함 최초 로딩 시의 자바스크립트 리소스가 커지는 단점이 있지만, 한번 로딩된 이후엔 서버를 거쳐 리소스를 받아올 일이 적어 훌륭한 UI/UX를 제공하는 장점이 있음
싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장
과거 PHP나 JSP 기반의 웹 애플리케이션은 대부분의 렌더링이 서버 사이드에서 이뤄짐. 자바스크립트는 보조 수단으로 사용되었고, 이후 자바스크립트가 다양한 작업을 수행하게 되며 CommonJS와 AMD가 등장함. 이후 2010년경 Backbone.js, AngularJS, Knockout.js 등이 등장하고 프레임워크가 인기를 끌며 React, Vue, Angular의 시대가 오게 됨.
싱글 페이지 애플리케이션은 프론트엔드 개발자들에게 간편한 DX를 제공하고 편하게 웹 애플리케이션을 만들 수 있다는 장점 덕분에 싱글 페이지 애플리케이션이 개발되기 시작. 기존의 웹 개발은 LAMP 스택(Linux(운영체제), Apache(서버), MySQL(데이터베이스), PHP/Python(웹 프레임워크) 등)으로 구성되어 있었으나, 프레임워크의 등장으로 JAM 스택(JavaScript, API, Markup)이 인기를 끌기 시작함.
새로운 패러다임의 웹서비스를 향한 요구
자바스크립트에서 처리해야 하는 코드의 절대적인 양의 증가로 자바스크립트 파싱을 위해 CPU 소비 시간이 눈에 띄게 증가함. 인터넷 속도 등 웹 환경이 개선됐음에도 웹 애플리케이션의 로딩 속도는 오히려 느려지는 문제가 발생.
4.1.2. 서버 사이드 렌더링이란?
서버 사이드 렌더링의 장점
최초 페이지 진입이 비교적 빠르다
서버에서 HTTP 요청을 수행하고, HTML을 문자열로 그려 내려주는 것이 클라이언트에서 HTML에 삽입하는 것보다 빠름
검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다
메타 정보 또한 SPA와 같이 자바스크립트에 의존하는데, 로봇은 페이지의 정적인 정보를 가져오므로 자바스크립트를 다운로드하거나 실행하지 않음. SSR은 최초의 렌더링 작업이 서버에서 일어나므로 검색 엔진 최적화에 유리
누적 레이아웃 이동이 적다
페이지를 보여준 후, HTML 정보가 추가되거나 삭제되어 화면이 덜컥거리는 부정적인 UX를 방지할 수 있다. SSR은 요청이 완료된 이후에 완성된 페이지를 제공하기 때문
사용자의 디바이스 성능에 비교적 자유롭다
자바스크립트 리소스 실행은 사용자 디바이스 성능에 의존적인데, SSR은 부담을 서버에 나눌 수 있으므로 디바이스 성능으로부터 자유롭다.
보안에 좀 더 안전한다
API 호출과 인증 같은 민감한 작업도 브라우저에 노출되는데, SSR은 작업을 서버에서 수행하고 결과만 브라우저에 제공하므로 보안 위협을 피할 수 있다.
단점
소스코드를 작성할 때 항상 서버를 고려해야 한다
브라우저 전역 객체인
window
또는sessionStorage
와 같은 브라우저의 전역 객체 등에 대한 접근을 최소화하고, 서버 사이드에서 실행되지 않도록 처리해야 한다.적절한 서버가 구축돼 있어야 한다
SSR은 사용자의 요청을 받아 렌더링을 수행할 서버가 필요함.
서비스 지연에 따른 문제
SSR에서 지연이 일어나면 렌더링 작업이 끝날때까지 어떤 정보도 제공할 수 없음. 다양한 요청이 얽혀 병목 현상이 심해지면 더 안 좋은 UX를 제공할 수도 있음
4.1.3. SPA와 SSR을 모두 알아야 하는 이유
서버 사이드 렌더링 역시 만능이 아니다
잘못된 웹페이지 설계는 서버와 클라이언트 두 군데로 관리 포인트만 늘어나는 역효과가 될 수 있음. 웹페이지의 설계와 목적, 우선순위에 따라 결정해야 함.
싱글 페이지 애플리케이션과 서버 사이드 렌더링 애플리케이션
가장 뛰어난 SPA는 가장 뛰어난 MPA보다 낫다.
Gmail과 같은 완성도가 뛰어난 SPA를 보면, 최초 페이지 진입 시 필요한 정보만 최적화해 요청 및 렌더링, 이미지 같은 중요성이 떨어지는 리소스는 레이지 로딩 적용, 코드 스플리팅을 통해 불필요한 자바스크립트 리소스의 다운 및 실행을 방지, 라우팅 발생 시 필요한 HTML 영역만 교체 등 SPA의 매끄러운 라우팅으로 뛰어난 성능을 보여줌.
평균적인 SPA는 평균적인 MPA보다 느리다.
MPA는 매번 서버에 렌더링 요청, 서버는 매 요청마다 비슷한 성능의 렌더링 수행.
SPA는 렌더링과 라우팅이 최적화가 되어 있지 않다면 기기에 따라 들쑥날쑥한 성능으로 MPA 대비 아쉬운 경험.
이러한 최적화는 어려우므로 평균적인 노력으로 동일한 서비스를 만든다면 서버에서 렌더링되는 MPA가 우위에 있을 수 있음.
최근에는 MPA의 라우팅 문제를 해결하기 위한 API가 브라우저에 추가 중- 페인트 홀딩: 같은 출처에서 라우팅이 일어날 경우, 흰 화면 대신 이전 페이지의 모습을 잠깐 보여줌
- back forward cache: 브라우저 앞으로 가기, 뒤로가기 실행 시 캐시된 페이지를 보여줌
- Shared Element Transitions: 페이지 라우팅 발생 시, 두 페이지의 동일 요소에 대해 콘텍스트를 유지해 부드럽게 전환되게 함
현대의 서버 사이드 렌더링
현대의 SSR은 LAMP 스택에서 표현한 서버 사이드 렌더링 방식과는 조금 다르다.
기존 LAMP 스택은 모든 페이지 빌드를 서버에서 렌더링해 초기 페이지 진입은 빠른 반면, 라우팅 발생 시에도 서버에 의존해야 하므로 SPA 방식에 비해 라우팅이 느림.
최근 SSR은 두 가지 장점을 모두 취한 방식으로 작동. 최초 진입 시 서버 사이드 렌더링 방식으로 서버에서 완성된 HTML을 제공받고, 이후 라우팅은 자바스크립트를 바탕으로 SPA처럼 작동함.
4.2. 서버 사이드 렌더링을 위한 리액트 API 살펴보기
리액트는 애플리케이션을 서버에서 렌더링할 수 있는 API도 제공한다.
4.2.1. renderToString
SSR 구현에 가장 기초적인 API로, 인수로 넘겨받은 리액트 컴포넌트를 렌더링해 HTML 문자열로 리턴하는 함수.
인수로 주어진 컴포넌트를 기준으로 빠르게 렌더링할 수 있는 HTML을 제공하는 데 목적.
완성된 HTML을 서버에서 제공하므로 초기 렌더링에서 뛰어나고, 메타 정보도 준비한 채 제공할 수 있다.
리액트의 SSR은 빠르게 보여주는 데 목적이 있고, 인터랙션할 준비가 되기 까지는 별도의 JS 코드를 다운로드, 파싱, 실행 과정을 거쳐야 한다.
리액트로 만들어진 애플리케이션이 가진 루트 엘리먼트에 존재하는 속싱인 data-reactroot는 리액트 컴포넌트의 루트 엘리먼트가 무엇인지 식별하는 역할을 하고, 이후 자바스크립트를 실행하기 위한 hydrate 함수에서 루트를 식별하는 기준점이 됨.
4.2.2. renderToStaticMarkup
renderToString
과 유사한 함수로, 차이점은 루트 요소에 추가한 data-reactroot와 같은 리액트에서만 사용하는 추가적인 DOM 속성을 만들지 않아 완전히 순수한 HTML 문자열이 반환됨.
renderToStaticMarkup
의 결과물은 hydrate를 수행하지 않는다는 가정하에 순수한 HTML만 반환하기 때문에, 이벤트 리스너를 등록하는 등 hydrate를 수행해도 클라이언트에서 완전히 새롭게 렌더링하게 되어 에러가 발생.
그래서 이벤트 리스너가 필요 없는 순수한 HTML을 만들 때만 사용된다. 브라우저 액션이 없는 정적인 내용만 있는 블로그, 상품약관 등에 유용.
4.2.3. renderToNodeStream
renderToString
과 결과물이 완전히 동일하지만 두 가지 차이점을 지님.
앞선 두 API와 달리 브라우저에서 사용하는 것이 완전히 불가능
결과물의 타입이
string
이 아니라 Node.js의ReadableStream
이다.ReadableStream
은 utf-8로 인코딩된 바이트 스트림으로, Node.js같은 서버 환경에서만 사용 가능.
스트림은 큰 데이터를 다룰 때 데이터를 청크로 분할해 조금씩 가져오는 방식인데, renderToString
으로 생성한 HTML 결과물의 크기가 크면 서버에 큰 부담이 될 수 있어 스트림을 활용해 데이터를 청크 단위로 분리해 순차적으로 처리할 수 있어 renderToNodeStream
을 사용한다.
4.2.4. renderToStaticNodeStream
renderToNodeStream
과 결과물이 동일하나, renderToStaticMarkup
와 같이 순수 HTML 결과물을 반환하는 메서드
4.2.5. hydrate
hydrate 함수는 renderToString
과 renderToNodeStream
으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 역할을 한다. 정적으로 생성된 HTML에 이벤트와 핸들러를 붙여 인터랙션이 가능하도록 완전한 웹페이지 결과물을 만드는 역할.
브라우저에서만 사용되는 render
메서드의 경우, 컴포넌트와 HTML의 요소를 인수로 받아 해단 컴포넌트를 렌더링하고 이벤트 핸들러를 붙이는 작업을 수행한다.
hydrate
는 이미 렌더링된 HTML이 있다는 가정하에 작업을 수행하는데, 렌더링된 HTML을 기준으로 이벤트를 붙이는 작업만 실행한다. 서버에서 제공한 HTML이 클라이언트의 결과물과 같을 것이라는 가정하에 실행된다.
서버에서 렌더링한 정보가 없는 경우(renderToStaticMarkup
의 결과물인 순수한 HTML을 hydrate
에 두 번째 인수로 넘기는 경우 등) 경고가 노출되는데, 그럼에도 리액트는 정상적으로 웹페이지를 만든다. hydrate
가 렌더링을 한 번 더 수행하며 hydrate
가 수행한 렌더링 결과물 HTML과 인수로 넘겨받은 HTML을 비교하는 작업을 수행하기 때문이며, 여기서 발생한 불일치가 에러의 원인이다. 불일치 발생 시 hydrate
가 렌더링한 기준으로 웹페이지를 그린다. 이렇게 렌더링하는 것은 서버와 클라이언트에서 두 번 렌더링을 하게 되며, 서버 사이드 렌더링의 장점을 포기하는 문제.
불가피하게 불일치가 발생할 수 있는 경우에는 해당 요소에 suppressHydrationWarning
을 추가해 경고를 제거할 수 있다.