본문 바로가기

JavaScript

An in-depth perspective on webpack's bundling process (한글) (2)

이 글은 아래 원문을 번역한 글로 의역이 있을 수 있습니다. 정확한 의미를 파악하고 싶으신 분은 원문을 참고해주시기 바랍니다.

 

원문: https://indepth.dev/posts/1482/an-in-depth-perspective-on-webpacks-bundling-process

저작권 정보: https://indepth.dev/


1편: https://front-end-news.tistory.com/entry/An-in-depth-perspective-on-webpacks-bundling-process-1-%ED%95%9C%EA%B8%80

Chunk, ChunkGroup, EntryPoint 정의

이제 모듈에 좀 더 친숙해졌으니, 이 섹션 제목의 개념들을 설명해보겠습니다. 모듈이 무엇인지 한 번 더 짧게 언급하자면, 모듈은 파일의 업그레이드 버전입니다. 한 번 생성되고 빌드된 모듈은 원시 소스 코드 이외에도 많은 의미 있는 정보를 포함합니다. 예를 들면, 사용된 로더, 종속성, exports(있는 경우), 해시 등입니다.

 

Chunk는 하나 또는 여러 개의 모듈을 캡슐화합니다. 언뜻 보면, 엔트리 파일(엔트리 파일 = entry 객체의 항목)의 개수가 청크의 수와 비례한다고 생각할 수 있습니다. entry 객체는 항목은 하나만 가질 수 있고, 청크의 수는 1개 이상일 수 있기 때문에 앞선 문장은 부분적으로 참입니다. 각 항목마다 해당하는 청크가 dist 폴더에 있지만, import() 함수를 사용하는 경우와 같이 암묵적으로 다른 청크가 생성될 수도 있습니다. 어떻게 생성되든 간에 각 청크는 dist 폴더에 해당하는 파일이 있습니다. ChunkGroup 빌드 섹션에서 이 내용을 확장해서 어떤 모듈이 청크에 속하는지 혹은 그렇지 않은지 명확히 구분할 것입니다.

 

ChunkGroup은 하나 이상의 청크를 포함합니다. ChunkGroup은 다른 ChunkGroup의 상위 또는 하위 그룹일 수 있습니다. 예를 들어, dynamic imports를 사용할 때, 각 import() 함수에 대해 청크 그룹이 생성되고, 이 그룹의 상위 그룹은 기존에 있던 ChunkGroup, 즉 import() 함수가 사용된 파일(즉, 모듈)을 구성하는 ChunkGroup입니다. ChunkGraph 빌드 섹션에서 시각화한 자료로 확인할 수 있습니다.

 

EntryPoint는 entry 객체의 각 항목에 대해 생성되는 ChunkGroup의 한 유형입니다. 청크가 EntryPoint에 속한다는 사실은 렌더링 과정에 영향을 줍니다. 이는 향후 글에서 더 명확하게 설명하도록 하겠습니다.

 

이제 이런 개념들에 좀 더 익숙해졌으니, 계속해서 ChunkGraph에 대해 알아보겠습니다.

ChunkGraph 빌드

지금까지 우리가 알아본 것은 이전 섹션에서 이야기한 ModuleGraph 뿐이라는 것을 기억하세요. ModuleGraph는 번들링 과정의 필수 부분 중 하나일 뿐입니다. 모듈 그래프는 코드 분할과 같은 기능에 사용됩니다.

 

번들링 과정의 이 시점에서 entry 객체의 각 항목에 대해 EntryPoint가 있습니다. ChunkGroup의 한 종류로, 적어도 하나의 청크를 포함합니다. 따라서 entry 객체에 3개의 항목이 있는 경우, 3개의 EntryPoint 인스턴스가 있고, 각각은 청크를 갖고 있습니다. 이 청크는 엔트리포인트 청크라고 부르며, entry 항목의 키값을 이름으로 갖습니다. 엔트리 파일과 관련된 모듈을 엔트리 모듈이라고 하며 각 모듈은 그들의 엔트리포인트 청크에 속합니다. 이것들이 ChunkGraph 빌드 과정의 시작점이기 때문에 중요합니다. 청크는 하나 이상의 엔트리 모듈을 가질 수도 있습니다.

// webpack.config.js
entry: {
  foo: ['./a.js', './b.js'],
},

위의 예시에서 foo(항목의 키)라는 이름의 청크가 있을 것이고, 이는 2개의 엔트리 모듈을 가질 것입니다. 하나는 a.js 파일과, 다른 하나는 b.js 파일과 연결된 것입니다. 물론, 청크는 entry 항목을 기반으로 생성된 EntryPoint 인스턴스에 속할 것입니다.

 

구체적으로 들어가기 전에 빌드 과정에 대해 논의할 예시를 보겠습니다.

entry: {
    foo: [path.join(__dirname, 'src', 'a.js'), path.join(__dirname, 'src', 'a1.js')],
    bar: path.join(__dirname, 'src', 'c.js'),
},

이 예제는 앞서 얘기한 ChunkGroups(즉, 동적 imports), 청크, EntryPoints의 부모 자식 관계를 아우릅니다.

 

위의 예제는 여기에서 실행해볼 수 있습니다. 다음에 나올 다이어그램은 이 예제를 기반으로 합니다.

 

ChunkGroup은 재귀적인 방식으로 빌드됩니다. 모든 엔트리 모듈을 큐(대기열)에 추가하는 것으로 시작합니다. 그런 다음, 엔트리 모듈이 처리될 때, 즉 모듈의 종속성이 검사 될 때, 각 종속성도 대기열에 추가됩니다. 대기열이 비워질 때까지 이 작업이 반복됩니다. 이 과정은 모듈이 방문되는 부분입니다. 그러나 이는 단시 첫 번째 과정일 뿐입니다. ChunkGroup은 다른 ChunkGroup의 부모 혹은 자식일 수 있음을 기억하세요. 이런 연결은 두 번째 과정에서 해결됩니다. 예를 들어, 앞서 말한 바와 같이, 동적 import(import() 함수)는 새로운 자식 ChunkGroup을 생성합니다. 웹팩 용어로 말하면, import()는 종속성의 비동기 블록을 정의합니다. 저는 이걸 블록으로 봅니다. 가장 먼저 와닿는 것이 다른 무언가를 포함하고 있다는 것이기 때문입니다. import('./foo.js'.then(module => ...)의 경우, 우리의 목적이 비동기적으로 무언가를 로드하는 것이며, module 변수를 사용하기 위해서는 실제 모듈이 사용되기 전에 foo(foo 자체도 포함)의 모든 종속성(즉, 모듈)이 해결돼야 함이 명백합니다. 향후 기사에서 import() 함수가 어떻게 동작하는지, 그 특수성(magic comments와 다른 옵션들 등)을 포함하여 구체적으로 논의해 볼 것입니다.

만약 궁금하면 여기에 AST 분석 도중 블록이 생성되는 곳이 있습니다.

ChunkGroup 빌드 과정을 요약해 놓은 이 소스 코드는 여기에서 볼 수 있습니다.

 

그럼 이제, 위의 설정의 만들어진 ChunkGroup의 다이어그램을 살펴보겠습니다.

위 다이어그램은 여기에서 볼 수 있습니다.

 

이 다이어그램은 ChunkGroup의 매우 단순한 버전이지만, 청크와 ChunkGroups 간의 관계를 보여주는 데는 충분합니다. 우리는 4개의 청크를 볼 수 있고, 따라서 4개의 출력 파일이 있을 것입니다. foo 청크는 4개의 모듈을 가질 것이고, 그중 2개는 엔트리 모듈입니다. bar 청크에는 하나의 엔트리 모듈만 있고, 나머지는 일반 모듈로 간주할 수 있습니다. 또한 각 import() 문으로 새 청크를 포함하는 새로운 ChunkGroup(bar EntryPoint를 부모로 갖는)이 생성되었음을 알 수 있습니다.

 

생성된 파일의 내용은 ChunkGroup을 기반으로 하므로 이 과정은 전체 빌드 과정에서 매우 중요합니다. 다음 섹션에서 청크 자산(즉, 생성된 파일)에 대해 간략하게 설명하겠습니다.

 

ChunkGraph를 을 사용하는 실제 예를 살펴보기 전에, 몇 가지 특수성을 언급하겠습니다. ModuleGraph와 유사하게, ChunkGraph에 속하는 노드는 ChunkGraphChunk(ChunkGroup에 속하는 청크)라고 불리며, 이는 단순한 장식용 청크로 청크의 일부인 모듈, 청크의 엔트리 모듈 등과 같은 추가 속성입니다. ModuleGraph와 같이, ChunkGraph는 WeekpMap<Chunk, ChunkGraphChunk> 형태의 맵의 도움을 받아 이런 추가 속성이 있는 청크를 추적합니다. ModuleGraph의 맵과 비교하면, ChunkGraph에 의해 유지되는 이 맵은 청크 사이의 연결에 대한 정보는 포함하지 않습니다. 대신, 필요한 모든 정보(예: 소속된 청크 크룹)는 청크 내에 자체적으로 저장되어 있습니다. 청크는 ChunkGroups로 묶이고 이런 청크 그룹은 서로 부모-자식 관계일 수 있다는 것을 기억하세요. 모듈은 서로 의존할 수는 있지만, 부모 모듈에 대한 명확한 개념이 없기 때문에 이런 부분은 좀 다릅니다.

 

이제 사용자 지정 플러그인에 ChunkGraph를 사용하여 좀 더 이해하기 쉽게 만들어보겠습니다. 사용한 예제는 위 다이어그램에서 설명한 것과 동일합니다.

const path = require('path');

// We're printing this way in order to highlight the parent-child
// relationships between `ChunkGroup`s.
const printWithLeftPadding = (message, paddingLength) => console.log(message.padStart(message.length + paddingLength));

class UnderstandingChunkGraphPlugin {
  apply (compiler) {
    const className = this.constructor.name;
    compiler.hooks.compilation.tap(className, compilation => {
      // The `afterChunks` hook is called after the `ChunkGraph` has been built.
      compilation.hooks.afterChunks.tap(className, chunks => {
        // `chunks` is a set of all created chunks. The chunks are added into
        // this set based on the order in which they are created.
        // console.log(chunks);
        
        // As we've said earlier in the article, the `compilation` object
        // contains the state of the bundling process. Here we can also find
        // all the `ChunkGroup`s(including the `Entrypoint` instances) that have been created.
        // console.log(compilation.chunkGroups);
        
        // An `EntryPoint` is a type of `ChunkGroup` which is created for each
        // item in the `entry` object. In our current example, there are 2.
        // So, in order to traverse the `ChunkGraph`, we will have to start
        // from the `EntryPoints`, which are stored in the `compilation` object.
        // More about the `entrypoints` map(<string, Entrypoint>): https://github.com/webpack/webpack/blob/main/lib/Compilation.js#L956-L957
        const { entrypoints } = compilation;
        
        // More about the `chunkMap`(<Chunk, ChunkGraphChunk>): https://github.com/webpack/webpack/blob/main/lib/ChunkGraph.js#L226-L227
        const { chunkGraph: { _chunks: chunkMap } } = compilation;
        
        const printChunkGroupsInformation = (chunkGroup, paddingLength) => {
          printWithLeftPadding(`Current ChunkGroup's name: ${chunkGroup.name};`, paddingLength);
          printWithLeftPadding(`Is current ChunkGroup an EntryPoint? - ${chunkGroup.constructor.name === 'Entrypoint'}`, paddingLength);
          
          // `chunkGroup.chunks` - a `ChunkGroup` can contain one or mode chunks.
          const allModulesInChunkGroup = chunkGroup.chunks
            .flatMap(c => {
              // Using the information stored in the `ChunkGraph`
              // in order to get the modules contained by a single chunk.
              const associatedGraphChunk = chunkMap.get(c);
              
              // This includes the *entry modules* as well.
              // Using the spread operator because `.modules` is a Set in this case.
              return [...associatedGraphChunk.modules];
            })
            // The resource of a module is an absolute path and
            // we're only interested in the file name associated with
            // our module.
            .map(module => path.basename(module.resource));
          printWithLeftPadding(`The modules that belong to this chunk group: ${allModulesInChunkGroup.join(', ')}`, paddingLength);
          
          console.log('\n');
          
          // A `ChunkGroup` can have children `ChunkGroup`s.
          [...chunkGroup._children].forEach(childChunkGroup => printChunkGroupsInformation(childChunkGroup, paddingLength + 3));
        };
        
        // Traversing the `ChunkGraph` in a DFS manner.
        for (const [entryPointName, entryPoint] of entrypoints) {
          printChunkGroupsInformation(entryPoint, 0);
        }
      });
    });
  }
};

예시는 StackBlitz 앱에서 확인할 수 있습니다. npm run build를 실행하면 아래와 같은 출력이 나옵니다.

Current ChunkGroup's name: foo;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: a.js, b.js, a1.js, b1.js

Current ChunkGroup's name: bar;
Is current ChunkGroup an EntryPoint? - true
The modules that belong to this chunk group: c.js, common.js

   Current ChunkGroup's name: c1;
   Is current ChunkGroup an EntryPoint? - false
   The modules that belong to this chunk group: c1.js

   Current ChunkGroup's name: c2;
   Is current ChunkGroup an EntryPoint? - false
   The modules that belong to this chunk group: c2.js

들여쓰기는 부모 자식 관계를 의미합니다. 출력이 다이어그램과 일치하는 것을 보아 트래버설의 정확성을 확신할 수 있습니다.

청크 자산 방출

결과 파일이 단순히 원본 파일의 복사-붙여넣기가 아니라는 점이 중요합니다. 왜냐하면 기능을 수행하기 위해선, 예상한 대로 동작하도록 만드는 사용자 지정 코드를 웹팩이 추가해주어야 하기 때문입니다. 

 

웹팩이 어떤 코드가 생성되어야 하는지 어떻게 알 수 있을까요? 모든 것은 가장 기본적인(그래고 유용한) 계층인 module에서부터 시작됩니다. 모듈은 멤버를 export 할 수 있고, 다른 멤버를 import 할 수 있으며, 동적 imports를 사용할 수 있고, 웹팩의 함수(예: require.resolve)를 사용할 수 있습니다. 모듈의 소스 코드를 기반으로, 웹팩은 원하는 기능을 실행하기 위해 어떤 코드를 만들어야 할지 결정할 수 있습니다. 이는 종속성이 발견되는 AST 분석 중에 시작됩니다. 지금까지는 종속성과 모듈을 비슷한 의미로 사용했지만, 이 경우는 좀 더 복잡합니다.

 

예를 들어, 'import { aFunction } from './foo'를 실행하면, 두 개의 종속성(하나는 import 문 자체를 위한 것이고 다른 하나는 지정자, 즉 함수를 위한 것)이 만들어지고, 여기서 단일 모듈이 생성됩니다. 다른 예로는 import() 함수를 들 수 있습니다. 이전 섹션에서 언급했듯이, import() 함수는 종속성의 비동기 블록이 되고, 이런 종속성 중 하나가 동적 import에 한정된 ImportDependency 입니다.

이런 종속성은 생성되어야 하는 코드에 대해 몇 가지 힌트를 제공하기 때문에 중요합니다. 예를 들어, ImportDependency는 비동기적으로 모듈을 가져오고 내보낸 멤버를 사용하기 위해 웹팩에 무엇을 알려야 하는지 정확하게 알고 있습니다. 이 힌트들은 런타임 요구사항이라고 부를 수 있습니다. 예를 들어, 모듈이 일부 멤버를 내보내는 경우, HarmonyExportSpecifierDependency라고 불리는 종속성이 있습니다. 이것은 멤버를 내보내기 위한 로직을 처리해야 함을 웹팩에 알립니다.

 

요약하자면, 모듈은 런타임 요구사항과 함께 제공되며, 이는 모듈이 소스 코드 내에서 무엇을 사용하는지에 따라 달라집니다. 청크의 런타임 요구사항은 해당 청크에 속하는 모든 모듈의 런타임 요구사항 집합입니다. 이제 웹팩은 모든 청크의 요구 사항을 알고 있으므로, 적절한 런타임 코드를 생성할 수 있습니다.

 

이를 렌더링 과정이라고도 부르며, 자세한 내용은 다른 기사에서 설명할 것입니다. 현재는 렌더링 과정이 ChunkGraph에 크게 의존한다는 것을 이해하는 것으로 충분합니다. 왜냐하면 그것은 청크의 그룹(ChunkGroup, EntryPoint 등)을 포함하고, 청크의 그룹은 청크를 포함하며, 청크는 모듈을 포함하고, 모듈은 웹팩이 만들어야 하는 런타임 코드에 대한 정보와 힌트를 포함하기 때문입니다.

 

이제 이론적인 부분은 끝났습니다. 다음 섹션에서는 웹팩의 소스 코드를 디버깅하는 몇 가지 방법을 알아보겠습니다. 문제가 해결하거나 웹팩의 작동 방식에 대해 더 자세히 알고 싶을 때 유용할 것입니다.

결론

이 글에서는 웹팩을 다른 관점에서 볼 수 있도록 불필요한 정보는 제외하고 필요한 만큼의 정보를 포함하려고 노력했습니다. 웹팩은 복잡한 (그리고 매혹적인) 도구이며 이 글은 그것을 더 작고 소화하기 쉽게 나누고자 합니다.

 

읽어주셔서 감사합니다!

 

사용된 다이어그램들은 Excalidraw를 통해서 만들어졌습니다.

 

이 글을 리뷰해주고 굉장히 의미 있는 피드백을 준 MaxKoretskyi에게 특별한 감사를 전합니다.