Netlify와 Next.js로 부피가 큰 빌드 분해하기

게시 됨: 2022-03-10
빠른 요약 ↬ 정적 생성은 성능에 매우 좋습니다. 앱이 너무 커져 빌드 시간이 한계에 다다를 때까지입니다. 오늘 우리는 Netlify의 새로운 온디맨드 빌더가 이를 어떻게 고칠 수 있는지 살펴보겠습니다. 또한 최고의 사용자 및 개발자 경험을 위해 Next.js의 Incremental Static Regeneration과 함께 사용합니다. 그리고 물론 그 결과를 벤치마킹하십시오!

정적으로 생성된 웹사이트로 작업할 때의 가장 큰 고통 중 하나는 앱이 성장함에 따라 빌드 속도가 점점 느려지는 것입니다. 이것은 스택이 어느 시점에서 직면하게 되는 피할 수 없는 문제이며 작업하는 제품의 종류에 따라 다른 지점에서 발생할 수 있습니다.

예를 들어 앱에 배포 아티팩트를 생성할 때 여러 페이지(보기, 경로)가 있는 경우 각 경로는 파일이 됩니다. 그런 다음 수천 명에 도달하면 미리 계획할 필요 없이 언제 배포할 수 있는지 궁금해하기 시작합니다. 이 시나리오는 이미 웹의 큰 부분이지만 전부는 아닌 전자 상거래 플랫폼이나 블로그에서 일반적입니다. 그러나 경로가 유일한 병목 현상은 아닙니다.

리소스를 많이 사용하는 앱도 결국 이 전환점에 도달합니다. 많은 정적 생성기가 자산 최적화를 수행하여 최상의 사용자 경험을 보장합니다. 빌드 최적화(증분 빌드, 캐싱, 곧 제공)가 없으면 이 또한 결국 관리할 수 없게 됩니다. 웹사이트의 모든 이미지를 살펴보는 것에 대해 생각해 보십시오. 크기 조정, 삭제 및/또는 새 파일 생성이 계속 반복됩니다. 모든 작업이 완료되면 Jamstack이 콘텐츠 전송 네트워크 의 가장자리에서 앱을 제공한다는 것을 기억하십시오. 따라서 우리는 여전히 컴파일된 서버에서 네트워크 가장자리로 항목을 이동해야 합니다.

Jamstack 일반 서비스 아키텍처
Jamstack 일반 서비스 아키텍처(큰 미리보기)

무엇보다도 데이터가 종종 동적이라는 사실도 있습니다. 즉, 앱을 빌드하고 배포할 때 몇 초, 몇 분 또는 심지어 한 시간이 걸릴 수도 있습니다. 한편, 세상은 계속 돌고 있고, 다른 곳에서 데이터를 가져오는 경우 앱은 구식이 될 수밖에 없습니다. 받아들일 수 없다! 업데이트를 위해 다시 빌드하세요!

한 번 빌드, 필요할 때 업데이트

대용량 빌드 해결은 기본적으로 모든 Jamstack 플랫폼, 프레임워크 또는 서비스에서 한동안 최우선 과제였습니다. 많은 솔루션이 증분 빌드를 중심으로 합니다. 실제로 이것은 빌드가 현재 배포에 대해 수행하는 차이만큼 부피가 커질 것임을 의미합니다.

하지만 diff 알고리즘을 정의하는 것은 쉬운 일이 아닙니다. 최종 사용자 가 실제로 이러한 개선의 이점을 얻으려면 고려해야 하는 캐시 무효화 전략이 있습니다. 간단히 말해서 변경되지 않은 페이지나 자산에 대한 캐시를 무효화하고 싶지 않습니다.

Next.js는 Incremental Static Regeneration( ISR )을 제안했습니다. 본질적으로 이것은 각 경로에 대해 얼마나 자주 다시 빌드하기를 원하는지 선언하는 방법입니다. 내부적으로는 서버 측의 많은 작업을 단순화합니다. 모든 경로(동적이든 아니든)는 특정 시간 프레임이 주어지면 자체적으로 재구성되고 모든 빌드에서 캐시를 무효화한다는 Jamstack 공리와 완벽하게 맞습니다. max-age 헤더라고 생각하지만 Next.js 앱의 경로에 대한 것입니다.

애플리케이션을 시작하려면 구성 속성만 있으면 ISR이 시작됩니다. 경로 구성 요소( /pages 디렉토리 내부)에서 getStaticProps 메소드로 이동하여 반환 객체에 revalidate 키를 추가합니다.

 export async function getStaticProps() { const { limit, count, pokemons } = await fetchPokemonList() return { props: { limit, count, pokemons, }, revalidate: 3600 // seconds } }

위의 스니펫은 내 페이지가 매시간 다시 빌드되고 더 많은 포켓몬이 표시되도록 가져옵니다.

우리는 여전히 때때로 대량 빌드를 받습니다(새 배포를 발행할 때). 그러나 이를 통해 콘텐츠를 코드에서 분리할 수 있습니다. 콘텐츠를 콘텐츠 관리 시스템 (CMS)으로 이동하면 애플리케이션의 크기에 관계없이 몇 초 안에 정보를 업데이트할 수 있습니다. 오타 업데이트를 위한 웹훅은 이제 그만!

주문형 빌더

Netlify는 최근에 Next.js용 ISR을 지원하는 접근 방식인 온디맨드 빌더를 출시했지만 Eleventy 및 Nuxt를 포함한 프레임워크에서도 작동합니다. 이전 세션에서 우리는 ISR이 빌드 시간을 단축하고 사용 사례의 상당 부분을 해결하기 위한 훌륭한 단계임을 확인했습니다. 그럼에도 불구하고 다음과 같은 주의 사항이 있었습니다.

  1. 전체 구축은 지속적인 배포를 기반으로 합니다.
    증분 단계는 배포 데이터에 대해서만 발생합니다. 코드를 점진적으로 배송할 수 없습니다.
  2. 증분 빌드는 시간의 산물입니다.
    캐시는 시간 기준으로 무효화됩니다. 따라서 코드에 설정된 재검증 기간에 따라 불필요한 빌드가 발생하거나 필요한 업데이트가 더 오래 걸릴 수 있습니다.

Netlify의 새로운 배포 인프라를 통해 개발자는 앱의 어떤 부분이 배포를 기반으로 구축되고 어떤 부분이 지연될 것인지(그리고 어떻게 연기될 것인지) 결정하는 로직을 생성할 수 있습니다.

  • 비판적인
    조치가 필요하지 않습니다. 배포하는 모든 것은 push 기반으로 구축됩니다.
  • 연기
    앱의 특정 부분은 배포 시 빌드되지 않으며 첫 번째 요청이 발생할 때마다 요청 시 빌드되도록 연기된 다음 해당 유형의 다른 리소스로 캐시됩니다.

온디맨드 빌더 작성

우선 프로젝트에 netlify/functions 패키지를 devDependency 로 추가합니다.

 yarn add -D @netlify/functions

완료되면 새 Netlify 함수를 생성하는 것과 동일합니다. 특정 디렉토리를 설정하지 netlify/functions/ 로 이동하여 빌더에 대한 임의의 이름의 파일을 생성하십시오.

 import type { Handler } from '@netlify/functions' import { builder } from '@netlify/functions' const myHandler: Handler = async (event, context) => { return { statusCode: 200, body: JSON.stringify({ message: 'Built on-demand! ' }), } } export const handler = builder(myHandler)

위의 스니펫에서 볼 수 있듯이 주문형 빌더는 builder() 메서드 내에서 핸들러를 래핑하기 때문에 일반 Netlify 함수와 분리됩니다. 이 메서드는 함수를 빌드 작업에 연결합니다. 그리고 이것이 필요할 때만 빌드를 위해 애플리케이션의 일부를 연기하는 데 필요한 전부입니다. 처음부터 작은 증분 빌드!

Netlify의 Next.js

Netlify에서 Next.js 앱을 빌드하려면 일반적으로 더 나은 경험을 위해 추가해야 하는 2개의 중요한 플러그인이 있습니다. Netlify 플러그인 캐시 Next.js와 Essential Next-on-Netlify입니다. 전자는 NextJS를 더 효율적으로 캐시하므로 직접 추가해야 하지만 후자는 Netlify에 더 잘 맞도록 Next.js 아키텍처가 구축되는 방식을 약간 조정하고 기본적으로 Netlify가 식별할 수 있는 모든 새 프로젝트에서 사용할 수 있습니다. Next.js를 사용합니다.

Next.js를 사용한 온디맨드 빌더

성능 구축, 성능 배포, 캐싱, 개발자 경험. 이것들은 모두 매우 중요한 주제이지만, 너무 많고 적절하게 설정하는 데 시간이 걸립니다. 그런 다음 사용자 경험 대신 개발자 경험에 초점을 맞추는 것에 대한 오래된 토론으로 이어집니다. 잊혀지기 위해 백로그의 숨겨진 지점으로 이동하는 시간입니다. 설마.

Netlify가 지원합니다. 몇 단계만 거치면 Next.js 앱에서 Jamstack의 모든 기능을 활용할 수 있습니다. 이제 소매를 걷어붙이고 모든 것을 한데 모아야 할 때입니다.

사전 렌더링된 경로 정의

이전에 Next.js 내에서 정적 생성을 사용한 적이 getStaticPaths 메서드에 대해 들어본 적이 있을 것입니다. 이 방법은 동적 경로(광범위한 페이지를 렌더링하는 페이지 템플릿)를 위한 것입니다. 이 방법의 복잡성에 대해 너무 많이 생각하지 않고, 반환 유형이 2개의 키가 있는 객체라는 점에 유의하는 것이 중요합니다. 예를 들어 Proof-of-Concept에서 이것은 [Pokemon]동적 경로 파일이 될 것입니다.

 export async function getStaticPaths() { return { paths: [], fallback: 'blocking', } }
  • paths 는 사전 렌더링될 이 경로와 일치하는 모든 경로를 수행하는 array 입니다.
  • fallback 에는 3가지 가능한 값이 있습니다: blocking, true 또는 false

우리의 경우 getStaticPaths 는 다음을 결정합니다.

  1. 경로는 미리 렌더링되지 않습니다.
  2. 이 경로가 호출될 때마다 대체 템플릿을 제공하지 않고 요청 시 페이지를 렌더링하고 사용자를 기다리게 하여 앱이 다른 작업을 수행하지 못하도록 차단 합니다.

주문형 빌더를 사용할 때 대체 전략이 앱의 목표를 충족하는지 확인하십시오. 공식 Next.js 문서: 대체 문서가 매우 유용합니다.

온디맨드 빌더 이전에는 getStaticPaths 가 약간 달랐습니다.

 export async function getStaticPaths() { const { pokemons } = await fetchPkmList() return { paths: pokemons.map(({ name }) => ({ params: { pokemon: name } })), fallback: false, } }

우리는 우리가 갖고자 하는 모든 포켓몬 페이지의 목록을 수집하고 모든 pokemon 객체를 포켓몬 이름이 있는 string 에 매핑하고 이를 전달하는 { params } 객체를 getStaticProps 에 전달했습니다. 경로가 일치하지 않는 경우 Next.js가 404: Not Found 페이지를 발생시키길 원했기 때문에 fallbackfalse 로 설정되었습니다.

Netlify에 배포된 두 버전을 모두 확인할 수 있습니다.

  • 온디맨드 빌더 사용: 코드, 라이브
  • 완전 정적 생성: 코드, 라이브

이 코드는 Github에 오픈 소스로 제공되며 빌드 시간을 확인하기 위해 쉽게 배포할 수 있습니다. 그리고 이 대기열을 사용하여 다음 주제로 넘어갑니다.

빌드 시간

위에서 언급했듯이 이전 데모는 실제로 Proof-of-Concept 이며 측정할 수 없는 경우 실제로 좋거나 나쁜 것은 없습니다. 우리의 작은 연구를 위해 PokeAPI로 가서 모든 포켓몬을 잡기로 결정했습니다.

재현성을 위해 요청을 1000 으로 제한했습니다. 이것들이 실제로 API 내에 있는 것은 아니지만 특정 시점에 업데이트되는지 여부에 관계없이 모든 빌드에 대해 페이지 수가 동일하도록 강제합니다.

 export const fetchPkmList = async () => { const resp = await fetch(`${API}pokemon?limit=${LIMIT}`) const { count, results, }: { count: number results: { name: string url: string }[] } = await resp.json() return { count, pokemons: results, limit: LIMIT, } }

그런 다음 기본적으로 동일한 환경에서 공존할 수 있는 미리 보기 배포 덕분에 Netlify에 대해 별도의 분기에서 두 버전을 모두 실행했습니다. 두 방법의 차이점을 실제로 평가하기 위해 ODB 접근 방식은 극단적이었고 해당 동적 경로에 대해 사전 렌더링된 페이지가 없었 습니다. 실제 시나리오에는 권장되지 않지만(트래픽이 많은 경로를 미리 렌더링해야 함) 이 접근 방식으로 달성할 수 있는 빌드 시간 성능 향상 범위를 명확하게 표시합니다.

전략 페이지 수 자산 수 빌드 시간 총 배포 시간
완전 정적 생성 1002 1005 2분 32초 4분 15초
주문형 빌더 2 0 52초 52초

우리의 작은 PokeDex 앱의 페이지는 매우 작고 이미지 자산은 매우 희박하지만 배포 시간의 이점은 매우 중요합니다. 앱에 중간에서 많은 양의 경로가 있는 경우 ODB 전략을 고려할 가치가 있습니다.

배포를 더 빠르게 만들어 더 안정적으로 만듭니다. 성능 적중은 첫 번째 요청에서만 발생하며, 후속 요청부터 렌더링된 페이지는 Edge에서 바로 캐시되어 성능이 완전 정적 생성과 정확히 동일합니다.

미래: 분산 영구 렌더링

같은 날 온디맨드 빌더가 발표되고 얼리 액세스가 제공되었으며 Netlify는 DPR(Distributed Persistent Rendering)에 대한 의견 요청도 게시했습니다.

DPR은 온디맨드 빌더의 다음 단계입니다. 이러한 비동기식 빌드 단계를 사용한 다음 실제로 업데이트될 때까지 자산을 캐싱하여 더 빠른 빌드를 활용합니다. 더 이상 10k 페이지의 웹사이트에 대한 전체 빌드가 없습니다. DPR은 개발자에게 견고한 캐싱 및 온디맨드 빌더 사용을 통해 시스템 구축 및 배포에 대한 전체 제어 권한을 부여합니다.

이 시나리오를 상상해 보십시오. 전자 상거래 웹 사이트에 10,000개의 제품 페이지가 있습니다. 이는 배포를 위해 전체 애플리케이션을 빌드하는 데 약 2시간이 걸린다는 것을 의미합니다. 우리는 이것이 얼마나 고통스러운지 논쟁할 필요가 없습니다.

DPR을 사용하면 모든 배포에서 빌드할 상위 500페이지를 설정할 수 있습니다. 트래픽이 가장 많은 페이지는 항상 사용자를 위해 준비되어 있습니다. 그러나 우리는 상점입니다. 즉, 매초가 중요합니다. 따라서 다른 9500페이지의 경우 빌드 후 후크를 설정하여 빌더를 트리거할 수 있습니다. 즉, 나머지 페이지를 비동기식으로 즉시 캐싱하여 배포합니다. 어떤 사용자도 다치지 않았고 우리 웹사이트는 가능한 가장 빠른 빌드로 업데이트되었으며 캐시에 존재하지 않는 다른 모든 것은 그런 다음 저장되었습니다.

결론

이 기사의 많은 논의 사항이 개념적이며 구현이 정의되어야 하지만 Jamstack의 미래가 기대됩니다. 커뮤니티로서 우리가 하고 있는 발전은 최종 사용자 경험을 중심으로 이루어집니다.

Distributed Persistent Rendering에 대해 어떻게 생각하십니까? 애플리케이션에서 온디맨드 빌더를 사용해 보셨습니까? 댓글로 자세히 알려 주시거나 트위터로 전화주세요. 정말 궁금하다!

참고문헌

  • "Next.js를 사용한 증분 정적 재생(ISR)에 대한 완전한 가이드", Lee Robinson
  • "온디맨드 빌더를 사용하여 Netlify의 대규모 사이트를 위한 더 빠른 빌드", Netlify 블로그 Asavari Tayal
  • "분산 영구 렌더링: 더 빠른 빌드를 위한 새로운 Jamstack 접근", Netlify 블로그 Matt Biilmann
  • "분산 영구 렌더링(DPR)", Cassidy Williams, GitHub