최근 patterns.dev 읽으며 디자인 패턴을 공부하고 있다.
디자인 패턴을 간단하게 정리하며 마무리했고 어제는 렌더링 패턴으로 넘어왔는데,
항상 얕게만 알고 있던 이 렌더링 패턴을 파헤쳐 제대로 정리를 좀 해야겠다고 생각이 들었다.
개인적으로 patterns.dev의 내용만으로는 조금 부족하다고 느껴서 추가적인 내용은 여러 곳에서 모아 정리한다.
SSR, CSR, SSG를 설명하려면 JS의 역사부터 꺼내오지 않을 수 없겠지만 ~
거두절미하고 최대한 각각의 개념에만 접근해 보겠다.
Server-Side Rendering
프런트엔드 개발자라면 아주 익숙한 약어인 SPA(Single Page Application)이 있기 전에, MPA(Multi Page Application)이 있었다.
MPA는 여러 개의 HTML 파일로 웹 어플리케이션을 구성하는 방식을 말한다. 이 때 클라이언트에서 서버로 데이터를 전송하려면, form 태그를 이용해서 각 input 내의 데이터를 onSubmit 이벤트로 전송했다. 그리고 이 onSubmit 이벤트는 새로고침을 일으킨다. 화면을 업데이트해서 서버로부터 새로운 데이터를 받아와야하기 때문이다.
이 방식을 SSR(Server-Side Rendering)이라고 하며 사용자의 페이지 요청에 대해 서버에서 모든 처리를 진행하고 전체 HTML을 만들어서 클라이언트로 보내 렌더링하는 것을 말한다. 렌더링 된 컨텐츠에는 데이터 저장소나 외부 API로부터 받은 데이터가 포함되어 있다. 이 때 서버는 각 요청들을 모두 새로운 요청으로 간주하여 처음부터 데이터를 생성한다.
장점
- 렌더링 될 코드를 서버 측에서 실행하여 JavaScript의 크기를 줄이기 때문에 사용자는 화면을 빨리 볼 수 있고 웹사이트의 콘텐츠가 로드되었음을 비교적 빠르게 인지하고 서비스를 이용할 수 있다. (FCP, TTI 감소)
- 같은 이유로 크롤러가 방문했을 때 쉽게 크롤링되어 SEO최적화 수준이 향상된다.
단점
- 모든 프로세스가 서버에 의해 처리되므로 동시에 아주 많은 요청이 들어오거나 네트워크가 느릴 때 서버의 응답이 지연되는 상황이 발생할 수 있다.
- 모든 코드를 클라이언트에서 사용할 수 없으므로 서버와 통신하여 새로운 데이터가 필요한 경우 전체 페이지를 새로고침 해서 다시 응답을 받아야 한다.
이 전체 페이지 새로고침 과정은 말그대로 "새로", "고쳐버려야" 하는 과정이다. 새로 받아오지 않아도 되는 불필요한 데이터도 중복해서 계속 받아왔다. 이를 해결하고자 나오게 된 것이 아래의 CSR이다.
Client-Side Rendering
CSR(Client-Side Rendering)에서 서버는 뼈대가 되는 최소한의 HTML을 최초 한 번만 응답으로 보내 클라이언트에 렌더링한다.
로직, 데이터, 템플릿, 라우팅을 통한 페이지 전환은 모두 브라우저에서 실행되는 JavaScript에 의해 처리된다.
이것으로 인해 웹 애플리케이션의 View단이 최초 로딩 후 서버와 완벽히 분리되었다. 프런트엔드와 백엔드가 구분된 것이다.
어떻게?
1 - 브라우저에서 서버로 웹 페이지 조회 요청을 보낸다. (GET /index.html)
2 - 서버는 <body> 태그 내부가 비어 있는 index.html을 브라우저에게 돌려준다.
3 - 브라우저는 html파일에서 <head> 태그를 읽으며 추가 자원(index.js, index.css 등)을 다시 요청한다.
4 - js, css 파일들을 응답 받은 브라우저는 DOM 트리를 구축하고 CSS를 조합하여 render 트리를 만든다. 이 렌더 트리를 기반으로 요소의 위치를 계산하고(layout/reflow), 화면을 그린다(paint).
5 - 페이지 이동이 필요할 경우에는 기본 HTTP 요청을 막고 브라우저 주소는 변경시킨 다음, 해당 페이지에 알맞은 화면을 DOM API를 통해 다시 그린다.
장점
- 화면을 바꾸는 데 새로고침 없이, 필요한 데이터만 API 호출을 통해 JSON 객체로 받아와서 해당 부분의 데이터만 변경시킨다. 실시간 변경이 가능하다. 불필요한 통신으로 인한 오버헤드를 줄일 수 있으면서 사용자에게는 좋은 UX를 줄 수 있다.
- 페이지간 이동 시 SSR보다 속도가 빨라져 페이지가 좀 더 빨리 반응하는 것 처럼 보인다.
단점
- 사이트의 복잡도가 증가하는 만큼 첫 페이지를 렌더링하기 위해 필요한 JavaScript의 크기 또한 증가한다. 번들의 크기가 클 수록 사용자가 빈 화면을 보게 되는 시간이 증가한다. (FCP, TTI 증가)
- 같은 이유로 렌더링이 늦어져 크롤러가 데이터를 인덱싱할 수 없을 수 있다. SEO최적화를 위한 추가적인 작업을 해 주어야 한다.
웹사이트는 점점 복잡도가 증가하고 JavaScript의 최적화가 필요해진다.
React에서는 변화가 있을 때마다 실제 DOM을 업데이트 하지 않고, 메모리에 올려둔 가상 DOM을 업데이트 한다. 이 변화가 잦을 것을 대비하여 변화를 반영하는 타이밍을 스케줄러를 통해 관리한다. 변화는 스케줄러에 의해 배치로 모아진 다음 적절한 타이밍에 비동기적으로 처리된다.
이를 Reconciliation(재조정) 이라고 하며 나름의 규칙으로 효율적으로 DOM 요소를 교체하는 것을 말한다.
이외에도 개발자들은 JavaScript 번들 크기를 관리하여 초기 페이지 로드 속도를 빠르게 만들거나, 지연 로딩을 활용해 초기에 받아야 하는 코드량을 줄이는 등의 노력을 하고 있다.
하지만 여기까지 내용을 돌이켜 보면,
결국 각각의 장단점이 뚜렷하기 때문에 우리에게는 이 SSR, CSR의 적절한 조화가 필요하다는 것을 알 수 있다.
Universal Rendering
JavaScript는 같은 언어로 서버, 클라이언트 환경을 모두 지원할 수 있다. 하나의 환경에서 SSR과 CSR을 함께 지원하는 것을 Universal Rendering 이라고 부른다.
React의 경우 서버 측 React(react-dom/server)에서 앱을 렌더링하고, 문자열로 변환(serialization)해 클라이언트 측으로 넘겨준다. 클라이언트는 렌더링하며 JavaScript 이벤트 리스너를 연결한다. 이를 hydration이라고 한다.
ReactDOMServer.renderToString(element) 함수는 React 엘리먼트와 대응되는 HTML문자열을 반환한다.
renderToString() 함수는 ReactDOM.hydrate() 함수와 함께 사용된다. 서버에서 렌더링 된 HTML을 그대로 클라이언트에서 쓸 수 있고 클라이언트에서는 이벤트 핸들러들을 등록하면 된다.
Static Site Generator (Next.js)
SSG(Static Site Generator)는 사이트를 빌드할 때 미리 렌더링해 둔 HTML 자체를 서빙한다. 사용자가 접속할 수 있는 각 라우팅 경로에 대응하는 HTML파일들이 미리 생성된다. 이상적으로 클라이언트 측 자바스크립트가 최소화되고 페이지를 다운로드 받자마자 인터렉티브가 가능하게 된다.
- 사이트가 next build 명령을 통해 빌드되면 /pages/about.js 파일은 about.html 파일로 프리렌더되어 /about 경로에 서빙된다.
- 빌드 시점에 데이터를 불러올 필요가 있는 경우 데이터와 HTML로 렌더링 된 템플릿이 병합되어야 한다. 또 페이지는 얼마나 있는지 알기 위해 HTML로 만들어진 페이지에 해당하는 리스트 데이터가 필요하다.
- 각각 대응하는 항목의 링크를 클릭하여 라우팅하게 되어야 하는 상세 페이지 등의 경우에는 빌드 시점에 각각의 데이터를 불러오면서 해당 데이터의 path를 매칭시켜 데이터를 보내준다.
- 이 때 fallback: false 일 경우 path가 없으면 404 페이지를 보여준다.
하지만 SSG는 컨텐츠가 변경될 때 마다 새로 빌드해야 한다. 컨텐츠 변경 이후 배포하지 않으면 이전 컨텐츠가 계속 보여질 수 있다. 따라서 자주 변경되어야 하는 컨텐츠거나 HTML파일이 아주 많은 경우 적합하지 않을 수 있다.
Incremental Static Regenerator (Next.js)
ISR(Incremental Static Regeneration)은 SSG와 SSR을 결합하는 혁신적인 방법을 제공한다. 일부 페이지를 정적으로 생성하면서 해당 페이지의 콘텐츠를 동적으로 업데이트할 수 있다. 웹 페이지를 미리 렌더링하여 정적인 HTML 파일로 제공하면서, 이후에 콘텐츠가 업데이트되면 해당 페이지를 다시 렌더링하고 갱신된 콘텐츠를 제공한다.
- getStaticProps 함수에서 revalidate 옵션을 사용하여 페이지를 얼마나 자주 재생성할지를 설정한다.
- getStaticPaths 함수에서 fallback: 'blocking' 옵션을 사용하여 다른 경로 요청 시 ISR을 사용하여 생성하도록 한다. (optional)
여기까지 정리하고 마무리하는 것이 목표였는데 쓰는 도중 다음 글 숙제가 생겨버렸다.
나도 Next.js를 써본 경험이 예전이라,
그 때는 12버전이어서 열심히 getStaticPaths 함수 삽질하고 있었던 적이 있었는데 ..
몰랐는데 막판에 ISR 개념 정리하려고 블로그 글 읽다 보니
Next.js 최신 버전인 13이상이 되면 Page Router -> App Router가 되면서 Data Fetching 방식이 변경되었다고 한다.
따라서 getStaticProps(), getServerSideProps(), .... 등등의 서버 사이드 API가 더 이상 사용되지 않는다!
그렇다면 내가 정리한 내용도 (Next.js) 이제 새롭게 시작하는 프로젝트에서는 필요가 없다는 것이겠지 ...
하지만 .. 렌더링 개념들은 여전하고 ... 넥스트의 레거시 프로젝트에서는 아직도 쓸... 쓰겠지? 하하
아무튼 최신 버전에서 바뀐 것이 이거 외에도 많다고 알고 있었으니 또 공부해 봐야겠다.
출처:
https://nextjs.org/docs/pages/building-your-application/data-fetching/get-static-paths
https://patterns-dev-kr.github.io/
https://reactnext-central.xyz/blog/understanding-isr-in-nextjs
프리온보딩 프론트엔드 챌린지 (23년 7월) 오종택 강사님 강의자료