[Next.js] CSR vs SSR

솔방울·2023년 1월 20일
9
post-thumbnail

Next.js를 도입하게 되면서, 다양한 pre-render 방식에 대해 접하게 되었다. 하지만 자료를 읽으며 용어에 혼란이 온 부분이 있었다. 가령 SSG도 넓은 범위에서는 서버 사이드 렌더링의 정의에 속하지만, 현재 나오는 Next.js의 pre-render 방식과 관련해 정의한 문서들은 SSG를 서버 사이드 렌더링과 다른 방식인 것처럼 정의내렸다. 더 나아가 CSR과 SSR을 설명함에 있어 중요한 MPA와 SPA를 다루지 않는 글도 많았다. 그래서 개념 정리를 위해, 그리고 이 글을 보는 누군가도 개념을 확실히 잡기를 바라는 마음에서, CSR/SSR/SSG/ISR로 나누어 설명하는 render 방식을 CSR과 SSR이라는 큰 틀로 나누어 설명하고자 한다.

잘못된 설명이나 지적, 수정이나 질문은 언제나 환영입니다!
어떻게 쓰는가에 대해서는 Next.js 공식문서에 잘 정의되있어서, 원론적인 부분만을 다루려 합니다.

TL;DR

CSR과 SSR은 모두 필요한 렌더링 방식이다. Next.js 환경에서 CSR과 SSR 둘 다 구현이 가능하므로, 적절히 필요한 상황에 섞어쓰자.

1. CSR(Client Side Rendering)

클라이언트로 렌더링 책임을 넘기는 방식이다. pre-render랑은 아무런 관련이 없으며, 웹사이트를 구축하는 방식 중 SPA(Single Page Application)을 구현하는데 주로 사용한다.

SPA

싱글 페이지 애플리케이션(single-page application, SPA, 스파)은 서버로부터 완전한 새로운 페이지를 불러오지 않고 현재의 페이지를 동적으로 다시 작성함으로써 사용자와 소통하는 웹 애플리케이션이나 웹사이트를 말한다.
출처 : 위키백과

SPA는 말 그대로 페이지가 하나인, 기본 HTML 파일을 하나로 두는 것을 의미한다. 다른 페이지로 이동할 때 새로운 HTML 파일을 가져오는 것이 아니라 필요한 부분만을 수정하여 바꿔끼우는 방식이다. SPA는 initial request 시에 모든 정적 리소스들(이미지, HTML, JS 등)를 다운받고, 해당 페이지에 필요한 데이터는 클라이언트에서 요청하여 서버로부터 동적으로 받아온다. 프론트엔드 프레임워크 3대장인 React, Vue, Angular가 다음과 같은 방식을 채택하면서 유명해졌다.

하지만 SPA는 항상 CSR의 방식으로 렌더링되지는 않는다. 물론 SPA에서 SSR을 구현하도록 하는 장치들도 있다.(ex. Next.js) 그러나 대부분의 SPA가 필요한 내용을 client 측에서 서버측에게 요청을 보내기 때문에, SPA는 CSR로 렌더링한다는 얘기가 붙여지는 것 같다. 이는 클라이언트에 렌더링 책임을 넘긴다는 의미와 일맥상통한다.

SPA 형식으로 개발하면 component 단위로 개발하여 여러 페이지에 재사용하기 쉽다. 이는 API 요청을 하는 backend code도 마찬가지므로, 모바일 앱의 기초가 되는 형식이 된다.

CSR (Client Side Rendering)

위에서 언급했듯이, 클라이언트에서 렌더링 책임을 받는 형태이며 SPA를 구축하는데 용이한 렌더링 방식이다. 리액트를 기준으로 밑 사진과 같이, 브라우져측에 HTML와 CSS, JS, 이미지 등의 파일을 다운받을 수 있는 CDN 링크를 주고 브라우져에서 이를 다운받게 된다. 이때 리액트에서 jsx 구문의 파일은 본래 JS 파일인지라, 따로 다운로드 받아져 빈 HTML file만을 받게 된다.

여기서 CSR과 SSR을 결정짓는 것에는 TTV와 TTI라는 개념도 있다.

TTV

Time To View, 클라이언트에서 페이지의 내용을 '볼 수 있을 때'까지의 시간을 의미한다.

TTI

Time To Interact, 클라이언트에서 페이지의 내용을 '동작할 수 있을 때'까지의 시간을 의미한다.

CSR은 이 TTV와 TTI가 같다. 결국 클라이언트에게 보여지는 내용을 브라우져가 모두 다운받아야 하기 때문이다. SPA에서는 첫 방문 시 필요한 모든 JS script들을 다운로드 받기 때문에, CSR에서 중요한 것은 이런 스크립트 파일들을 얼마나 잘 code splitting해서 빠르게 유저가 다운로드 받아볼 수 있는지가 중요해진다.

CSR의 장점

1) Blank page flikering

페이지를 이동할 때 (깜박거림)이 없다. 새로운 HTML 파일을 받아오는 것이 아니라 필요한 부분만을 클라이언트에서 요청하여 업데이트 하기 때문이다. 이는 사용자 경험 증진에 도움이 될 수 있다.

2) Less server load

앞서 이야기한 것의 연장선상으로, 필요한 부분만을 요청하기 때문에 서버 리소스를 상대적으로 덜 낭비한다. 그리고 서버측의 렌더링 부담을 줄여준다. 그리고 한번 다운로드 받은 정적 리소스들은 cache로 저장하기 때문에, 다시 받아오지 않는다.

3) Fast loading after visiting the site

CSR은 첫 방문 시에 필요한 모든 리소스들을 다운 받고, 이후 다른 페이지로 라우팅할 때에는 그 페이지에 해당되는 클라이언트에서 서버로 요청하면 된다. 그러므로 다른 페이지로 이동할 때마다 모든 파일을 다시 요청할 필요가 없어 로딩 시간이 SSR에 비해 빠르다.

웹사이트 로딩 시간에 따른 여러 통계 자료는 다음 자료를 참고하기 바란다.

CSR의 단점

1) Bad SEO(Search Engine Optimization)

이미 내용이 채워진 상태로 받는 것이 아니라, 관련 내용들은 브라우져에서 다운로드 받아야 하므로 HTML 파일은 비워진 상태이다. 그러므로 검색 엔진이 판단하기에 해당 사이트가 무엇을 하는 곳인지 인지하기 힘들다. SEO quality가 떨어질수록 검색 랭킹에 높게 잡히게 할 수 없다.

허나 V8과 같은 js 기반 엔진은 이를 해결할 수 있다고 한다. 혹은 react-helmet 등 meta 태그를 별도로 설정할 수도 있다. (참고)

2) Slow loading the first time the site is accessed

CSR 방식은 방문 당시 필요한 모든 리소스를 다운받기 때문에, 번들링된 JS script들에 대해서 code splitting이 되어있지 않다면 이를 모두 받아야 하므로 사용자에게 첫 화면을 느리게 보여줄 수도 있다.
(해결방법 : code splitting)

2. SSR(Server Side Rendering)

모든 렌더링 책임을 클라이언트에게 맡기는 CSR 방식에 비해, 서버단에서 pre-render되어져 브라우져로 보내게 되는 파일에는 빈 HTML 파일이 아니라 컨텐츠가 채워져 있는 상태로 보내지게 된다. MPA(Multi Page Application)를 구현하는데 쓰이는 렌더링 방식이다.

MPA

A multi-page app (MPA) basically means a traditional web application that loads on the server’s side.
출처 : Madappgang

MPA는 SSR의 방식으로 렌더링될 수밖에 없다. 말 그대로 여러 개의 페이지를 두는 MPA는 유저가 페이지를 새로고침하거나 다른 페이지로 이동할 때마다 해당 페이지의 HTML file을 서버로부터 다시 받아야 한다. 이때 HTML 파일은 위 사진과 같이 이미 컨텐츠가 들어있는 상태의 HTML file을 받는다. 그러므로 이미 server에서 rendering된 파일들을 주기 때문에, MPA는 SSR이라는 렌더링 방식을 채택한다.

또한 위 사진을 보면 "Traditional Page Lifecycle", 즉 MPA 방식이 전통적인 웹사이트 구현 방식이였음을 알 수 있다. 이는 CSR보다 SSR이 전통적인 렌더링 방식이었음을 알 수 있다.

필요한 부분만을 클라이언트에서 요청하는 방식이 아니라, 전체 골격을 다시 request을 하는 방식이라고 이해할 수 있다.

SSR(Server Side Rendering)

이렇듯 SSR이란 서버에서 렌더링된 상태로 사용자에게 전해지는 것이다. React를 기준으로한 밑 사진을 보면 알 수 있듯이, 렌더링된 HTML 파일이 먼저 사용자에게 다운로드 받아진 후, 이후 실행에 필요한 JS 코드를 다운로드 받게 되는 형식이다. 그러므로 사용자는 빠르게 볼 수 있는 화면을 받아볼 수 있다. JSX 구문이 js 파일이긴 하지만, SSR 환경에서는 모두 렌더링된 상태로 사용자에게 보내진다. 여기서 js 코드를 다운로드 받는다는 의미는 그 외에 util 목적으로 쓰인 js 코드나 cdn등을 의미한다.

그러므로 SSR은 TTV와 TTI의 시간이 다르다. 정확히 말하자면 TTV의 시간이 짧고 TTI의 시간이 느리다. rendered HTML을 받기 때문에 곧바로 사용자에게 viewable 하는 화면은 보여주지만, 이후 JS 코드를 다운로드 받아야 본격적인 상호작용이 가능하므로 TTV와 TTI의 사이 시간동안의 괴리가 생긴다. 이 시간의 차이를 어떻게 줄이느냐가 사용자 경험에 중요한 요소이다.

SSR의 장점

1 ) Faster first Page Load Time

위에서 언급하였듯이, 컨텐츠가 들어있는 상태로 첫 HTML 파일을 받게 된다. 즉 사용자가 볼 수 있는 화면은 브라우져에서 HTML 파일을 받음과 동시에 바로 확인할 수 있다. 또한 필요한 리소스만 다운로드 받기에, 모든 리소스들을 다운받아야 확인할 수 있는 CSR의 방식보다 사용자에게 더 빠르게 첫 화면을 보여줄 수 있다.

2 ) SEO(Search Engine Optimization)

Pre-Rendered Page의 연장선상에 해당되는 이야기이다. 각 브라우져의 search machine 들은 HTML 내의 내용을 토대로 (meta 태그 및 body에 정의되는 내용들) 검색 키워드에 맞게 사용자에게 검색결과 화면을 보여준다. 그러므로 컨텐츠의 내용이 정의되서 오는 SSR의 경우 검색 엔진에 최적화시킨 상태로 브라우져가 파일을 인식하게 된다.

SSR의 단점

1 ) TTV < TTI

하지만 SSR이 무조건 UX적 관점에서 도움이 되는 것은 아니다. 오히려 악영향을 끼칠 수도 있다. 예를 들어 사용자와 interaction이 많이 들어간 페이지는 SSR로 구현했을 때 빠르게 viewable한 화면은 볼 수 있으나, interactable한 것은 아니다. 즉, TTV와 TTI가 불일치하기에, 이 사이의 간극동안 유저 입장에서 불쾌한 경험을 선사할 수 있다. interactable한 js 코드가 필요없는 정적 사이트인 경우에는 해당되지 않는다.

2 ) More server-side load, Slower FFTB

결국 서버단에서 처리되는 코드가 늘어나게 되며 서버에 많은 부하를 준다.

Let’s say that it takes you 500ms to SSR your page, that means you can at most do at most 2 requests per second.

서버에서 처리되는 양이 늘어난다는 것은 즉, 서버가 들어온 request를 얼마나 빠르게 쳐내는지와도 관련된다. 대부분 이런 request는 synchronous 하게 진행되므로 traffic이 많은 사이트에 SSR을 무작정 도입하는 것은 엄청난 지연을 일으킬 수 있다.

이는 TTFB(Time To First Byte)와도 관련되어있다. TTFB는 클라이언트 단의 브라우져가 request를 보내고 1 byte의 데이터를 받기까지의 시간을 의미한다. SSR의 경우 서버에서 Html을 렌더링하는 작업을 진행하므로, 일단 빈 HTML을 보내고 클라이언트에서 js를 loading하도록 하는 CSR보다 느릴 수 밖에 없다. 일례로 서버 응답시간이 1초 느려질 때마다, 1000명의 사용자가 빠져나간다는 이야기도 있다.

그러므로 TTFB 자체는 거의 항상 CSR이 SSR보다 빠르다. SSR의 경우 rendered Html을 그려내기 위한 시간이 포함되기 때문이다. 하지만 SSR이 CSR보다 첫 페이지를 받아오는 로딩 속도가 느릴 수도 있다. CSR에 code splitting이 되어 있을 수도 있고, SSR에는 Html을 pre-render 하는 것 이외에 서버단에서 data fetch 등의 작업도 필요할 수 있다. 이런 경우에는 TTFB 자체가 너무 느려져, 나중에 첫 화면을 받아보는 시간이 CSR보다 더 느린 사태가 생길 수 있다.

마지막으로 SSR은 일반적으로 첫 페이지를 로딩하는 속도는 빠르지만, 이후 다른 페이지로 넘어가는 경우에는 CSR보다 느리다. 코드 재사용 없이 받아오고자 하는 페이지를 통째로 다시 요청하기 때문이다.

3 ) Blank page flicker

SSR은 기본적으로 모든 페이지를 한꺼번에 로딩하지 않는다. 즉, 현재 사용자가 필요한 부분의 페이지만을 던져준다. 그러므로 요청마다 서버에서 페이지에 대한 pre-rednered HTML을 요청하게 된다. 그러므로 중간에 해당 파일을 받아올 때까지 blank page가 나타나게 되는데, 이를 우리는 화면 깜빡임이라고 부른다.

이렇게 SSR의 장단점을 야무지게 살펴보았다. 이렇게만 보면 굳이 공들여서 Next.js를 써야하나 싶기도 하다. 하지만 그럼에도 굳건히 Next.js가 React Framework로 자리잡은 이유는, 저런 서버 사이드 렌더링 방식의 단점을 보완한 여러 기능들이 존재하기 때문이다.

pre-rendering in Next.js

Next.js는 React의 Production을 최적화하기 위한 Framework이다. 앞서서 봤을 땐 CSR과 SSR에는 서로 융합되지 못한 채 각각의 장단점들이 명확했다. 하지만 Next.js는 이러한 장단점들을 보완한 기능들을 제공해준다. Next.js는 SPA의 장점을 그대로 들고오면서 SSR을 사용할 수 있는 하이브리드 툴이다.

-1) getStaticProps (feat : SSG)

getStaticProps는 Next.js에서 SSG를 구현할 수 있게 만드는 기능이다. 방금까지 SSR을 설명하다가 SSG는 뭐지?하고 당황할 수 있으리라 생각한다. 하지만 결국엔 똑같은 뿌리를 공유하고 있다는 사실을 확인하게 될 것이다.

SSG는 Static Site Generation, 즉 정적인 사이트를 생성한다는 의미이다. 이런 Static Site를 만드는 tool들을 Static Site Generator라고 하며, 그 중 Next.js가 가장 많은 인기를 받고 있다.

위에서 SSR은 서버단에서 매 요청시마다 pre-rendered 되어 뼈대와 살이 있는 HTML 파일을 받아볼 수 있다고 이야기 하였다. SSG가 정적 사이트라면 SSR의 정의와 어긋나는 것처럼 생각할 수 있다. SSG는 SSR처럼 pre-render된 페이지를 보여주지만, SSG는 변경사항을 업데이트 할 수 없는 SSR이다.

즉 SSG가 SSR과 같이 서버에서 처리되서 client로 전달되는 같은 뿌리를 공유하더라도, SSG는 다음과 같은 특성으로 SSR과 다른 렌더링 방식이 되었다.

Next.js will pre-render this page at build time using the props returned by getStaticProps.

Next.js에서 getStaticProps를 사용하면 "사이트를 배포할 때" 에서 정의하는 props을 사용하여 사이트를 생성하게 된다. 이는 사이트를 배포한 후에는 업데이트가 불가능하다는 이야기이다. 재배포를 하지 않는 이상, getStaticProps를 통해 가져온 데이터는 변하지 않는다. db가 변경되거나 수정된다고 해도 말이다.


출처 : 벨로그

만약 벨로그에 글을 출판해서 request를 던져 db까지 반영이 되었다고 가정해보자. 일반적으로는 출판과 동시에 main page로 redirect 된 후, redirect된 페이지에서 다시 페이지가 render되며 data를 fetch 해온다. 그렇게 되어 db에 새로 생긴 내용까지 가져오면서 사용자는 데이터가 추가된 블로그 글을 보게 된다. 하지만!! getStaticProps로 블로그 글 리스트를 가져오게 된다면 애초에 build time 때의 prop을 그대로 가져가기 때문에 반영되지 않는다.

getStaticProps always runs on the server and never on the client. You can validate code written inside getStaticProps is removed from the client-side bundle with this tool.

getStaticProps는 항상 server 에서만 작동하는 코드이다. 이는 즉 pre-rendered된 HTML이 Client로 보내질 때, 이미 서버 상에서 처리가 되어서 온다는 사실을 알 수 있다. 이는 우리가 전통적인 SSR 방식이 그러하다는 것을 잘 알고 내려왔기에 당연한 사실이다!

그러므로 api call을 통해 data fetch를 하는 부분은 client에서 하는 것이 아니라, 서버에서 이미 끝난 상태로 오게 된다. 또한 getStaticProps에 정의된 코드는 client에서 받아볼 때는 이미 bundle 되어 없어져있기 때문에, 안에 api를 fetch하는 url이 보여지거나 중요 정보가 보여지는 걱정을 하지 않아도 된다.

내가 앞에서 그렇게 대차게 깠지만, Next.js 공식문서에서는 SSG, 즉 getStaticProps를 사용하는 것을 권장하고 있다.

We recommend using Static Generation over Server-side Rendering for performance reasons. Statically generated pages can be cached by CDN with no extra configuration to boost performance. However, in some cases, Server-side Rendering might be the only option.

Build time 이후로 기존 prop을 그대로 사용하는 getStaticProps는 리소스적인 측면에서 경제적이다. 예를 들면 이미 쓴 블로그 글의 경우엔 수정을 하지 않는 이상 바꿀 일이 없다. 그러므로 getStaticProps를 통해 받아올 경우, 새로고침으로 서버에 다시 request를 보낸다한들 build time 때 이미 생성된 Json file(props)을 재사용하게 될 뿐이기에 추가적인 소요가 들지 않는다. 하지만 그렇다면 새로고침하면 props은 그대로 쓰지만 HTML 파일은 요청마다 그대로 받아올까?

This HTML will then be reused on each request. It can be cached by a CDN.

Next.js에서 getStaticProps로 생성된 HTML 파일은 CDN에 의해 캐싱되어 재사용하게 된다. 이로써 추가적인 요청을 하지 않고 캐싱되어 있는 파일을 계속 사용하게 된다. 리소스 낭비를 더욱 줄일 수 있게 되었다. 이러한 기능은 마치 SPA에서 SSR이 가능한 듯 보인다. 실제로 그러한 기능을 위해 탄생한 것이라고 봐도 된다!

When you navigate to a page that’s pre-rendered using getStaticProps, Next.js fetches this JSON file (pre-computed at build time) and uses it as the props for the page component.

getStaticProps에서 내려주는 prop의 경우엔 JSON file의 형태로 caching되어 꺼내쓰게 된다. 이는 미리 build time에 정의된 path의 경우이고, 만약 getStaticPaths에 정의되있지 않은 path로 요청을 보내는 경우엔 fallback 설정을 어떻게 하느냐에 따라 요청을 거부할수도, 혹은 서버에서 새롭게 만들어주도록 할 수도 있다.

만약 현재 존재하는 path를 모두 dynamic하게 생성하도록 한다면, 사용자의 TTFB가 느려질 수 있다.(SSR처럼 HTML 파일을 만들고 getStaticProps에 정의된 코드를 실행해야 한다).

그렇다고 모든 path들을 미리 정의한다면, 페이지 수에 따라 빌드 타임또한 늘어난다...

그러므로 build time과 사용자의 fast loading 사이의 간극을 잘 맞추도록 getStaticPaths에 일정 게시글만 미리 loading을 해놓는다던지, 혹은 dynamic routing이 필요없는 경우엔 fallback을 false로 설정할 수도 있다.


다음은 브라우저에서 메인 홈페이지로 첫 요청을 했을 때의 개발자 도구의 Network 탭이다. 초록색으로 표시된 부분이 바로 Next.js에서 pre-fetching 된 상태로 날라온 HTML 파일이다. getStaticProps로 가져올 경우 앞서 얘기했듯이 build 타임 때의 props을 그대로 들고 오고, 이 HTML 파일은 cdn으로 caching되어 재사용된다. 다음은 refresh를 통해 브라우저에 재요청을 했을 때의 Network Tab이다.

초록색 부분이 현저하게 줄어들었음을 볼 수 있다. 재사용이 주는 이점이라고 할 수 있겠다. 이미 캐싱된 HTML과 JSON file(prop)을 재사용하므로 SSR의 단점 중 하나인 blank page flicker와 서버 부하에 대한 문제를 어느정도 해결할 수 있다.

하지만 그렇게 되면 getStaticProps를 통해 받아온 데이터는 정말로 평생 영영 다시 사이트를 재배포하지 않는 이상 업데이트가 불가능한 것일까? 나중에 ISR에 대해 설명을 하겠지만 가능하게 만드는 방법이 있다!

-2) getServerSideProps (feat : SSR)

getServerSideProps는 Next.js에서 SSR을 구현하는 기능이다. 매 요청마다 완전히 새로운 request를 받는다. (또한 getStaticProps와 달리 캐싱하여 재사용하지 않는다.)

지금까지 CSR과 SSR을 비교설명하다가, Next.js안에 갑자기 SSG는 뭐고 SSR은 뭔지 헷갈릴 수 있을 것이다. 내가 위에서 CSR과 SSR을 비교 설명한 것은 넓은 개념에서 이야기 한것이라고 생각하면 편하겠다.

즉 CSR과 SSR을 비교할 때의 SSR은 그 말대로 서버 사이드 렌더링, 서버에서 pre-rendered된 HTML을 주는 렌더링 방식을 의미하고, Next.js에서 SSG와 SSR을 비교하며 얘기하는 SSR은 위 SSR 개념을 공유하면서 Next.js의 더 좁은 개념을 적용한 "기능"이라고 생각해주길 바란다. 그 기능을 구현하는 장치가 getServerSideProps이다.

Next.js will pre-render this page on each request using the data returned by getServerSideProps.

Next.js에서 SSR은 매 요청시마다 getServerSideProps에 있는 코드를 다시 실행하여 새로운 상태의 props을 받아온 후 이를 반영한 HTML 파일을 내려주게 된다.

GetServerSideProps is similar to getStaticProps, but the difference is that getServerSideProps is run on every request instead of on build time.

Next.js에서 getStaticProps(SSG)와 getServerSideProps(SSR)는 같은 배에 나온 이란성 쌍둥이라고 할 수 있다. 이미 server에서 만들어진 상태로 사용자에게 전달된다는 점은 같지만, prop의 상태가 다르다. SSR의 경우 매 요청으로 최신의 prop을 받아볼 수 있다면, SSG는 build time 때의 prop을 가져간다.

그러므로 SSG는 전통적인 SSR의 단점(서버 부하)을 보완하는 하는 반면, Next.js의 getServerSideProps는 매 요청마다 새로운 Html File을 가져오므로 그 단점을 온몸으로 받아들인 친구이다.

You should use getServerSideProps only if you need to render a page whose data must be fetched at request time.

그러므로 getServerSideProps를 사용할 때에는 요청 시 data가 최신화될 필요성이 있는 부분에 적용되어야 한다. Next.js 공식문서에서는 그러한 부분을 authorization headers or geo location이라고 정의할만큼, data가 유동적으로 바뀌는 페이지에 알맞은 방식이다. 이런 부분에 getStaticProps를 적용하면 같은 data만 계속 보게 될 것이다.

페이지를 새로고침할 때마다 캐싱된 Html file도 없을 뿐더러 prop 최신화를 위해 api call도 실행하게 되어, 위 이미지와 같이 큰 데이터 덩어리를 매 요청마다 받아오게 된다. 그러므로 Next.js 환경상에서도 제한적인 경우에만 사용하는 기능이라고 할 수 있다.

하지만 세상엔 안되는 법은 없나보다. SSR 환경에서도 caching을 할 수는 있다. (참고)
그러므로 필자 생각엔 아예 새로운 요청을 보내는 것이 아니라, cache control의 max-age는 0으로 설정하고 stale-while-revalidation 값을 1~2분정도로 설정한다면 필요한 데이터에 대해서만 재검증 요청을 background에서 진행하고 아예 새로운 request는 막을 수 있어 좀 더 효율적이지 않을까 생각한다.

-3) Advanced getStaticProps (feat : ISR)

실제로 advanced getStaticprops라는 말은 없지만, 향상된 버전의 getStaticProps를 소개하고자 한다. 우리가 지금까지 위에서 getStaticProps의 고질적인 문제점으로 build time 시 생성된 prop으로 HTML file을 만들기 때문에 업데이트가 되지 않는다는 것을 잘 알고 있다. 가령 아무리 일주일에 한 번 게시물을 출판을 해서 업데이트가 꾸준히 필요 없더라도, 그 때 한번 게시물을 올렸을 때 반영은 되어야 할 것이 아닌가! 그렇다고 거기에 getServerSideProps를 적용해버리면 사용자는 업데이트가 되지 않는 상황에서도 새롭게 페이지를 받아와야 하는 리소스 부담이 생긴다.

Next.js will attempt to re-generate the page when a request comes in, and at most once after every revalidate time

이에 Next.js에서는 ISR(Incremental Static Regeneration)이라는, getStaticProps에 SWR(Stale-With-Revalidation)이라는 개념을 도입하였다. 즉 revalidate에 대한 시간을 설정하여 data를 다시 받아와야 하는 여부를 결정하는 것이다.

만약 revalidate를 10초로 설정하면, 사용자는 처음 request를 한 후로 10초간은 SSG의 형태로 caching된 파일을 받고, 10초가 지난 후로 request를 다시 요청하면 마치 SSR처럼 해당 요청에 대해 새로운 상태의 props를 내려준다. SSR과 SSG의 hybrid인 셈이다.

출처: web-dev

다음 사진은 ISR이 등장하기 이전부터, request를 보내는 Header에 설정할 수 있는 속성인 SWR(Stale-With-Revalidation)에 대한 개념 구조도이다. max-age를 1초로, swr을 60초로 설정해놓는다고 가정할 때, data를 막 받아오는 순간인 0~1초는 데이터가 fresh한 상태이다. 이 상태동안은 유효성 검증없이 캐싱된 응답을 그대로 사용한다.

그 이후 1~60초 사이까지는 data가 fresh하지 않고 stale하다고 판단하지만 계속 캐싱된 데이터를 사용한다. 하지만 data가 stale해지는 순간부터, 즉 max-age를 초과하는 순간부터는 브라우져 측에서 캐시된 응답의 사용을 지연시키지 않는 방식으로 revalidation request를 브라우져에서 보내게 된다. 이때 받아온 내용은 같을 수도 있고 다를 수도 있지만, 로컬 캐시에 저장하게 된다.
마지막으로 이후 60초가 지나면 더이상 revalidate를 통해 업데이트된 캐시를 사용하지 않는다. 그러므로 revalidation request가 아니라 네트워크에 아예 새로운 페이지를 받아오기 위한 request를 보낸다. 즉 data를 새로 받아온 후 다시 캐시를 저장한다.

이는 모든 사용자마다 개별로 보내지는 것은 아니다. 예를 들어 사이트에 처음으로 들어온 A라는 사용자가 traffic을 일으키는 순간부터 trigger가 되며, max-age에 정의된 시간을 넘기지 않는 동안 그 사이에 들어온 client 들은 A와 같은 view를 공유하게 된다.

ISR도 SWR의 개념을 그대로 따른다. getStaticProps에 revalidate 속성을 어떻게 정의하느냐에 따라서 해당 응답의 cache-control 속성은 다음과 같이 바뀐다.

s-maxage=REVALIDATE_SECONDS, stale-while-revalidate

s-maxage는 proxy나 cdn 상의 max-age를 의미한다. Next.js는 자동적으로 cdn 상에 캐시를 저장하기 때문에, cache-control에 정의된 속성에 따라 알아서 관리해준다. 다음 캐시와 같이 cdn상에 저장된 캐시의 max-age를 revalidate 속성에 따라 cache-control에 정의해준다. 본인의 생각이지만, stale-while-revalidate 속성이 정의되어 있지 않는 이유는 stale하다고 판단하면 새로운 request를 보내기 위함이라고 생각한다.

하지만 ISR도 한계점이 존재한다. 일단 사용자가 request를 보내서 max-age가 설정되었을 때, data가 stale한 상태로 변하기 전까지의 괴리가 존재한다. server의 부하를 줄이면서도 업데이트가 된다는 점은 분명히 참신하고 좋은 approach이나, serverSideProps처럼 매 request마다 받아보지는 못한다. ..

참고 : data가 revalidate 시간을 지나기 전까지 rendering 되지 않음

그래서 getStaticProps의 revalidate 속성을 더욱 효율적으로 이용할 수 있도록 on-demand 방식이 존재한다. 즉, data가 수정이 되었을 때 revalidate를 하도록 하는 것이다. 이는 webhook처럼, 서버를 폴링하여 물어보는 것이 아니라 서버측에서 event를 제시해주는 것이다. 대충의 워크플로우는 다음과 같다.

  1. server에서 data가 수정될 때 Next.js의 api 폴더에 정의된 route로 revalidate를 trigger한다.

  2. Next.js의 api route에는 revalidate 콜백이 정의되어져 있고, 인자에는 바꾸고자 하는 page path가 들어간다. 활성화가 되면 곧바로 cache에 대한 revalidate가 이루어진다.

이렇게 된다면 사용자들은 revalidate 시간으로 인한 불편함을 겪지 않고 data가 수정될때마다 수정된 data를 받아볼 수 있으면서도, getStaticProps가 주는 이점은 그대로 챙겨갈 수 있다.

If revalidate is omitted, Next.js will use the default value of false (no revalidation) and only revalidate the page on-demand when revalidate() is called.

그러므로 revalidate 속성을 정의하지 않아도 된다. 하지 않으면 무조건 revalidate 콜백이 불러와질 때만 revalidation 요청이 생기게 된다.

이에 대해 잘 정리한 분의 게시글을 첨부하겠다.

끝으로...

정말 방대한 양의 문서를 보고 정리하며 글을 쓰느라 일주일은 걸렸던 것 같다. 위에서 아주 축약해놓은 TL;DR을 썼는데, 이제는 좀 살이 추가된 tl;dr을 써보도록 하겠다.

TL;DR
1. SPA이면 Next.js는 어떤 경우에서든 쓰자. CSR/SSR뿐만 아니라 더 좋은 기능들도 있다.(SSG/ISR)
2. 요청이 잦은 페이지의 경우엔 SSR에 Cache-control 속성을 지정해서 써보자. 그냥 SSR 쓰는 것보다는 경제적인 것 같다.(getStaticProps의 revalidate에 0을 넣는거랑 비슷한 느낌인 것 같다)
3. 되도록이면 모든 data fetch를 할 때 ISR의 on-demand 방식을 써보자. 백엔드측에서 할 일은 늘어나도 가장 효율적인 SSG/SSR의 하이브리드 형식이다.

참고문서

https://umbraco.com/knowledge-base/jamstack/

https://www.cloudflare.com/ko-kr/learning/performance/static-site-generator/

https://snipcart.com/blog/choose-best-static-site-generator

https://wonit.tistory.com/357?category=829651

https://nextjs.org/docs/basic-features/data-fetching/overview

https://vuejs.org/guide/scaling-up/ssr.html#ssr-vs-ssg

https://velog.io/@lky5697/what-the-heck-is-ssg-static-site-generation-explained-with-nextjs#ssg%EC%97%90%EC%84%9C%EC%9D%98-nextjs-%EC%82%AC%EC%9A%A9

https://proglish.tistory.com/216

https://velog.io/@yu2jeong/TTV-Time-To-View-TTI-Time-To-Interact

https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8

https://kokohapps.tistory.com/entry/Nextjs-nextdynamic-%EC%9D%B4%EC%9A%A9%ED%95%B4%EC%84%9C-%ED%8A%B9%EC%A0%95-%EC%BB%B4%ED%8F%AC%EB%84%8C%ED%8A%B8-SSR-%EC%95%88%ED%95%98%EA%B3%A0-%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8%EC%97%90%EC%84%9C%EB%A7%8C-%EB%A0%8C%EB%8D%94%EB%A7%81%ED%95%98%EA%B8%B0

https://d2.naver.com/helloworld/7804182

https://develogger.kro.kr/blog/LKHcoding/133

https://velog.io/@seungchan__y/NextJS%EC%99%80-ISR

https://www.tecforfun.com/frameworks/a-simple-clear-guide-to-on-demand-isr-in-next-js/

https://medium.com/walmartglobaltech/the-benefits-of-server-side-rendering-over-client-side-rendering-5d07ff2cefe8

profile
당신이 본 큰 소나무도 원래 작은 솔방울에 불과했다.

2개의 댓글

comment-user-thumbnail
2023년 1월 20일

와! 정리가 잘 되어 있네요! 참고하겠습니다😁

1개의 답글