본문 바로가기

JavaScript

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

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

 

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

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


웹팩은 오늘날 웹 개발자들이 그들의 애플리케이션을 만드는 데 사용하는 필수적인 요소로 간주될 수 있는 매우 강력하고 흥미로운 도구입니다. 하지만, 많은 사람들은 그것의 복잡성 때문에 웹팩을 가지고 일하는 것은 꽤 도전적인 일이라고 주장합니다.

 

이 시리즈 글에서 웹팩으로 일하는 것이 좀 더 쉬워지길 바라면서 웹팩의 내부 작업에 대한 여러 세부 사항을 공유하고자 합니다. 이 글은 다른 웹팩 기능에 대해 더 자세히 살펴볼 다음 글들의 기초가 될 것입니다. lazy loading의 작동 방식, tree shaking의 작동 방식, 특정 loaders가 일하는 방식 등에 대해 배울 수 있습니다. 이 시리즈 글의 목표는 당신이 웹팩 관련 문제를 해결할 때 좀 더 수월해지는 것입니다. 이 글의 목적은 전체 프로세스에 대한 충분한 통찰력을 주어서 언제든지 당신이 스스로 웹팩의 어느 부분을 이해하거나 어떤 문제를 디버깅을 할 수 있게 하는 것입니다. 따라서, 마지막 세션에서는 웹팩의 테스트의 몇 가지 예제들을 통해 웹팩의 소스 코드를 디버깅하는 방법을 살펴보겠습니다.

 

전체 번들링 과정을 그린 다이어그램으로 시작할 것입니다. 일부 세부 사항은 다음 글의 주제이기 때문에 생략되었습니다. 그런 다음, 다이어그램에서 몇 단계를 확장할 것입니다. 이어서 모듈, 청크 등의 개념도 설명할 것입니다. 또한, 이해를 쉽게 하기 위해, 소스 코드의 스니펫을 다이어그램과 단순화된 코드 스니펫으로 대체하겠습니다. 하지만 소스 코드의 링크는 포함할 것이니 유용하게 사용할 수 있을 것입니다.

 

관례에 따라 `NormalModule`을 간단하게 모듈이라고 칭하겠습니다. `ExternalModule`(모듈 연합 사용 시), `ConcatenatedModule`(`require.context()` 사용 시)과 같은 다른 유형의 모듈들이 있으며, 이는 별도의 문서에서 다루도록 하겠습니다. 이 글에서는 `NormalModule`에만 초점을 맞출 것입니다.

 

기사를 읽으면서 소스 코드를 살펴보고 싶으면 웹팩의 소스 코드 디버깅 섹션을 먼저 확인해보세요.

다이어그램으로 프로세스 시각화하기

Excalidraw에서 직접 확인하면 다이어그램이 더 잘 보입니다. [링크]

 

다이어그램은 Excalidraw 링크에서 보는 것을 추천합니다. 이 링크는 다음에 소개할 각 세션의 구조 설명과 하나하나의 단계 혹은 여러 단계를 자세히 설명하는 데 사용됩니다.

 

그럼 시작해봅시다!

`entry` 객체

모든 것이 `entry` 객체에서 시작된다는 점을 언급하는 것이 매우 중요합니다. 예상할 수 있듯이, 이것은 많은 구성을 지원하기 때문에, 이 주제는 단독으로 글을 쓸 가치가 있습니다. 따라서 우리는 `entry` 객체가 그저 key-value 쌍의 집합일 뿐이라는 단순한 예제를 고려할 것입니다.

// webpack.config.js
entry: {
	a: './a.js',
	b: './b.js',
	/* ... */
}

개념적으로 웹팩의 모듈은 파일과 연결됩니다. 따라서 다이어그램에서 'a.js'는 새 모듈을 생성하며 'b.js'도 그렇습니다. 현재로선, 모듈이 파일의 업그레이드 버전임을 아는 것으로 충분합니다. 일단 생성되고 빌드되면, 모듈은 원시 소스 코드뿐만 아니라 사용된 로더, 종속성, exports(있는 경우), 해시 등과 같은 많은 의미 있는 정보를 포함합니다. `entry` 객체의 각 항목은 모듈 트리의 루트 모듈로 생각할 수 있습니다. 루트 모듈이 다른 모듈을 필요로 할 수 있기 때문에(이는 상당히 의존적인 것입니다), 모듈 트리는 다른 모듈이 필요할 수 있습니다. 이를 생각하면 더 높은 수준에서 어떻게 이러한 트리가 구축될 수 있는지 볼 수 있습니다. 이런 모든 모듈 트리들은 함께 모듈 그래프(ModuleGraph)에 저장됩니다. 이는 다음 섹션에서 살펴보겠습니다.

 

다음으로 언급해야 하는 것은 웹팩이 많은 플러그인들 위에 구축되어 있다는 것입니다. 번들링 과정이 잘 확립되어 있지만, 커스텀 로직을 추가하기 위해 조각을 추가할 수 있는 많은 방법이 있습니다. 웹팩의 확장성은 을 통해서 구현됩니다. 예를 들면, 모듈 그래프가 빌드된 후, 청크를 통해 새로운 자산이 만들어진 경우, 모듈이 빌드되기 전이면(로더가 실행되고 소스가 파싱 된 상태) 커스텀 로직을 추가할 수 있습니다. 이는 매우 흥미롭고, 웹팩 커스터마이징에 관련된 많은 문제들에 해결책을 제공할 수 있으므로, 향후 별도 글에서 살펴볼 것입니다. 대부분의 경우, 훅은 목적에 따라 분류되어 있고, 잘 정의된 목적을 위해 플러그인이 있습니다. 예를 들어 `import()` 함수(주석 및 인수 구문 분석 담당)를 처리하는 플러그인이 있습니다. 이를 ImportParserPlugin이라고 하며 이는 AST 파싱 중에 `import()`가 호출되면 훅을 추가합니다.

 

`entry` 객체를 다루는 플러그인이 두 개 있다는 것은 놀라운 일이 아닙니다. 실제로 `entry` 객체를 가져와서 객체의 각 항목마다 `EntryPlugin`을 생성하는 `EntryOptionPlugin`이 있습니다. 이 부분은 중요하고 이 섹션의 시작 부분에서 언급한 내용과도 관련이 있습니다. - `entry` 객체의 각 항목은 모듈의 트리를 생성합니다(모든 트리는 서로 분리됩니다). 기본적으로 `EntryPlugin`은 모듈 트리의 생성을 시작하고, 각 트리는 동일한 단일 장소인 모듈 그래프에 정보를 추가합니다. 비공식적으로, `EntryPlugin`이 이 복잡한 과정을 시작한다고 말할 수 있습니다.

초기 다이어그램과 동등한 수준을 유지하기 위해, `EntryPlugin`이 EntryDependency(엔트리 종속성)`를 생성하는 장소이기도 하다는 것을 언급할 가치가 있습니다.

 

상단의 다이어그램을 기반으로, 우리 스스로 느슨하게 실행하여, `EntryOptionPlugin`의 중요성에 대해 자세히 알아보겠습니다.

class CustomEntryOptionPlugin {
  // 이것은 플러그인을 만드는 일반적인 방법입니다.
  // 이는 간단한 함수이지만 대부분의 플러그인이 생성되는 방식과 동일하게 만들기 위해 이런 방식을 사용합니다.
  apply(compiler) {
    // 훅이 우리에게 번들링 과정에 개입할 수 있는 가능성을 제공한다는 것을 기억하세요.
    // `entryOption` 훅의 도움으로 우리는 번들링 과정의 시작을 의미하는 로직을 추가하고 있습니다.
    // `entryObject`의 인수는 구성 파일의 `entry` 객체를 갖고 있고,
    // 우리는 이를 모듈 트리를 생성하는 과정에 사용할 것입니다.
    compiler.hooks.entryOption.tap('CustomEntryOptionPlugin', entryObject => {
      // `EntryOption` 클래스는 모듈 트리의 생성을 처리합니다.
      const EntryOption = class {
        constructor (options) {
          this.options = options;
        };

        // 이것은 플러그인이기 때문에, 표준에 따르고 있습니다.
        apply(compiler) {
          // `start` 훅은 번들링 과정의 시작을 나타냅니다.
          // `hooks.entryOption`이 호출된 후에 호출됩니다.
          compiler.hooks.start('EntryOption', ({ createModuleTree }) => {
            // 플러그인의 구성을 기반으로 새로운 모듈 트리를 만드는 중입니다.
            // `options`는 entry 이름(기본적으로 청크의 이름이 되는)과 파일 이름이 포함됩니다.
            // `EntryDependency`는 이러한 옵션을 갭슐화하고 모듈을 만드는 방법을 제공합니다.
            // (`NormalModule`을 만드는 `NormalModuleFactory`와 매핑되어 있기 때문입니다)
            // `createModuleTree` 호출 후에는, 파일의 소스 코드를 찾은 다음, 모듈 인스턴스가 생성되고,
            // 그러면 웹팩이 AST를 받게 됩니다. 이 AST를 추후에 번들링 과정에서 사용됩니다.
            createModuleTree(new EntryDependency(this.options));
          });
        };
      };

      // `entryObject`의 각 항목에 대해 모듈 트리를 생성할 준비를 하고 있습니다.
      // 각 모듈 트리는 다른 모듈 트리와 독립되어 있다는 것을 기억하세요.
      // `entryObject`는 `{ a: './a.js' }`와 같을 수 있습니다.
      for (const name in entryObject) {
        const fileName = entryObject[name];
        // 아래와 같이 말한다고 볼 수 있습니다.
        // `좋아 웹팩아, 번들링 과정이 시작되면, 이 `entry`에 대한 모듈 트리를 생성할 준비를 해.'
        new EntryOption({ name, fileName }).apply(compiler);
      };
    });
  }
};

이 섹션의 마지막 부분에서는 종속성이란 무엇인지 조금 더 자세히 설명하겠습니다. 종속성은 이 글에서 계속 사용될 것이고 다른 글에서도 언급될 것이기 때문입니다. 당신은 이제 `EntryDependency`가 무엇이고 왜 필요한지 궁금할 것입니다. 제 관점에서는, 새로운 모듈을 생성하는 것은 모두 똑똑한 추상화로 귀결됩니다. 간단히 말해서, 종속성은 실제 모듈 인스턴스의 예비일 뿐입니다. 예를 들면, `entry` 객체의 항목들도 웹팩의 관점에서 보면 종속성이며, 이들은 생성될 인스턴스의 최소한의 정보(경로. 예를 들면 ./a.js, ./b.js)를 나타냅니다. 종속성에는 모듈의 요청에 필수적인 정보들, 예를 들면 어디서 모듈의 소스를 찾을 수 있는지에 대한 경로 정보 같은 것들이 있기 때문에, 종속성 없이는 모듈을 생성할 수 없습니다. 종속성은 또한 모듈을 구성하는 방법을 나타내며 모듈 팩토리에 대해서도 마찬가지입니다. 모듈 팩토리는 원시 상태(예를 들면, 단순 문자열인 소스 코드)에서 시작하여 웹팩에 의해 활용되는 구체적인 엔티티에 도달하는 방법을 알고 있습니다. `EntryDependency`는 사실 `ModuleDependency`의 일종으로, 모듈의 요청을 확실히 유지합니다. 모듈 팩토리는 `NormalModuleFactory`입니다. `NormalModuleFactory`는 그저 경로만으로 웹팩에 의미 있는 것을 만들기 위해 무엇을 해야 하는지 정확히 알고 있습니다. 이에 대해 다른 방식으로 생각해보면, 모듈은 처음에 그저 단순한 경로(`entry` 객체 또는 `import` 구문의 일부)였고, 그 후 종속성이 되었고, 마지막으로 모듈이 되었다는 것입니다. 

이를 시각화하면 다음과 같습니다.

위 다이어그램의 Excalidraw 링크는 여기를 참고하세요.

 

따라서 `EntryDependency`는 모듈 트리의 루트 모듈을 생성할 때 처음에 사용됩니다. 나머지 모듈에는 다른 유형의 종속성이 있습니다. 예를 들어 `import defaultFn from './a.js'`와 같이 `import` 구문을 사용하는 경우, 모듈의 요청(이 경우 './a.js')을 보관하고 `NormalModuleFactory`에 매핑하는 `HarmonyImportSideEffectDependency`가 있습니다. 따라서 'a.js'에 대한 새로운 모듈이 있을 것이고 이제 종속성이 수행하는 중요한 역할을 볼 수 있을 것입니다. 이는 기본적으로 웹팩에 모듈을 만드는 방법을 지시합니다. 이 종속성에 대한 자세한 내용은 글의 뒷부분에서 설명합니다.

 

이 섹션에서 배운 내용을 간단히 요약하겠습니다. `entry` 객체의 각 항목은 `EntryDependency`가 생성되는 `EntryPlugin` 인스턴스가 될 것입니다. `EntryDependency`는 모듈의 요청(즉, 파일의 경로)을 보관하고, 모듈 팩토리에 매핑하여 요청을 의미 있는 것(즉, `NormalModuleFactory`)으로 만드는 방법을 제공합니다. 모듈 팩토리는 파일 경로만으로 웹팩에 유용한 엔티티를 만드는 방법을 알고 있습니다. 다시 한번 말하지만, 종속성은 모듈의 요청과 요청을 처리하는 방법과 같은 중요한 정보를 갖고 있기 때문에 모듈을 만드는 데 매우 중요합니다. 종속성은 몇 가지 유형이 있으며 모든 유형이 새 모듈을 생성하는데 유용하지는 않습니다. 각 `EntryPlugin` 인스턴스와 새로 생성된 `EntryDependency`의 도움으로 모듈 트리가 생성됩니다. 모듈 트리는 모듈과 그들의 종속성 위에 구축되며, 모듈이기도 하고, 의존성을 가질 수도 있습니다.

 

이제 모듈 그래프에 대해 자세히 알아보면서 학습 여정을 계속해 보겠습니다.

모듈 그래프(ModuleGraph) 이해하기

모듈 그래프는 빌드된 모듈을 추적하는 방법입니다. 2개의 서로 다른 모듈을 연결하는 방법을 제공한다는 면에서 종속성에 크게 의존합니다. 예를 들면:

// a.js
import defaultBFn from '.b.js/';

// b.js
export default function () { console.log('Hello from B!'); }

2개의 파일과 2개의 모듈이 있습니다. 파일 a는 파일 b의 무언가가 필요하므로 `import` 구문으로 된 종속성이 있습니다. 모듈 그래프에 관란 한, 종속성은 두 모듈을 연결하는 방법을 정의합니다. 이전 섹션의 `EntryDependency`도 그래프의 루트 모듈(null 모듈이라고 함)과 `entry` 파일에 연결된 모듈, 이 두 개의 모듈을 연결합니다. 위 코드는 아래와 같이 시각화할 수 있습니다.

단순 모듈(예를 들면, `NormalModule` 인스턴스)과 모듈 그래프에 속해있는 모듈을 명확히 구분하는 것이 중요합니다. 모듈 그래프의 노드는 `ModuleGraphModule`이라고 하고 이는 단순히 꾸며진 `NormalModule` 인스턴스입니다. 모듈 그래프는 이와 같은 (`Map<Module, ModuleGraphModule>`)의 도움을 받아 이런 꾸며진 모듈들을 추적합니다. 할 수 있는 것이 많지 않은 `NormalModule` 인스턴스만 있으면 서로 통신하는 방법을 모르기 때문에 이런 측면들을 언급하는 것이 필요합니다. 모듈 그래프는 앞서 언급하였듯이 `NormalModule`과 `NormalGraphModule`을 서로 연결하는 맵의 도움을 받아 서로 연결함으로써 이러한 베어 모듈에 의미를 부여합니다. 이는 모듈 그래프 빌드하기 섹션의 마지막에 더 의미가 있습니다. 여기서 그래프를 순회하기 위해 모듈 그래프와 그 내부 맵을 사용 합니다. 몇 가지 추가 속성의 차이로 구성되기 때문에, 모듈 그래프의 모듈을 간단하게 모듈이라고 합니다.

모듈 그래프에 속하는 노드의 경우, 수신 연결 발신 연결이라는 몇 가지 항목이 잘 정의되어 있습니다. 연결의 모듈 그래프의 또 다른 작은 엔티티이며, 이는 원본 모듈, 목적지 모듈, 앞서 말한 두 모듈을 연결하는 종속성 등과 같은 의미 있는 정보를 갖고 있습니다. 구체적인 예를 들자면, 위 다이어그램을 기반으로 아래와 같이 새로운 연결이 생성되었습니다.

// This is based on the diagram and the snippet from above.
Connection: {
	originModule: A,
	destinationModule: B,
	dependency: ImportDependency
}

그리고 위 연결은 `A.outgoingConnections` set과 `B.incomingConnections` set에 추가됩니다.

이는 모듈 그래프의 기본 개념입니다. 이전 섹션에서 언급하였듯이, `entry`에서 생성된 모든 모듈 트리는 동일한 단일 장소인 모듈 그래프에 의미 있는 정보를 출력합니다. 이는 모듈의 모든 트리가 최종적으로 null 모듈(모듈 그래프의 루트 모듈)과 연결되기 때문입니다. null 모듈에 대한 연결은 `EntryDependency`과 `entry` 파일에서 생성된 모듈을 통해 만들어집니다.

모듈 그래프에 대한 제 생각은 다음과 같습니다.

위 Excalidraw 링크는 여기입니다. 참고로 이 다이어그램은 이전 예를 기반으로 하지 않습니다.

 

보시다시피, null 모듈은 `entry` 객체의 항목으로부터 만들어진 각 모듈 트리의 루트 모듈과 연결되어 있습니다. 그래프의 각 간선은 두 모듈 사이의 연결을 나타내고, 각 연결은 출발 노드, 목적지 노드, 그리고 종속성(이 두 모듈이 왜 연결되었는지에 대한 비공식적인 답변?) 정보를 갖고 있습니다.

 

이제 모듈 그래프에 대해 더 알게 되었으므로, 모듈 그래프가 어떻게 빌드되는지 알아보겠습니다. 

모듈 그래프(ModuleGraph) 생성하기

이전 섹션에서 살펴본 바와 같이 모듈 그래프는 null 모듈로 시작하고, null 모듈의 직계 자손은 `entry` 객체의 항목에서 만들어진 모듈 트리들의 루트 모듈들입니다. 따라서, 모듈 그래프가 만들어지는 과정을 이해하기 위해, 단일 모듈 트리의 빌드 과정을 살펴볼 예정입니다.

가장 먼저 생성된 모듈

아주 간단한 `entry` 객체부터 시작하겠습니다.

entry: {
	a: './a.js',
}

첫 번째 섹션에서 말한 바를 토대로 하면, 어느 시점에 './a.js'에 대한 `EntryDependency`를 갖게 됩니다. 이 `EntryDependency`는 `NormalModuleFactory`라고 불리는 모듈 팩토리에 매핑되기 때문에 요청에 대한 의미 있는 것들을 만드는 방법을 제공합니다. 여기서 우리는 첫 번째 섹션을 끝냈습니다.

 

이 과정의 다음 단계는 `NormalModuleFactory`입니다. `NormalModuleFactory`는 작업을 성공적으로 완료하면, `NormalModule`을 생성합니다.

불확실한 상황을 방지하기 위해 `NormalModule`은 원시 문자열에 지나지 않는 파일의 소스 코드 역직렬화 버전일 뿐입니다. 원시 문자열은 많은 가치를 제공하지 않으므로 웹팩은 그것으로 더 많은 것을 하지 못합니다. `NormalModule`은 소스코드를 문자열로 저장하지만, 동시에 의미 있는 정보와 기능을 제공합니다. 이를 테면, 지원된 로더의 정보, 모듈을 빌드하는 로직, 런타임 코드를 생성하는 로직, 그것의 해시값 등과 같은 것입니다. 즉, `NormalModule`은 웹팩의 관점에서 보면 단순한 원시 파일의 유용한 버전입니다.

 

`NormalModuleFactory`가 `NormalModule`을 만들어내기 위해서는 몇 가지 단계를 거쳐야 합니다. 모듈을 생성한 후에 해야 하는 일도 있습니다. 예를 들면 모듈을 빌드하고, 종속성이 있는 경우에는 이를 처리하는 과정이 필요합니다.

 

위에서 봤던 다이어그램입니다. 모듈 그래프를 만드는 부분에 집중해봅시다.

위 다이어그램의 링크는 여기를 참고하세요.

 

`NormalModuleFactory`는 생성 메서드를 호출하여 마법을 시작합니다. 그러면 해결 과정이 시작됩니다. 여기서 요청(파일의 경로)이 해석되고 해당 파일 유형의 로더도 마찬가지입니다. 로더의 파일 경로만 결정되었음을 알려드립니다. 이 단계에서는 아직 로더가 호출되지는 않았습니다.

모듈의 빌드 과정

필요한 모든 파일의 경로가 확인되면, `NormalModule`이 생성됩니다. 그러나 이 시점에서 모듈은 그다지 가치가 없습니다. 많은 관련 정보는 모듈이 빌드된 후에 나올 것입니다. `NormalModule`의 빌드 과정은 몇 가지 다른 단계로 구성됩니다.

  • 첫째로, 원시 소스 코드에서 로더가 호출됩니다; 만약 로더가 여러 개인 경우, 한 로더의 출력이 다른 로더의 입력이 될 수 있습니다(설정 파일에서 제공되는 로더의 순서가 중요합니다);
  • 두 번째로, 모든 로더가 실행된 결과인 문자열은 주어진 파일의 AST를 만드는 acorn(JavaScript 파서)에 의해 파싱 됩니다;
  • 마지막으로, AST가 분석됩니다; 이 단계에서 현재 모듈의 종속성(예를 들면 다른 모듈들)이 결정되기 때문에 분석이 필요합니다. 웹팩은 `require.context`, `module.hot` 등과 같은 magic function을 감지할 수 있습니다; AST 분석은 JavaScriptParser에서 이루어지며, 링크를 클릭하면 여러 사례를 확인할 수 있습니다; 번들링 과정의 다음 단계가 이 단계에 달려있기 때문에 이 부분은 가장 중요한 부분 중에 하나입니다;

AST를 통한 종속성 탐색

너무 자세히는 말고 적당한 선에서 탐색 과정을 알아봅시다.

위 다이어그램의 링크는 여기를 참고하세요.

 

`moduleInstance`는 `index.js` 파일에서 생성된 `NormalModule`을 참조합니다. 빨간색 `dep`은 첫 번째 `import` 구문에서 생성된 종속성을 의미하고, 파란색 `dep`은 두 번째 `import` 구문에서 생성된 종속성을 의미합니다. 이것은 단순화한 것입니다. 실제로는, 앞서 언급했듯이, 종속성은 AST를 얻은 후에 추가됩니다.

 

이제 AST를 알아봤으므로 이 섹션의 시작 부분에서 말했던 모듈 트리를 빌드하는 과정을 이어서 진행해보겠습니다. 다음 단계는 이전 단계에서 찾은 종속성을 처리하는 것입니다. 위의 다이어그램을 따라가 보면, `index` 모듈에는 2개의 종속성이 있습니다. 이 또한 모듈이고 이름은 `math.js`와 `util.js`입니다. 그러나 종속성이 실제 모듈이 되기 전에는 `index` 모듈은 그저 `module.dependencies`에 모듈 요청(파일 경로)과 `import` 지정자(`sum`, `greet`)와 같은 정보를 2개 갖고 있을 뿐입니다. 이들을 모듈로 변환하려면, 이러한 종속성이 매핑되어 있는 `ModuleFactory`를 사용해야 합니다. 그리고 위에서 설명한 것과 동일한 단계를 반복해야 합니다(반복은 이 섹션의 시작 부분에 있는 다이어그램에서 화살표로 표시되어 있습니다). 현재 모듈의 종속성을 처리한 후, 해당 종속성에도 종속성이 있을 수 있으며 더 이상 종속성이 없을 때까지 계속합니다. 이것이 모듈 트리가 빌드되는 과정이고, 물론 부모 모듈과 자식 모듈 사이에 적절한 연결이 만들어졌는지 확인합니다.

 

지금까지 얻은 지식을 바탕으로 모듈 그래프를 실제로 사용해 보는 것이 좋습니다. 이를 위해, 모듈 그래프를 순회하는 커스텀 플러그인을 구현하는 방법을 알아보겠습니다. 다음은 모듈에 서로 어떻게 종속되는지 보여주는 다이어그램입니다.

위 다이어그램의 링크는 여기를 참고하세요.

 

`a.js` 파일은 `b.js` 파일을 가져오고, `b.js` 파일은 `b1.js` 파일과 `c.js` 파일을 모두 가져옵니다. `c.js` 파일은 `c1.js` 파일과 `d.js` 파일을 가져오고 마지막으로 `d.js` 파일은 `d1.js` 파일을 가져옵니다. 끝으로 `ROOT`는 모듈 그래프의 루트인 null 모듈을 의미합니다. `entry` 옵션은 `a.js`라는 하나의 값으로만 구성됩니다.

// webpack.config.js
const config = {
  entry: path.resolve(__dirname, './src/a.js'),
	/* ... */
};

이제 사용자 지정 플러그인에 어떻게 표시되는지 살펴보겠습니다.

// 기존 웹팩 훅에 로직을 추가하는 방법은 `tap` 메소드를 이용하는 것입니다.
// `tap(string, callback)`
// 여기서 `string`은 주로 디버깅을 위한 것으로 커스텀 로직이 추가된 곳을 표시합니다.
// `callback`의 인수는 우리가 추가하는 커스텀 기능의 훅에 달려있습니다.

class UnderstandingModuleGraphPlugin {
  apply(compiler) {
    const className = this.constructor.name;
    // `컴파일` 객체: 이 객체는 번들 과정의 대부분의 *상태*가 보관되는 곳입니다.
    // 여기에는 모듈 그래프, 청크 그래프, 생성된 청크, 생성된 모듈, 생성된 자산 등과 같은 정보가 포함됩니다.
    compiler.hooks.compilation.tap(className, (compilation) => {
      // *모든* 모듈(관련된 모든 종속성을 포함하여)이 빌드된 후에, `finishModules`이 호출됩니다.
      compilation.hooks.finishModules.tap(className, (modules) => {
        // `modules`는 빌드가 끝난 모든 모듈의 set(집합)입니다.
        // 이는 단순한 `NormalModule` 인스턴스입니다.
        // 다시 한번 말하지만, `NormalModule`은 `NormalModuleFactory`에 의해 만들어집니다.
        // console.log(modules);

        // **module map**(Map<Module, ModuleGraphModule>).
        // 그래프를 순회하는데 필요한 정보를 포함합니다.
        const {
          moduleGraph: { _moduleMap: moduleMap },
        } = compilation;

        // DFS 방식으로 모듈 그래프를 탐색해보겠습니다.
        const dfs = () => {
          // `ModuleGraph`의 루트 모듈은 null 모듈임을 기억하세요.
          const root = null;

          const visited = new Map();

          const traverse = (crtNode) => {
            if (visited.get(crtNode)) {
              return;
            }
            visited.set(crtNode, true);

            console.log(
              crtNode?.resource ? path.basename(crtNode?.resource) : 'ROOT'
            );

            // 관련된 `ModuleGraphModule`을 가져옵니다.
            // `NormalModule` 외에는 그래프를 순회하는데 필요한 몇 가지 추가 속성만 있습니다.
            const correspondingGraphModule = moduleMap.get(crtNode);

            // `Connection`의 `originModule`은 화살표가 시작되는 곳이고,
            // `Connection`의 `module`은 화살표가 끝나는 곳입니다.
            // 따라서 `Connection`의 `module`은 자식 노드입니다.
            // 여기 당신이 그래프의 연결에 대해 더 알 수 있는 주소가 있습니다.
            // https://github.com/webpack/webpack/blob/main/lib/ModuleGraphConnection.js#L53.
            // `correspondingGraphModule.outgoingConnections`는 Set이거나 undefined(이 경우 노드는 자식이 없음)입니다.
            // 여러 연결을 통해 한 모듈이 동일한 모듈을 참조할 수 있기 때문에 `new Set`을 사용합니다.
            // 예를 들어, `import foo from 'file.js'`는 다음과 같은 두 개의 연결을 생성합니다.
            // 하나는 단순 import용이고 다른 하나는 `foo`의 default 지정자용입니다.
            // 이런 세부 구현까지는 신경쓰지 않아도 괜찮습니다.
            const children = new Set(
              Array.from(
                correspondingGraphModule.outgoingConnections || [],
                (c) => c.module
              )
            );
            for (const c of children) {
              traverse(c);
            }
          };

          // 순회를 시작합니다.
          traverse(root);
        };

        dfs();
      });
    });
  }
}

이 예제는 StackBlitz 앱에서 확인할 수 있습니다. 플러그인이 작동하는지 보기 위해 `npm run build`를 실행해주세요. 모듈의 계층 구조를 기반으로 `build` 명령어를 실행한 후, 아래와 같은 결과를 얻을 수 있습니다.

a.js
b.js
b1.js
c.js
c1.js
d.js
d1.js

모듈 그래프가 다 만들어졌습니다. 당신이 이 과정을 잘 이해하셨기를 바랍니다. 이제 다음에 무슨 일이 일어날지 알아볼 차례입니다. 주요 다이어그램에 따르면, 다음 단계는 청크를 만드는 것입니다. 이제 그 과정을 살펴보겠습니다. 그러나 그전에 청크, 청크 그룹, 시작점(EntryPoint)과 같은 몇 가지 중요한 개념들을 명확히 할 필요가 있습니다.


내용이 길어 번역을 나누어서 진행했습니다.

다음 편에서 계속됩니다.