patterns.dev 렌더링 패턴 소개
이 글은 https://patterns.dev 에서 제공하는 글인 https://www.patterns.dev/posts/rendering-introduction/ 글을 번역한 글 입니다.
들어가면서
들어가면서
새로운 웹 앱을 만들기 시작할때에, 가장 기본적으로 드는 질문은 이것일 것입니다. "어떻게 그리고 어디에 내가 보여줄 컨텐츠를 보여주고 싶은가?" 웹 서버가 되어야 하는가? 아니면 빌드 서버? 아니면 엣지? 아니면 클라이언트에 직접적으로? 한번에 보여줘야 하는가? 부분적으로? 점진적으로?
이 중요한 결정의 답은 실제 사용되는 케이스에 의해 결정 됩니다. 가장 최적화된 렌더링 패턴을 선택하는 것은 엔지니어링 팀을 위한 개발자 경험(DX)과 당신이 엔드 유저를 위해 디자인한 유저 경험(UX)에 새로운 세계를 만들것 입니다.
올바른 패턴을 선택하는 것은 더 빠른 빌드와 작은 프로세싱 비용으로 훌륭한 로딩 성능으로 이끌것 입니다. 반대로, 잘못된 패턴의 선택은 좋은 비즈니스 적인 아이디어를 실현 할 수 있는 앱을 죽이게 될것입니다. 따라서 당신이 가진 모든 혁신적인 아이디어들은 적절한 렌더링 패턴과 함께 개발단으로 실현되어야 합니다.

출처:
https://www.patterns.dev/posts/rendering-introduction/
렌더링 패턴의 중요성
훌륭한 UX를 만들기 위해서는, Core Web Vitals (CWV)와 같이 유저 기반의 수치에 최적화를 하려고 대개 노력합니다. CWV 수치 값들은 가장 유저 경험과 밀접한 값들을 측정합니다. CWV를 최적화 하는것은 우리 앱의 훌륭한 UX와 최적화된 SEO를 가져다 줄 수 있습니다.

출처:
https://www.patterns.dev/posts/rendering-introduction/
엔지니어링 팀을 위한 훌륭한 DX를 만들기 위해서는, 더 빠른 빌드 시간, 쉬운 롤백, 증축 가능한 인프라, 그리고 그 밖에 개발자들이 목표에 도달 할수 있게 하는 많은 다른 기능들을 포함한 개발 환경들을 최적화 해야 합니다.

출처:
https://www.patterns.dev/posts/rendering-introduction/
이러한 규율에 맞추어 개발환경을 셋업 하는 것은 우리 개발팀에게 훌륭한 제품 효율성을 가져다 줄것입니다.
우리의 기대를 요약하면서, 꽤나 긴 리스트를 구축해 왔습니다. 하지만, 올바른 렌더링 패턴을 선택한다면, 대부분의 이러한 이점들을 실제로 이용할 수 있습니다.

출처:
https://www.patterns.dev/posts/rendering-introduction/
패턴 고르기
서버사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR)에서부터 여러 다른 커뮤니티들에서 논의되고 실험되고 있는 고도로 발전된 패턴들을 다루어오면서, 렌더링 패턴은 긴 역사를 가지고 있습니다. 이러한 것들이 조금 과할 수 있지만, 우리는 모든 패턴들은 특정한 사용 케이스들에 맞추어 디자인 되었다는 것을 기억해야 합니다. 하나의 사용 케이스를 위한 특유의 이점은 다른 사용케이스들에서는 단점이 될수 있습니다. 또한 하나의 웹사이트만 하더라도 다른 여러 종류의 페이지들을 갖고 있어서 다른 렌더링 패턴들을 적용해야 하기도 합니다.
크롬 팀은 개발자들에게 full rehydration 방법론을 말미암아 정적 혹은 서버사이드 렌더링을 추천해 왔습니다. 시간이 가면 갈수록, 점진적인 로딩과 렌더링 기술들이 당연하게 성능의 좋은 밸런스를 잡아주는데 도움이 되었고 모던 프레임워크의 새로운 기능들로 전달되어 왔습니다.
다음으로 이어질 챕터들에서는 다른 패턴들을 알아봅니다 - 옛 방식과 요즘 방식을 자세히 말이지요. 하지만 그전에, 어디에서 제일 잘 쓰일수 있는지 당신을 이해시키기 위해 그중 몇몇을 간단히 소개하겠습니다.
Static Rendering (정적 렌더링)
정적 렌더링은 간단하지만 파워풀한 패턴입니다. 거의 즉시적으로 페이지를 로드하는 빠른 웹사이트를 구축하기 위해 이 패턴을 쓸수 있습니다.
정적 렌더링에서는, 전체적인 페이지를 위한 HTML이 빌드 타임에 생성되며 다음 빌드 전까지는 변화하지 않습니다. HTML 컨텐츠들은 정적이며 CDN이나 Edge Network를 통해서 쉽게 캐싱될수 있습니다. CDN들은 빠르게 캐싱된 pre-rendered된 HTML을 클라이언트에게 가져다 줄수 있습니다. 그들이 특정한 페이지를 요청할때 말이지요. 이러한 방법은 서버사이드 렌더링에서 이루어지는 특정 요청, 요청에서 받아온 HTML 컨텐츠를 보여주기 까지의 시간들을 굉장히 단축시킬수 있습니다.
위에서 설명된 프로세스는 자주 변하지 않고 그 누구가 요청을해도 같은 데이터를 보여주는 페이지들에게 굉장히 최적화 되어있습니다. 요즘에는 웹사이트에서 굉장히 동적이며 커스터마이징 된 데이터를 보여주기 때문에 정적 렌더링에서도 다른 사용케이스들을 쓸 수 있습니다.

출처:
https://www.patterns.dev/posts/rendering-introduction/
Basic/Plain Static Rendering (기본적이며 간단한 정적 렌더링)
정적 렌더링에도 많은 종류의 변형들이 있으므로, 좀전에 다룬 주된 기술을 Plain Static Rendering 이라고 부릅시다. 동적인 컨텐츠가 없는 페이지에서 사용할수 있습니다.
따라서 나올 부동산 관련 웹사이트 데모 페이지는 언제나 같은 컨텐츠를 똑같이 누구에게나 보여줍니다. 아무런 동적인 데이터도 없으며 개인화된 데이터도 없습니다.
사이트가 배포되고 빌드 될때 (예를들어, Vercel) 연관된 HTML이 생성되고 서버에 정적인 스토리지에 보관됩니다.
유저가 페이지를 요청하면, 서버는 이미 생성된 HTML을 클라이언트에 전송합니다. 이런 응답은 또한 사용자와 가장 가까운 엣지 로케이션에 캐싱됩니다. 브라우저는 HTML을 보여주고 페이지를 동작하게 하기 위해 자바스크립트 번들을 실행합니다.

플레인 정적 렌더링은 성능에 좋습니다. 굉장히 빠른 TTFB를 가져다 주기 때문이지요. HTML이 이미 서버에서 준비되어있기 때문에 브라우저는 더 빠른 응답을 받을 수 있으며 빠르게 보여줄 수 있습니다. 빠른 FCP와 LCP를 가져다 주면서 말이지요. 컨텐츠가 정저기익 때문에 그리는 동안 레이아웃 쉬프팅도 없습니다.

그러므로, 플레인 정적 렌더링은 캐싱을 사용하는 CDN에 특히나 사용되며, 훌륭한 Core Web Vitals를 이루도록 도와줍니다. 그러나, 대부분의 웹사이트들은 최소한 몇개라도 동적인 컨텐츠들을 갖고 있으며 유저 인터랙션도 필요합니다.
클라이언트 사이드의 fetch를 이용한 정적 렌더링
우리의 부동산 데모 사이트를 개선하고 싶다고 해봅시다. 가장 최근에 리스팅된 것을 보여주기 위해서죠. 우리는 이러한 리스팅들을 만들기 위해서 데이터 프로바이더를 이용해야 합니다.

우리는 이 케이스에서 클라이언트 사이드의 fetch를 이용한 정적 렌더링을 이용할 수 있습니다. 이 패턴은 매 request마다 데이터를 업데이트 하고 싶을때 쓰면 좋습니다.

여전히 정적 렌더링을 쓸수 있으며 Skeleton Component와 같은 UI를 렌더 할 수 있습니다. 동적인 리스트 데이터를 위치하고 싶은곳에 말이지요. 그리고, 페이지가 로드되면 데이터를 fetch 할수 있습니다.(예시에선 SWR을 사용)

커스텀 API route가 CMS로부터 데이터를 fetch하고 이 데이터를 반환하기 위해 사용되었습니다.
미리 생성된 HTML파일은 유저가 페이지를 요청할때에 클라이언트로 전송됩니다. 유저는 처음으로는 Skeleton UI를 데이터 없이 마주하게 됩니다. 클라이언트는 데이터를 API route를 통해서 요청하게 되고, 응답을 받고 리스팅을 보여줍니다. (예시에서는 hydration call은 포함되어 있지 않습니다)

클라이언트 사이드의 fetch를 이용한 정적 렌더링이 우리에게 좋은 TTFB와 FCP를 주는 동안, FCP는 살짝 덜 최적화 되었죠. 왜냐하면 "largest content"가 우리가 리스팅 데이터를 API route를 통해 가져와야만 보여지기 때문이죠.

또한 높은 가능성으로 레이아웃 쉬프팅이 일어날수 있습니다. 만약 스켈레톤 UI의 크기가 결국 보여질 컨텐츠들의 크기와 맞지 않다면 말이죠.
또 다른 단점은 이러한 방법론은 더 높은 서버 비용이 들게 할수 있습니다. 우리가 API route를 매 페이지 요청마다 부르기 때문이지요.
Next.js는 몇가지 해결책을 제시합니다. 다음에 이어질 몇가지 섹션들에서 앱이 다이나믹한 데이터를 다룰때에 성능을 향상 시킬수 있는 방법을 논의할겁니다.
getStaticProps를 사용한 정적 방법

이 방법론은 당신을 데이터 프로바이더에게 접근할수 있게 하고 빌드 타임에 서버에서 데이터를 페치할수 있게 합니다. 이것은 빌드타임에 언제나 당신이 원하는 동적인 데이터가 존재 할때에 좋은 방법입니다.

getStaticProps 메서드를 통해서 우리는 HTML을 데이터와 함께 서버에서 제공할수 있습니다. 그러므로, 클라이언트에서 API 를 페치하는 route를 만들지 않아도 됩니다. 비슷하게, 스켈레톤 컴포넌트는 데이터가 로드되기 전에 필수가 아닙니다. 왜냐면 페이지 자체가 데이터와 함께 바로 제공될 것이기 때문이지요.
프로젝트를 빌드 할때에, 데이터 프로바이더가 콜 되며, 반환된 데이터는 생성된 HTML을 통하게 됩니다.
유저가 페이지를 요청할때에, 프로세스는 플레인 정적 렌더링과 비슷합니다. 응답이 캐싱되며 스크린에 보여집니다. 그리고 브라우저는 자바스크립트 번들을 fetch하게 되고 페이지를 hydration 시킵니다.

클라이언트의 관점에서 네트워크와 메인쓰레드가 플레인 정적 렌더링과 동일합니다. 그래서 우리는 비슷한 굉장한 성능을 얻게됩니다.

사이트가 커가면서, 우리가 이러한 방법을 쓸때에 DX가 좋진 않습니다.
수백개의 페이지를 가진 사이트들에서 (블로그 사이트와 같이) 정적으로 빌드된다면 getStaticProps 메서드는 반복적으로 불릴 것이며 긴 빌드타임을 반환하게 됩니다. 외부의 API를 사용한다면, 리퀘스트 리밋을 넘어서거나 엄청난 비용을 지불하게 될거에요.
이 방법은 또한 빌드타임에서 비교적 자주 데이터를 새롭게 하지 않아도 될때에만 좋습니다. 잦은 데이터의 업데이트는 우리가 사이트를 자주 재 빌드 하고 재 배포해야 한다는 것을 의미하니까요.
Incremental Static Regeneration (점진적인 정적 재 생성)

우리는 이러한 빌드타임 이슈와 동적 데이터 이슈를 해결하기 위해서 점진적인 정적 재 생성을 사용합니다.
ISR은 유저가 요청하면 동적 페이지를 렌더링 하기도 하고 정적 페이지만을 렌더링 하기도 하는 하이브리드입니다. 이것은 더 적은 빌드 횟수와 자동화된 캐시 invalidation과 특정 인터벌 뒤의 페이지 재생성을 가져다 줍니다.

우리의 이전의 데모를 이제는 각각의 디테일한 정보를 보여주기 위해서 개발한다고 칩시다. 우리는 이러한 새로운 페이지들을 pre-render할수 있습니다. 유저가 리스트들을 클리했을때 빨리 로드하기 위해서죠.

Next.js는 getStaticProps를 사용하여 동적인 path들을 만들도록 달성하게 도와줍니다. 쿼리 파라미터를 통해 우리는 Next.js에게 어떤 페이지들을 먼저 생성할지 알려줄수 있습니다.
우리의 데모에서 모든 리스트들을 fetch하고 각각의 페이지들을 먼저 생성한다고 해봅시다. 이것은 수천개의 리스트가 있다면 굉장히 오래걸리는 일입니다. 그러한 케이스에서 Next에게 모든 페이지의 부분만 먼저 생성하라고 하고 남은 리스팅 페이지들이 요청에 의해서 생성될때에 fallback을 보여주게 할수있습니다. (유저가 요청할때에만)

미리 렌더링되고 생성된 온디맨드 페이지들은 비슷하게 전달됩니다. 유저가 미리 생성되지 않은 페이지를 요청한다면, 요청이 왔을때에만 생성이 되고 Edge에 의해서 캐싱됩니다. 그러므로 처음으로 요청한 유저만이 조금 느린 속도를 경험할 것입니다. 다른 모든 사람들은 이점을 갖게 될것입니다. 캐싱으로 말이지요.
긴 빌드타임 이슈는 해결이 된듯 합니다. 하지만 우리는 아직도 우리가 새로운 리스트를 추가할때마다 재배포를 해야하는 랜딩페이지가 남아있네요.

랜딩페이지를 리프레시 하기 위해서는, 캐시를 자동적으로 invalidate 할수 있습니다. 그리고 특정 간격에 따라서 백그라운드에서 페이지를 다시 생성할수 있지요. 우리는 이것을 revalidate 필드를 통해서 사용할수 있습니다.
만약 유저가 특정시간보다 더 오래 캐싱된 페이지를 요청했다면, 유저는 처음으로는 stale page를 보게됩니다. 페이지 재 생성은 즉시 실행됩니다. 페이지가 백그라운드에서 재생성되었다면, 캐시는 사라지고 최근에 재생성된 페이지로 업데이트 됩니다.

ISR로 인해서 우리는 다이나믹한 컨텐츠들을 자동적으로 새로 만들어주는 이점을 볼수 있습니다.
비록 이 방법이 이전에 다루었던 방법보다 이미 굉장한 성능 향상임에도, 몇가지 살펴봐야 할것이 남아있습니다. 우리의 컨텐츠들은 사실 우리가 설정한 시간간격보다는 훨씬 적게 업데이트 됩니다. 이것은 불필요한 페이지 재생성과 캐싱 invalidation을 유발 할것이에요. 이러한 현상이 나올때마다, 우리는 우리의 serverless function들을 다시 부르게 되고 높은 서버 비용이 나오게 하죠.
On-demand Incremental Static Regeneration (온디맨드 ISR)

우리가 마지막으로 언급한 문제점을 해결하기 위해서 우리는 ISR를 사용한 온디맨드 ISR이 있습니다. ISR이지만 재생성이 고정된 인터벌이 아닌 특정 이벤트에서만 작동합니다.
