Turbopack이 자바스크립트를 청크로 나누는 방법

TMT

https://nextjs.org/blog/turbopack-chunking

이 페이지에서 네트워크 탭을 열어 보면 아무렇게나 붙인 것처럼 보이는 이름의 자바스크립트 파일 목록이 나옵니다.

브라우저 네트워크 패널. 이 페이지가 로드한 자바스크립트 청크 목록입니다.

그중 하나를 클릭해서 압축을 풀어 보면 이런 코드가 나옵니다.

(globalThis.TURBOPACK || (globalThis.TURBOPACK = [])).push([
  "object" == typeof document ? document.currentScript : void 0,
,
  (e) => {
    "use strict";
    function r() {
      for (var e, r, o = 0, t = "", l = arguments.length; o < l; o++) {
        // ...
      }
      return t;
    }
    e.s(["clsx", 0, r, "default", 0, r]);
  },
,
  (e) => {
    "use strict";
    var r = e.i(7284);
    let o = function () {
      // ...
    };
  },
]);
 
//# debugId=e60a5ad0-3ac6-bb3a-e12b-6130e99fb7a6

이것이 바로 청크입니다. Turbopack은 여러분의 Next.js 앱을 위해 이런 청크를 수십 개 만들어 내는데, 그 안에는 여러분이 직접 쓴 코드, 의존하는 패키지, 그리고 이 모두를 연결해 주는 런타임이 담깁니다.

'청킹'은 어떤 코드를 어떤 청크에 넣을지 정하는 과정입니다. 청킹하는 방법은 여러 가지이고, 방법마다 내주는 것과 얻는 것이 다릅니다.

가장 단순한 방법부터 보겠습니다. 앱이 쓰는 자바스크립트 전부를 청크 하나에 담는 방식입니다.

앱 전체를 청크 하나에 담은 그림. 청크 1개 · 1.09 MB.

이러면 모든 페이지가 그 청크 하나를 불러옵니다. 모든 페이지가 같은 청크를 공유하니 첫 로드 이후에는 전부 캐시 적중입니다. 깔끔하죠. 페이지 이동도 빠릅니다.

물론 무엇을 내주게 되는지 이미 눈치채셨을 겁니다. 자바스크립트가 거의 없거나 아예 없는 페이지를 열어도 다른 모든 페이지의 자바스크립트까지 함께 받아야 합니다. 난감한 일이죠. 사이트가 커질수록 페이지를 열 때마다 점점 더 무거워집니다. 길게 보면 이런 방식은 쓸 수 없습니다.

페이지마다 청크를 하나씩 두는 방식으로 바꿔 볼 수도 있습니다. 그러면 청크는 가벼워지고, 필요 이상으로 코드를 보내는 일도 없습니다. 완벽해 보이죠.

페이지마다 청크를 하나씩 둔 그림. 청크 8개 · 1.20 MB.

하지만 이러면 캐싱을 잃습니다. 모든 페이지가 <Footer />를 쓴다면 그 코드가 페이지마다 청크 안에 들어갑니다. 네 페이지를 열어 본 방문자는 같은 푸터 코드를 네 번 내려받습니다.

그래서 더 잘게 쪼갠 방식이 필요합니다. 모듈마다 청크를 하나씩 주면 어떨까요? 필요 이상으로 보내는 코드가 하나도 없고, 여러 페이지가 공유하는 모듈은 한 번만 내려받습니다.

모듈마다 청크를 하나씩 둔 모자이크. 청크 355개 · 1.09 MB.

좋습니다. 다만 문제가 하나 있습니다. 아주 작은 자바스크립트 파일을 받으려고 네트워크 요청을 수백 건 보내야 한다는 점입니다. 요청 하나하나에 부담이 따르고, 그게 수백 건이면 사이트가 느려집니다. HTTP/2가 요청 비용을 낮췄지만 요청마다 붙는 부담은 여전합니다. gzip 같은 압축 알고리즘도 작은 파일이 많으면 성능이 떨어집니다. 반복되는 패턴을 파일 하나 안에서만 찾을 수 있기 때문입니다. 압축 사전이 도움은 되지만 이 문제를 해결해 주지는 않습니다.

요청을 줄일까요, 코드를 줄일까요?

그래서 남은 과제는 이렇습니다. 내려받는 크기는 가능한 한 작게, 요청 수도 가능한 한 적게 하고 싶습니다. 안타깝게도 이 두 목표는 서로 부딪칩니다. 청크를 적게 만들어 크게 묶으면 요청 수는 줄지만, 청크가 커질수록 여러 페이지에서 재사용하기는 어려워집니다.

Turbopack은 작은 청크들을 합쳐 더 큰 청크로 만드는 방식을 택했습니다. 어려운 부분은 어떤 청크를 합칠지, 그리고 합치는 게 정말로 이득인 때가 언제인지 판단하는 일입니다.

그 이야기를 하려면 새 용어 하나를 소개해야 합니다. 바로 청크 그룹입니다. 청크 그룹은 함께 로드되는 청크들의 묶음입니다. 예를 들어 /home에서 쓰는 청크 전부가 한 청크 그룹에 속하고, /blog의 청크들은 또 다른 그룹에 속합니다.

청크 하나를 공유하는 두 청크 그룹. 파란 점선이 `/home`의 청크 그룹, 분홍 점선이 `/blog`의 청크 그룹입니다.

청크 그룹은 코드를 필요 이상으로 보내는 문제를 해결합니다. Turbopack은 같은 그룹에 속한 청크끼리만 합치는데, 한 그룹의 청크는 어차피 항상 함께 로드되므로 이들을 합쳐도 페이지가 원래 받지 않던 코드가 끼어들 일이 없습니다.

그렇다면 브라우저에 요청을 과하게 몰아주지 않으면서 캐시 적중을 최대로 끌어올리려면 어떻게 해야 할까요?

이걸 따져 보려면 방문을 두 종류로 나눠서 생각해 보면 됩니다. 페이지 하나를 보고 떠나는 방문, 그리고 페이지 하나를 본 뒤 다른 페이지로 이동하는 방문입니다. 세 페이지 이상을 보는 세션에도 같은 분석이 자연스럽게 확장됩니다.

이 두 경우를 두고 <Footer />의 청크와 <VideoPlayer />의 청크를 합칠 때 드는 비용과 얻는 이득을 하나씩 짚어 보겠습니다. <Footer />의 청크를 A, <VideoPlayer />의 청크를 B라고 부르겠습니다.

A는 모든 페이지에서 쓰이고 B는 홈 페이지에서만 쓰입니다. 둘 다 홈 페이지의 청크 그룹에 들어 있으니 합칠 후보가 됩니다.

방문자가 홈 페이지를 열고 떠난다면 합치는 쪽이 무조건 유리합니다. 요청이 두 건에서 한 건으로 줄고, 두 청크 모두 어차피 필요했으니까요.

두 번째 페이지까지 연다면, 그 페이지가 두 청크를 모두 필요로 하는지 하나만 필요로 하는지에 따라 달라집니다. 홈 페이지에서 약관 페이지로 이동했다고 해 봅시다. 약관 페이지는 A는 필요하지만 B는 필요하지 않습니다. AB가 한 파일로 합쳐져 있으니 그 합친 파일은 여기서 쓸모가 없고, 브라우저는 A를 따로 다시 내려받습니다. 결국 방문자는 A를 두 번 받은 셈입니다.

페이지 이동까지 감안하면, 합치기가 이득이 되는 경우는 두 페이지가 모두 두 청크를 다 필요로 할 때뿐입니다. 그럴 때는 합친 파일을 그대로 재사용하니 요청 한 건을 아끼게 됩니다.

나머지 이동 시나리오는 다음과 같습니다. 아래 각 행은 두 페이지를 보는 세션이고, 첫 페이지가 필요로 하는 것과 두 번째 페이지가 필요로 하는 것을 차례로 적었습니다. 숫자는 AB를 따로 두었을 때와 비교해, 둘을 합치면 그 세션 전체에서 요청 수와 내려받은 코드가 어떻게 달라지는지 보여 줍니다.

이동요청 수 변화내려받은 코드 변화
AA00
AB00
BB00
AA + B0+ A
BA + B0+ B
A + BA0+ A
A + BB0+ B
A + BA + B−10

청크 AB를 합칠지 검토할 때는 A만 쓰는 청크 그룹, B만 쓰는 청크 그룹, A + B를 함께 쓰는 청크 그룹이 각각 몇 개인지 셉니다. 그리고 각 시나리오가 일어날 확률로 합치기의 비용과 이득에 가중치를 둡니다.

마지막으로 한 페이지만 보는 경우와 두 페이지를 보는 경우에도 각각의 확률로 가중치를 둡니다. 저희는 세션의 3분의 2가 한 페이지짜리이고, 3분의 1이 두 페이지 이상이라고 추정합니다.

청킹 전략 세 가지

지금까지 이야기한 청킹 방식들을 비교해 보겠습니다.

각 방식의 숫자를 뽑기 위해 Turbopack 청킹 설정 세 가지nextjs.org를 방문했습니다. 전혀 합치지 않는 설정, 기본 설정, 그리고 청크 그룹 안의 모든 것을 합치는 설정입니다.

각 버전에서 같은 순서로 페이지를 이동하며 요청 수와 클라이언트가 내려받은 자바스크립트 양을 측정했습니다.

이동합치지 않음Turbopack 기본 설정그룹당 청크 하나
1nextjs.org (최초 페이지 로드)363.6 KiB (요청 76건)344.2 KiB (요청 24건)315.3 KiB (요청 6건)
2/blog35.0 KiB (4)35.0 KiB (3)34.0 KiB (2)
3/blog/next-16-3-turbopack6.0 KiB (2)8.7 KiB (1)38.3 KiB (1)
4/learn7.4 KiB (5)6.6 KiB (2)6.6 KiB (2)
5/learn/dashboard-app109.6 KiB (3)115.0 KiB (3)142.0 KiB (1)
6/learn/dashboard-app/getting-started0 KiB (0)0 KiB (0)0 KiB (0)
7/showcase24.6 KiB (2)25.5 KiB (2)25.0 KiB (1)
8/docs15.4 KiB (4)19.9 KiB (3)48.8 KiB (2)
전체561.6 KiB (요청 96건)554.8 KiB (요청 38건)610.0 KiB (요청 15건)

보시다시피 청킹은 균형을 잡는 일입니다. nextjs.org에서는 기본 설정이 전혀 합치지 않을 때보다 요청 수를 절반 이상 줄이면서 코드도 조금 덜 보냈습니다. 최대한 합치는 설정은 요청 수를 더 줄였지만 전체적으로는 코드를 10% 더 많이 보냈습니다. 다만 페이지 이동을 덜 했다면 최대한 합치는 쪽이 더 유리했을 겁니다. 한 그룹의 청크를 모두 하나의 큰 청크로 합치면 최초 페이지 로드에서는 이득을 보지만, 길게 보면 비용이 큽니다.

Next.js 16.3의 새로운 청킹 기능

청킹이 잘 동작하는 데는 두 가지 한계가 있습니다. 합치기는 아무도 방문하기 전인 빌드 시점에 결정되므로, 브라우저가 이미 캐시해 둔 것에 맞춰 반응할 수 없습니다. 그리고 알고리즘은 사람들이 사이트를 어떻게 돌아다니는지 추측해야 하는데, 한 페이지만 보는 방문에 3분의 2라는 가중치를 두는 이유가 바로 이것입니다. 저는 이번 여름 Turbopack 팀에서 인턴으로 일하며 이 두 가지를 모두 다뤘고, 애초에 청크에 들어가는 코드를 덜어내는 작업도 함께 했습니다.

더 똑똑해진 청크 가져오기

위 표는 합치기에 드는 비용을 보여 줬습니다. 방문자가 A를 쓰는 페이지를 열고 그다음 A + B를 쓰는 페이지를 열면, 합쳐 둔 탓에 A를 두 번 내려받게 됩니다. 번들러는 이걸 피할 수 없습니다. 빌드 시점에는 브라우저가 무엇을 이미 갖고 있는지 모르기 때문입니다. 하지만 런타임은 알고 있습니다.

Next.js 16.3 이상에서 next.config.jsexperimental.turbopackChunking.generateComponentChunks를 켜면, Turbopack이 합친 청크와 함께 합치지 않은 청크도 같이 내보냅니다. 저희는 합친 청크가 어떤 항목들로 이뤄져 있고 그중 무엇이 이미 로드됐는지 추적하므로, 요청하는 시점에 더 저렴한 쪽을 고를 수 있습니다. 합친 청크를 받거나, 빠진 조각만 받는 것입니다.

반대 방향으로도 동작합니다. 방문자가 합쳐진 A + B를 이미 로드했다면, A를 따로 다시 로드하는 일을 건너뛸 수 있습니다.

그러면 소프트 내비게이션에서 불필요한 코드를 덜 로드하게 되고, 페이지 이동에서 비용을 치르지 않고도 합치기의 이득을 챙길 수 있습니다.

차이를 확인하려면 원문 페이지의 데모 앱을 직접 눌러 보세요.

저희는 only-if-cached 지시어도 실험하고 있습니다. 이 지시어를 쓰면 방문자가 들어온 시점에 무엇을 캐시해 두었는지 확인할 수 있습니다. 그러면 사이트를 떠났다가 나중에 다시 돌아오는 사람들에게도 같은 개선을 적용할 수 있습니다.

분석 데이터를 활용한 청킹

눈치채셨을지 모르지만, 저희 청킹 알고리즘은 웹사이트에 대해 여러 가정을 깔고 있습니다. 예를 들어 3분의 2라는 가중치는 추측한 값입니다. 여러 사이트에 두루 통하는 무난한 기본값이지만, 특정 사이트에 꼭 맞는 값이라고는 할 수 없습니다. 사람들이 여러분의 사이트를 실제로 어떻게 돌아다니는지 알고 있다면, 그 정보를 저희에게 알려 줄 수 있습니다.

이제 next.config.jsexperimental.turbopackChunking 아래에서 다음 항목들을 설정할 수 있습니다.

  • firstPageLoadPriority: 한 페이지만 보는 경우와 두 페이지를 보는 경우 사이의 가중치를 조정합니다. 0과 1 사이에서 값이 클수록 최초 로드 속도를 우선하고, 그만큼 페이지 이동 성능은 내줄 수 있습니다. 이탈률을 시작값으로 쓰면 무난합니다. 기본값은 0.67입니다.
  • priorityRoutes: 로드 속도가 가장 중요한 페이지 목록입니다. 이 라우트들에서는 청크를 더 적극적으로 합칩니다.
  • clusters: 함께 방문되는 경우가 많은 라우트 묶음이며, 각 묶음은 정규식 배열로 정의합니다. 한 클러스터 안에서 겹치는 청크는 더 쉽게 합칩니다. 다만 클러스터에 A만 쓰는 페이지나 B만 쓰는 페이지가 둘 다 쓰는 페이지와 섞여 있으면 합치는 정도를 줄입니다.

더 작고 더 적은 청크

여기까지는 코드를 어떻게 묶을지에 관한 이야기였습니다. 마지막으로 소개할 기능들은 애초에 코드를 덜 보내자는 이야기입니다. 이를 위해 다음 기능들을 작업했습니다.

  • CJS 모듈 트리 셰이킹: 이전에는 ESM만 지원했기 때문에, CJS 모듈에서 쓰지 않는 임포트와 익스포트가 클라이언트까지 전달되고 있었습니다. experimental.turbopackCjsTreeShaking으로 켤 수 있습니다. 앞으로 나올 Next.js 버전에서는 기본으로 켜집니다.
  • 공유 Turbopack 런타임: 이제 페이지마다 하나씩 있던 런타임 청크를 단일 런타임 청크가 대신합니다. experimental.turbopackSharedRuntime으로 켤 수 있습니다. 첫 이동 이후의 모든 페이지 이동에서 블로킹 요청 한 건과 클라이언트 자바스크립트 약 10 KB를 아낍니다. 이 기능도 나중에 기본으로 켜집니다.
  • 더 가벼워진 기본 런타임: 런타임이 이제 WebAssembly와 Web Worker 코드를 기본으로 포함하지 않습니다. 해당 모듈을 쓰는지 감지해서 그때 로딩 코드를 넣습니다.

직접 써 보세요

Next.js 16.3 이상에서 이 새 기능들을 직접 써 보세요. CSS 청킹까지 포함해 청킹에 대해 더 알고 싶다면 Tobias청킹의 트레이드오프와 제약을 다룬 최근 발표를 확인해 보세요.

Edit this page

Search the archive

Find a page by title, or search the text inside one.