리액트는 Single Page Application(SPA)으로 Client Side Rendering(CSR) 방식을 채택한다고 알고 있었다. 리액트를 사용하며 SPA의 장점에 대해 공부했고 모던 웹사이트는 대부분 SSR방식의 SPA가 대세라고 생각하게 되었다. 하지만 네이버 블로그의 "어서와, SSR은 처음이지?" 시리즈를 읽고 CSR과 SCR은 장단점이 있는 렌더링하는 방법일 뿐이고, SPA는 보통 CSR로 동작하지만, 리액트를 활용해 CSR과 SSR 방식 모두 가능하다는 것을 알게 되었다.
이 글에서는 위에 링크한 네이버D2의 글을 읽기 위해 필요한 선지식인 CSR과 SSR의 차이에 대해 집중적으로 설명하고, 네이버 블로그가 CSR에서 SSR로 넘어간 이유, React에서 Next.js를 사용해 SSR환경을 만들지 않고 Node.js기반의 SSR을 채택한 이유에 대해 간단히 덧붙이려 한다.
개인적으로 10년 이상 네이버 블로그에 글을 써오며 네이버 블로그의 역사를 경험한 헤비유저다 보니, 위의 글이 매우 재미있었다. React를 사용하고 있는 주니어 개발자로서는 실무에서 트레이드오프를 할 때 어떤 고민을 하는 지 구체적으로 알 수 있어 인상적이었다.
처음 이 개념에 대해 배울 때, 개념 자체도 어려웠을 뿐더러 SPA
, MPA
, CSR
, SSR
4가지 단어가 혼재되어 제대로 알지 못한 채 섞어쓰는 경우가 많았다. 이 네 가지를 단순하게 정리하자면 다음과 같다.
우선 이름 그대로 이해하면 된다.
SPA(Single Page Application)
: 한 개의 페이지로 구성된 어플리케이션MPA(Multiple Page Application)
: 여러개의 페이지로 구성된 어플리케이션CSR(Client Side Rendering)
: 클라이언트 측에서 렌더링을 하는 방식SSR(Server Side Rendering)
: 서버 측에서 렌더링을 하는 방식일반적으로 CSR
= SPA
, SSR
= MPA
이라고 공식처럼 얘기가 된다. React, Vue, Angular 같은 JS 기반 라이브러리나 프레임워크를 쓰면 기본적으로 HTML, CSS, JS 파일이 각 하나씩 나오기 때문에 자연스레 SPA
가 되면서 CSR
이 되는 것이고, 이를 SSR
로 구현하면 페이지 별로 렌더링을 따로 하기 때문에 MPA
가 되는 것이다. 참고로 현재 React, Vue, Angular 모두 SSR
도 지원한다.
렌더링이란 내용을 브라우저 화면에 표시하는 것이다.
브라우저는 사용자가 웹페이지에 접근할 때, 서버에 페이지에 대한 요청을 한다. 렌더링(Rendering)방식이란 결국 화면에 그려지는 것은 HTML인데 이것을 누가 하느냐 주최에 따라서 CSR(Client Side Rendering)과 SSR(Server Side Rendering)로 나뉘게 된다. 서버가 렌더링을 할 경우, 서버에서는 html, view와 같은 리소스들을 어떻게 보여질지 해석하고 렌더링하여 사용자에게 반환한다. 클라이언트에서 렌더링을 할 경우, 서버는 리소스를 제공하고, 클라이언트가 해석하여 렌더링한다. 브라우저는 서버가 보내준 HTML 파일을 파싱해서 문서 DOM트리를 만든다. DOM트리가 구축되는 동안 브라우저는 렌더트리를 구축한다. CSSOM을 구축하고 레이아웃을 그려 UI의 정확한 위치를 계산해 잡는다. 렌더링 트리가 그려진다. 이 과정을 통해 브라우저는 화면을 보여준다.
과거 MPA의 프론트엔드에서는 (사실 예전에는 프론트엔드라고 부르는 영역자체가 없었다) HTML 렌더링을 백엔드에서 했기 때문에, 단순한 AJAX 처리 후 DOM 조작을 이용한 극히 부분적 렌더링 정도만을 JavaScript로 처리했었다. 반면, 요즘 SPA에서는 대부분의 HTML 렌더링을 프론트엔드에서 처리한다. 그 결과, 프론트엔드가 백엔드의 역할을 상당히 많이 흡수하게 되었다. HTML 렌더링, 페이지 라우팅, 일부 비즈니스 로직, 어플리케이션 상태 저장 등등. 더구나 예전에는 프론트엔드가 굳이 알 필요 없었던 DB 모델링 스키마도 이제는 프론트엔드에서 당연히 알아야 하는 부분이 되어버렸다.
출처:DailyEngineering
전통적인 웹 사이트는 지금보다 문서 하나에 전달되는 파일의 용량이 적었다. 그래서 어떤 요소를 한번 클릭하면 완전히 새로운 페이지(HTML)를 서버에서 전송해 주곤 했다. 그래도 상관 없었다. 그러나 현대에 이르러 점차 웹 사이트가 고도화됨에 따라 한 페이지에 해당하는 페이지 용량이 커져갔고, 매번 새로운 페이지를 전달하는 게 점점 버거워지게 되었다.
전통적인 웹 어플리케이션은 SPA와 대비시키기 위해 MPA(Multiple Page Application)이라고 부르며, SSR 방식을 택한다. 이 방식의 가장 큰 단점은 프론트엔드과 백엔드가 강하게 결합되어 있다는 것이다. 이렇게 작성된 어플리케이션에서는 다른 페이지로 이동을 할 때마다, 서버에 새로운 페이지를 요청하고, 모든 탬플릿은 서버 연산을 통해 렌더링하고 서버는 완성된 페이지 형태를 응답한다.
SSR방식의 렌더링 과정
1.브라우저에서 URL을 입력해서 서버에 HTTP Request를 보낸다.
2.서버가 이 요청을 받아서 해당 페이지를 만들기 시작한다. 이때 필요한 Data Fetching을 미리 해서 (API Call, Hydrate 등) 빈 페이지가 아닌 초기 콘텐츠가 로딩된 페이지를 만들어준다.
브라우저가 이 HTML 페이지를 받아 페이지를 DOM에 그린다. (이 시점부터 페이지가 사용자에게 보이게 된다)
3.브라우저가 페이지를 그리면서 동시에 태그 등을 통해 JS, CSS파일 등을 로딩한다.
4.로딩된 JS를 실행한다.
5.Interactive 한 페이지 구성이 완료된다.
서버사이드렌더링(SSR)의 장점은 서버에서 한번에 완성된 페이지를 받아오기 때문에 초기 로딩속도가 빠르며, 서버에서 한 번에 웹의 정보를 HTML로 받아오기 때문에, JavaScript엔진이 없는 검색로봇이 페이지를 크롤링하기에 적합해 검색엔진최적화(SEO)가 가능하다는 것이다.
SEO
는 홈페이지 혹은 콘텐츠를 검색 결과의 상단에 위치시키는 작업이다.
검색 엔진은 '크롤링(Crawling, 웹 크롤러로 웹사이트 관련 데이터를 가져오는 과정)'과
'인덱싱(Indexing, 크롤링을 통해 얻은 정보를 검색 색인에 저장하는 과정)'을 통해
정보를 카테고리화 한다.
반면 단점은 다음과 같다. html의 링크 태그의 href를 통해 페이지를 이동하고 해당 리소스를 서버에 요청하기 때문에 새로운 페이지로 이동할 때마다 화면이 새로고침되어(깜빡거려) 사용자 경험을 저하시킨다. 또한, 매 페이지마다 서버에서 전송할 데이터를 저장해야 하여 부하가 발생할 수도 있다. 초기 로딩속도는 빠르지만 JS 파일이 로드되기 전까지는 인터렉션이 되지 않는다.
👍 서버사이드 렌더링(SSR)의 장점
👎 서버사이드 렌더링(SSR)의 단점
클라이언트 사이드 렌더링(CSR)은 어플리케이션 생명 주기 중에서 단 한 번만 리소스(HTML, CSS, JavaScript) 를 로딩하고, 그 후에는 데이터를 받아올 때만 서버와 통신한다.
서버는 JSON 파일만 보내주고, html을 그리는 역할은 자바스크립트를 통해 클라이언트 측에서 수행한다. 첫 요청시 딱 한 페이지만 불러오고 페이지 변경 시 기존 페이지의 내부를 수정해서 보여주는 방식이다. 이를 사용자(클라이언트) 관점에서 다시 말하자면 최초 페이지를 로딩한 시점부터는 페이지 리로딩 없이 필요한 부분만 서버로 부터 받아서 화면을 갱신하는 것이다. 필요한 부분만 갱신하기 때문에 네이티브 앱에 가까운 자연스러운 페이지 이동과 사용자 경험(UX)을 제공할 수 있다.
리액트 CSR방식의 렌더링 과정
1.브라우저에서 URL을 입력하면 서버에 HTTP Request를 통해 HTML을 다운로드한다.
2.맨 처음 다운로드한 HTML 파일은 아무것도 그려져 있지 않다. 다만 여러 JS, CSS파일을 불러올 수 있는 링크들이 있으며 이 링크를 통해 JS, CSS를 다운로드한다.
3.브라우저가 JS코드를 실행한다. 이 JS코드 안에서 React가 구동되며 이 React가 VirtualDOM에 콘텐츠를 렌더링 한다.
4.VirtualDOM 구성이 완료되면 이를 브라우저의 DOM에 붙인다. (기본적으로는"root"의 하위)
5.브라우저가 렌더 트리 구성, 페인트, 플로우 등의 과정을 거쳐 페이지를 그린다
6.브라우저에 페이지가 나타나게 된다
정확하게는 실제 페이지는 이동되지 않는다. 하나의 HTML 파일에서 JavaScript를 이용해 동적으로 갱신되는 UI화면 이지만, 사용자는 마치 페이지가 이동되는 것처럼 보여지는 느낌을 받는다.
CSR방식은 중복된 리소스 요청없이 정확히 필요한 요청만 하기 때문에 대부분의 경우 퍼포먼스 상 이득이 있고, 사용자에서 네이티브 앱과 비슷한 수준의 향상된 사용자 경험(UX)을 제공할 수 있어서 최근 SPA 가 대세가 되고 있다. 하지만 초기 구동속도가 느리며, 검색엔진 최적화가 어렵다는 단점이 있다. 자바스크립트로 짠 프로젝트는 자바스크립트 엔진이 내장된 검색엔진 크롤러가 아닌 경우(구글 이외의 대부분) 로봇이 데이터를 수집하지 못하기 때문이다.
SPA 어플리케이션은 서버에 정적 리소스를 최초 한번만 요청하고, 받은 데이터는 전부 저장해놓으며 이 데이터는 오프라인에서도 사용이 가능하다. 때문에 SPA에서는 로컬 데이터를 효과적으로 캐시(cache) 할 수 있다. 캐시(cache)란 데이터나 값을 미리 복사해 놓는 임시 장소를 가리킨다. 캐시에 데이터를 미리 복사해 놓으면 계산이나 별도의 접근 시간 없이 더 빠른 속도로 데이터에 접근할 수 있다.
👍 SPA 장점
👎 SPA 단점
이 두 가지 렌더링 방법을 상호보완하여, 첫 번째 페이지 로딩에서는 서버 사이드 렌더링(SSR)을 사용하고, 그 후에 모든 페이지 로드에는 클라이언트 사이드 렌더링(CSR)을 활용하는 방법을 많이 사용한다.
SPA는 보통 CSR로 동작하고, 그렇기 때문에 실제 HTML은 비어있는 경우가 많아 검색엔진 최적화(SEO)에서 불리한 부분이 많다. 이를 해결하기 위해 별도 기법을 쓰지 않는다면 자바스크립트 엔진이 없는 검색엔진에서는 자바스크립트 파일을 로봇이 읽어내지 못한다. 때문에 API 통신이 아닌, 실제 데이터를 포함한 채로 렌더링되는 SSR용 페이지를 별도로 만들어야 한다. 서버의 수고를 덜기 위해, 요즘은 프론트엔드에서 Next.js나 Nust.js, Node.js 등을 이용해 개발을 한다. 프론트엔드 개발자가 익숙한 JS를 사용해 서버 측 코드로 페이지를 개발한다고 이해하면 쉽다. 이 경우, SPA에서 SSR구현이 편해지고, 별도 SSR 개발을 위한 백엔드 개발자가 필요하지 않아진다. 그 결과, 백엔드와 프론트엔드 개발자는 API로만 소통할 수 있도록 분업화가 된다.
Next.js는 다음과 같은 방식으로 작동한다.
1. 사용자가 처음 페이지를 접속을 요청 했을때 Next.js 서버는 사용자에게 랜더링될 HTML을 응답 값으로 보내준다 (SSR 방식).
2. 그 후 브라우저는 추가적인 자바스크립트 번들을 다운로드 받아 실행한다.
3. 사용자가 해당 페이지에서 다른 페이지로 이동할때는 Next.js에 서버가 아닌 브라우저에서 처리하여 이동하게 한다. (CSR 방식)
또는 CSR에서 메타 태그를 정의해주는 라이브러리를 사용할 수 있다. 대표적인 라이브러리로 react-helmet
이 있다. 이 라이브러리는 동적으로 SEO에 필요한 메타태그들을 쉽게 변경할 수 있도록 도와준다. 이를 통해 JSX 또는 TSX 내부에서 메타태그를 관리할 수 있다.
이외에 CSR의 단점인 초기 렌더링 속도는 이 문제를 해결하기 위한 고안된 여러가지 렌더링 기법이 있다.
SPA가 최근에 개발된 방식인 것은 맞으나, SPA와 MPA 둘 중 어느 것을 사용해야 하는지 절대적으로 더 나은 것이라 정해진 바는 없다. CSR이 SEO에 취약한 것은 사실이지만, 모든 서비스가 아닌 공개되어야 하는 퍼블릭 데이터에만 SEO가 필요한 것이기 때문에, 계획중인 비즈니스에 SEO가 필요한 지도 고민해봐야 한다. CSR과 SSR 중 계획중인 비즈니스가 어떤 고객을 타겟팅하고 있는지, SEO 관점이 중요한지, 비즈니스의 현재 상황이 어떠한지 파악한 뒤 장단점을 고려해 정하면 된다. 네이버 블로그의 SSR 전환은 대표적으로 그 사례를 보여준다. 아래 인용은 네이버D2 원글에서 발췌해온 내용이다.
블로그는 주요 사용자들이 방문하는 블로그 세션 영역과 네이버 검색과 메인을 통해 노출되는 블로그 글 영역으로 구분할 수 있다. 블로그 서비스의 대다수 트래픽은 블로그 글에 집중되어 있다. 사용자들은 네이버 검색을 통해 블로그 글을 보고, 네이버 메인에 노출된 블로그 글을 본다.
CSR만을 지원하는 anuglar.js 프레임워크 내에서는 페이지 로드 이후 동적으로 콘텐츠를 생성하기 때문에 콘텐츠를 빠르게 소비하는 사용자의 요구 사항을 충족시킬 수가 없었다. 네트워크 상황이 좋지 않다면 CSR을 이용할 경우 사용자들은 글을 보기 전에 상당 시간 하얀 화면을 봐야 할 수도 있다.
React 기반의 SSR 환경을 만들 때 가장 간단한 방법은 Next.js를 이용해 SSR을 구성하는 것이다. Next.js는 SSR을 구성하기에 훌륭한 프레임워크이다. React 공식 가이드에서도 명시되어 있다.
하지만 모바일 블로그는 Next.js를 사용하지 않았다. 검토 당시 Next.js는 6.x대였기도 하거니와, React와 같이 뷰 전용 라이브러리가 아닌 서비스 플로우 전체를 담당하는 프레임워크를 블랙박스로 관리하기에는 운영 리스크가 너무 컸다. 또한, 빠르게 발전하는 프런트엔드 생태계의 변화를 Next.js에 의존해 적용해야만 했기에 모바일 블로그는 Node.js 기반의 SSR 환경을 자체 구축하기로 결정했다.Next.js는 9.x부터는 SSR을 구성할 때 갖추어야 할 고민들이 잘 녹아 들어가 있다. 각 모듈의 커스터마이징도 가능한 구조로 만들어져서 프런트엔드 생태계의 변화를 대다수 수용할 수 있는 구조가 되었다. 소규모의 프로젝트 같은 경우에는 적극 도입해 사용해 보기를 권장한다.
우리가 라이브러리나 프레임워크를 사용하는 목적은 개발 생산성 향상에 있다. 코드를 균일하게 만들어 협업을 용이하게 해준다거나, 개발 시 겪게 되는 문제를 효과적으로 해결할 수 있는 설루션을 제공해 개발자의 생산성 향상에 도움을 준다. 위에서 검토한 기술 스택 모두 이러한 관점에서는 결코 뒤지지 않는 기술 스택이다.
블로그의 경우에는 이런 개발 생산성도 중요하지만 대국민 서비스를 제공하고 있는 상황이기 때문에 다음과 같은 질문에 답을 구하려고 했다.
- 사전에 문제를 빠르게 인지하고 검증할 수 있는가?
- 문제 발생 시 어떻게 하면 쉽고 효과적으로 해결할 수 있는가?
- 다양한 상황에 대해 충분한 검증이 이루어졌는가?
즉, 안정성, 투명성 관점에서 보다 우위에 있는 기술 스택을 선정했다.
TypeScript의 타입 검증을 통해 개발자의 실수를 사전에 방지하고, 문제 발생 시 추적이 용이할 수 있도록 '단방향 데이터 흐름을 사용하는 redux'와 '함수의 lazy 속성을 이용한 redux-thunk 미들웨어'를 선택했다. 뿐만 아니라 이미 다양한 이슈에 대응한 오랜 역사가 있는 Express를 사용함으로써 안정성을 높이려고 했다.