서버 사이드 렌더링
서버 사이드 렌더링이란?
해당 챕터에서는 우선 서버 사이드 렌더링이 최근에 각광받게 된 이유에 대해 설명합니다.
싱글 페이지 애플리케이션의 세상
우선 먼저 서버 사이드 렌더링의 반대가 되는 싱글 페이지 애플리케이션에 대해 알아봅니다.
싱글페이지 애플리케이션이란?
렌더링과 라우팅을 포함한 대부분의 기능을 서버가 아닌 브라우저의 자바스크립트로 처리하는 방식을 말합니다. 서버에서 HTML
파일을 내려받지 않고 하나의 페이지에서 모든 것을 처리합니다.
만약 다른 페이지로 이동할 경우 앞서와 같이 HTML
페이지를 불러오는 것이 아니라 다음 페이지에 해당하는 JS
코드를 불러와서 DOM
을 추가, 수정, 삭제 하는 방법으로 페이지가 전환이 됩니다.
이러한 방식은 초기 렌더링시 자바스크립트 리소스가 크다는 단점이 있지만, 훌륭한 UI/UX를 제공합니다.
전통적인 방식의 애플리케이션과 싱글 페이지 애플리케이션의 작동 비교
이전 방식에서는 페이지 전환이 발생할 때마다 서버에 새롭게 페이지를 요청하고, HTML 페이지를 다운로드하여 파싱하는 과정을 거칩니다. 때문에 사용자 입장에서는 새롭게 만들어지는 것처럼 보입니다.
싱글 페이지 렌더링 방식의 유행과 JAM 스택의 등장
훌륭한 UI/UX를 제공한다는 장점으로 근래에 개발된 많은 웹 페이지들은 싱글 페이지 렌더링 방식을 채택하였습니다.
과거 PHP
나 JSP
를 기반으로 웹 애플리케이션을 만들때는 서버 사이드에서 렌더링이 이뤄졌습니다. 여기서 자바스크립트는 어디까지나 사용자에게 추가적인 경험을 주기 위한 보조적인 수단으로 사용됐습니다.
그리고 서서히 자바스크립트의 크기가 커지면서 모듈화하는 방안이 논의되었으며, 그에 따라 등장한 것이 CommonJS 입니다.
싱글 페이지 애플리케이션이 나온 것은 단순히 사용자 UI/UX를 고려한 것은 아닙니다. PHP
시절 웹 애플리케이션을 만들기 위해서는 자바스크립트 이외에도 고려할 부분이 많았지만 하지만 이제는 자바스크립트만으로도 대부분의 문제를 해결할 수 있습니다. 즉, 프런트엔드 개발자들에게 좀 더 간편한 개발 경험을 제공하였으며, 더욱 편하게 웹 애플리케이션을 생성할 수 있다는 장점을 제공하였습니다.
이러한 싱글 페이지 애플리케이션의 유행으로 JAM(JavaScript, API, Markup)스택이 유행하게 되었습니다. 또한 Node.js의 고도화에 힘입어 Express, MongoDB 처럼 API 서버 자체도 자바스크립트로 구현하는 구조가 인기를 끌었습니다.
이러한 발전에 힘입어 과거와 달리 현재는 네트워크 속도와 하드웨어의 성능이 좋아졌다. 또한 애플리케이션도 발전했기에 과거 단순히 보여주기 위한 수단에서 다양한 작업을 처리하고 있습니다.
서버 사이드 렌더링이란?
서버 사이드 렌더링(SSR)은 사용자가 처음 접속할 때 서버에서 페이지를 렌더링하여 HTML을 제공하는 방식을 의미합니다. 싱글 페이지 애플리케이션과의 차이는 웹 페이지 렌더링의 책임이 어디에 있는가에 있습니다. SPA의 경우 자바스크립트 번들에서 처리하지만 서버 사이드 방식을 채택하면 렌더링에 필요한 작업을 모두 서버에서 실행합니다.
서버 사이드 렌더링의 장점
최초 페이지 진입이 비교적 빠르다.
서버가 충분한 성능을 갖추고 있다면 서버에서 렌더링된 페이지를 즉시 제공할 수 있습니다. 하지만 서버가 이를 감당할 수 없다면 SPA보다 느릴 수 있습니다.
검색 엔진과 SNS 공유 등 메타데이터 제공이 쉽다.
검색 엔진과 사용자 방문의 차이는 페이지를 보는 것이 아닌 정적인 정보를 가져오는 것이 목적이므로 페이지를 방문할 경우 자바스크립트 코드를 실행하지 않습니다. 따라서 정적인 HTML 파일을 제공하는 서버 사이드 렌더링은 검색 엔진 최적화에 유용합니다.
누적 레이아웃 이동이 적다
누적 레이아웃 이동이란 콘텐츠가 늦게 로드되면서 화면이 갑자기 변경되어 발생하는 부정적인 사용자 경험을 말합니다. 싱글 페이지 애플리케이션은 페이지 콘텐츠가 API 요청에 의존하며, 응답 속도가 일정하지 않습니다. 하지만 서버 사이드 렌더링의 경우 완성된 페이지를 제공하므로 이 문제에서 비교적 자유롭습니다.
사용자 디바이스 성능에 비교적 자유롭다.
기존에 싱글 페이지 애플리케이션의 경우 자바스크립트 번들에 의존적이기 때문에 사용자의 디바이스 성능에 따라 애플리케이션의 성능 차이가 심하였습니다. 하지만 서버 사이드 렌더링의 경우 부담을 서버에 나눌 수 있으므로 사용자의 디바이스 성능으로부터 조금 더 자유로워질 수 있습니다.
보안에 더 안전하다.
서버 사이드 렌더링의 경우 민감한 작업을 서버에서 수행하고 그 결과만 브라우저에 제공하여 보안 위협을 피할 수 있습니다.
단점
소스 코드를 작성할 때 항상 서버를 고려해야한다.
코드를 작성하다 보면
window is not defined
와 같은 에러를 자주 접하게 됩니다. 그러므로 서버에서도 실행 가능성이 있는 코드라면window
접근을 최소화 해야하고,window
사용이 불가피하다면 서버 사이드에서 실행되지 않도록 처리해야한다.
적절한 서버가 구축돼 있어야 한다.
기존 서버는 정적인 데이터인 자바스크립트와
HTML
파일을 제공하는 역할로 충분했습니다. 하지만 서버 사이드 렌더링은 사용자의 요청을 받아 렌더링을 수행 할 서버가 필요합니다.
서비스 지연에 따른 문제
애플리케이션 규모가 커지고 작업이 복잡해진다면 이에 따라 다양한 요청이 얽혀있어 병목 현상이 일어나 더 안 좋은 사용자 경험을 제공할 수도 있습니다.
SPA와 SSR을 모두 알아야하는 이유
서버 사이드 렌더링 역시 완벽한 해결책은 아니다.
서버에서 모든 작업이 일어난다고 해서 모든 성능 문제가 해결되는 것은 아닙니다. 잘못된 설계는 관리 포인트만 늘어나기 때문에 역효과를 낳을 수도 있습니다. 제공하고 싶은 내용이 무엇인지, 또 어떤 우선순위에 따라 페이지의 내용을 보여줄지를 잘 설계하는 것이 중요합니다.
싱글 페이지 애플리케이션과 서버 사이드 렌더링 애플리케이션
가장 뛰어난 싱글 페이지 애플리케이션은 가장 뛰어난 멀티 페이지 애플리케이션보다 낫다. 게으른 로딩과 코드 분할을 칼같이 지키면서 불필요한 리소스 다운로드를 최소화한 애플리케이션은 최적화를 한 멀티 페이지 애플리케이션보다 낫습니다.
평균적인 싱글 페이지 애플리케이션은 평균적인 멀티 페이지 애플리케이션보다 느린 경우가 많습니다. 멀티 페이지 애플리케이션은 매번 서버에 렌더링 요청을 보내고 서버는 비슷한 성능의 렌더링을 수행할 것입니다.
현대의 서버 사이드 렌더링
현대의 서버 사이드 렌더링은 지금까지 LAMP 스택에서 표현했던 서버 사이드 렌더링 방식과는 조금 다릅니다. 초기 렌더링을 서버에서 하기 때문에 초기 페이지 진입이 빠르다는 장점이 있지만 라우팅이 발생할 때도 서버에 의존해야 하기 때문에 싱글 페이지 렌더링 방식에 비해 라우팅이 느리다는 단점이 있습니다. 이로 인해, 최근의 서버 사이드 렌더링은 이 두 가지 방식의 장점을 모두 취한 형태로 작동합니다. 때문에 서버 사이드 렌더링과 싱글 페이지 애플리케이션의 장점을 모두 알고 있어야 제대로 된 웹 서비스를 구축할 수 있습니다.
정리
웹 애플리케이션을 만들고 싶은 개발자라면 두 가지 방법을 모두 숙지할 필요가 있다. 둘 중 어느 것이 완벽하게 옳다고 말할 수 없으므로 두 가지 모두를 이해하고 필요에 따라 맞는 방법을 사용할 수 있다.
서버 사이드 렌더링을 위한 리액트 API 살펴보기
리액트에 애플리케이션을 서버에서 렌더링할 수 있는 API를 제공합니다. API 정보는 react-dom/server.js
에서 확인할 수 있습니다.
renderToString
함수 이름에서 알 수 있듯이 인수로 넘겨받은 리액트 컴포넌트를 렌더링하여 HTML 문자열로 변환하는 함수입니다. 서버 사이드 렌더링을 구현하는 데 가장 기본적인 API로, 최초 페이지를 렌더링하는 데 사용됩니다.
실행 코드
결과물
결과물을 본다면 <App/>
컴포넌트에 존재하는 useEffect
같은 훅은 결과물에 포함되지 않는 것을 알 수 있습니다.
renderToString
을 사용하면 검색 엔진이나 SNS 공유를 위한 메타 정보도 미리 포함한 HTML을 제공할 수 있으므로 싱글 페이지 애플리케이션 구조보다 손쉽게 완성할 수 있습니다.
여기서 중요한 사실은 리액트의 서버 사이드 렌더링은 단순히 최초 HTML 페이지를 빠르게 그려주는 것
에 목적이 있습니다. 사용자와 인터랙션할 준비가 되기 위해서는 이와 관련된 별도의 자바스크립트 코드를 모두 다운로드, 파싱, 실행하는 과정을 거쳐야 합니다.
추가로 div#root
에 존재하는 속성인 data-reactroot
를 본다면 해당 역할은 루트 엘리먼트가 무엇인지 식별하는 역할을 도와줍니다. 해당 속성은 이후에 hydrate
함수에서 루트를 식별하는 기준점이 됩니다.
renderToStaticMarkup
renderToString
과 굉장히 유사하지만 data-reactroot
와 같이 추가적인 DOM 속성을 만들지 않는 점이 있습니다. 하지만 순수한 HTML만 반환하기 때문에 해당 함수로 생성 시 useEffect
와 같은 client API를 사용할 수 없습니다.
renderToNodeStream
renderToString
과 결과물은 동일하지만 차이점이 있습니다. renderToNodeStream
는 브라우저에서 사용이 안된다는 점입니다. 하지만 반환되는 값은 Node.js의 ReadableStream
이고 이는 Node.js나 Deno, Bun과 같은 서버 환경에서만 사용할 수 있습니다. 브라우저가 원하는 결과물, 즉 string을 얻기 위해서는 추가적인 처리가 필요합니다. ReadableStream
는 브라우저에서도 활용이 가능한 객체이지만 만드는 과정을 브라우저에서 못하게 되어있습니다.
ReadableStream
을 사용하기 위해서는 스트림을 이해해야 합니다. 스트림은 큰 데이터를 다룰 때 데이터를 청크로 분할하여 조금씩 가져오는 방식을 의미합니다.
만약 스트림대신 renderToString
을 사용한다면 거대한 HTML 파일이 완성되기 까지 기다려야합니다. 때문에 스트림을 활용하여 작은 단위로 쪼개 연속적으로 받음으로써 Node.js 서버의 부담을 줄일 수 있습니다.
때문에 대부분의 리액트 서버 사이드 렌더링 프레임워크는 renderToString
대신 renderToNodeStream
을 사용하고 있습니다.
renderToStaticNodeStream
renderToNodeStream
과 결과물은 동일하나 자바스크립트에 필요한 리액트 속성을 제공하지 않습니다. 이또한 hydrate가 필요없는 순수 HTML 결과물을 반환합니다.
hydrate
renderToString
과 renderToNodeStream
으로 생성된 HTML 콘텐츠에 자바스크립트 핸들러나 이벤트를 붙이는 것을 의미합니다. hydrate를 동작시키기전에 반환 값만으론 사용자와 인터렉션하는 것은 불가능합니다. 때문에 정적으로 생성된 HTML 페이지에 이벤트와 핸들러를 붙여 완전한 페이지 결과물을 생성할 수 있습니다.
render
함수와 동일하게 HTML 요소를 인수로 받습니다. 그러나 render
함수와는 달리, 이미 렌더링된 HTML을 가정하고 실행되며, 이벤트 핸들러만 추가합니다
정리
싱글 페이지 애플리케이션만 만들어봤다면 서버 사이드 렌더링을 하기 위해서 할 것들이 많아서 복잡하다고 느낄 수도 있습니다. 이를 매번 개발자 개인이 작성하는 것은 매우 비효율적입니다.
사용자에게 더 빠른 페이지를 제공하기 위해서 서버를 다루는 것은 개발자에게 있어서 큰 부담이 됩니다. 때문에 리액트 팀이 적절한 프레임워크를 사용하는 것을 추천했는지 이해할 수 있다.
Next.js 톺아보기
무엇인가요?
Vercel이라는 미국 스타트업에서 만든 풀스택 웹 애플리케이션을 구축하기 위한 리액트 기반 프레임워크입니다. Next.js는 PHP의 개발 방식에서 영감을 받아 만들어졌습니다.
리액트 기반 프로젝트에서 서버 사이드 렌더링을 고려하고 있다면 Next.js를 사용하는 것이 현재 가장 합리적인 선택으로 보입니다. Vercel의 전폭적인 지원뿐만 아니라 SWR, SWC, Turbopack, Svelte 등 웹 생태계 전반에 영향력 있는 프로젝트를 계속해서 개발하거나 인수를 하고 있기 때문에 이는 합리적 일 수 있습니다.
시작하기
create-react-app
과 같이 Next.js에서는 create-next-app
을 통해 빠르게 Next.js 기반 프로젝트를 생성할 수 있도록 돕습니다.
next.config.ts
swcMinfy: Vercel에서는 SWC라 불리는 번들링과 컴파일을 도와주는 오픈소스를 개발하였습니다. 해당 오픈소스는 자바스크립트 기반의 바벨과 달리 러스트라는 새로운 언어로 작성하여 C++과 동등한 수준의 속도로 번들링과 컴파일을 도와주며 병렬로 처리한다는 점등이 있습니다.
Data Fetching
Next.js에서는 서버사이드 렌더링을 지원하기 위한 데이터 불러오기 전략을 가지고 있습니다. 이를 Data Fetching이라고 합니다. pages
와 같이 폴더에 라우팅이 되어있는 파일에서만 사용할 수 있습니다.
GetStaticPaths와 getStaticProps
이 두 함수는 CMS나 블로그, 게시판 등에서 사용자와 관계없이 정적으로 생성된 페이지를 제공할 때 사용됩니다. 반드시 함께 있을 때 사용할 수 있습니다.
getStaticPaths
는 /pages/post/[postId]
와 같이 접근 가능한 주소를 정의하는 함수입니다. 해당 주소로 정의를 해둘 경우 /pages/post/1
, /pages/post/2
의 경우 접근이 가능하지만 이외의 페이지에서는 404를 반환합니다.
getStaticProps
는 앞에서 정의한 페이지를 기준으로 요청이 왔을 때 제공할 props를 반환하는 함수입니다. 각각 1과 2로 제한돼 있기 때문에 각 함수의 응답 결과를 변수로 가져와 prop의 로 반환합니다.
마지막으로 포스트는 앞서 getStaticProps가 반환한 포스트를 렌더링하는 역할을 합니다.
종합해보면 다음과 같습니다:
getStaticPaths
에서 해당 페이지는 1, 2만 허용하게 작성을 하였고getStaticProps
에서는 1과 2에 대한 데이터 요청을 수행하여 props로 반환.마지막 포스트 페이지에서 해당 결과를 바탕으로 페이지를 렌더링 합니다.
이 두 함수를 사용하면 빌드 시점에 미리 데이터를 불러온 다음 HTML 페이지를 생성할 수 있습니다.
빌드 시점에 미리 생성되지 않은 페이지에 접근할 경우, getStaticProps
의 fallback
옵션을 사용할 수 있습니다.
getServerSideProps
앞선 두 함수가 정적인 페이지 제공을 위해 사용된다면, getServerSideProps는 서버에서 실행되는 함수이며 해당 함수가 있다면 무조건 페이지 진입 전에 이 함수를 실행합니다.
해당 함수를 본다면 의문이 들 수 있습니다.
앞서 리액트의 서버 사이드 렌더링의 순서를 보면 다음과 같습니다:
서버에서 fetch 등으로 렌더링에 필요한 정보들을 가져옵니다.
가져온 정보를 기반으로 HTML을 가져옵니다.
2에서 가져온 정보를 클라이언트에게 제공합니다.
3의 정보를 바탕으로 클라이언트에서 hydrate 작업을 합니다. 해당 작업은 DOM에 리액트 라이프 사이클과 이벤트 핸들러를 추가하는 역할을 합니다.
만약 hydrate로 만든 컴포넌트 트리와 서버에서 만든 HTML이 다르다면 불일치 에러를 나타냅니다.
만약 hydrate를 통해 결과물을 만들 때 똑같은 함수가 실행이 되므로 결과가 달라진다고 생각할 수 있습니다. 이걸 Next.js에서는 script 형태로 내려줌으로써 재요청을 방지할 수 있습니다.
또한 해당 함수는 사용자가 매 페이지를 호출할 때마다 실행되고 끝나기 전에는 어떠한 HTML도 보여줄 수 없습니다.
조회에 실패하여 다른 페이지로 리디렉션하고 싶을 경우, redirect 속성을 사용할 수 있습니다.
getInitialProps
getInitialProps
는 getStaticProps
나 getServerSideProps
나오기 전에 사용할 수 있었던 유일한 페이지 데이터 불러오기 수단입니다. 지금은 추천하지 않는 방법입니다. 하지만 최상위 컴포넌트인 _app.tsx
에서는 여전히 getInitialProps
를 사용할 수 있으므로 알고 있어야 합니다.
next.config.js 살펴보기
basePath
basePath를 설정하지 않았을 때 기본 값은 "/"
로 localhost:3000
에 접근하게 될 경우 basePath
기준으로 가지게 됩니다. 만약 basePath: docs
로 설정을 할 경우 localhost:3000/docs
로 접근을 할 경우 가지게됩니다. Next.js에서 제공하는 기능으로 router.push()
의 경우 따로 명시해줄 필요가 없지만 window.location.push()
와 같은 경우 설정을 해주어야 합니다.
poweredByHeader
Next.js를 사용할 경우 응답 헤더에 X-Power-by 정보를 재공하는데 false
를 선택할 경우 해당 정보를 제거합니다. 기본적으로 보안 관련 솔루션에서는 powerd-by
헤더를 취약점으로 분류하기에 false
로 설정하는 것이 좋습니다.
swcMinify
swc를 이용하여 코드를 압축할지를 나타냅니다. 기본값은 true 입니다.
redirects
특정 주소를 다른 주소로 보내고 싶을 때 사용됩니다. 정규식도 사용 가능합니다.
reactStrictMode
리액트에서 제공하는 엄격 모드를 설정할지 여부를 나타냅니다. 기본값은 false
이지만 true
를 권장한다.
assetPrefix
빌드된 결과물을 동일한 호스트가 아닌 다른 CDN등에 업로드하고자 한다면 해당 옵션에 해당 CDN 주소를 명시하면 된다.
정적인 리소스를 별도 CDN에 업로드하고 싶다면 이 기능을 활용하면 됩니다.
정리
Next.js가 제공하는 기능은 이미지를 최적화 할 수 있는 next/image
, 서드파트 스크립트를 최적화해서 불러올 수 있는 next/script
등 다양한 기능들이 많습니다. 해당 프레임워크는 서버 사이드 프레임워크의 대명사가 됐을 만큼 널리 사용되고 있습니다.