일이 지났습니다. 시의성에 유의하세요
서문
webpack은 비동기 로딩을 구현하고자 합니다. 즉, 주요 모듈을 먼저 로드하고, 특정 모듈이나 여러 모듈(즉, 번들된 chunk)이 필요할 때 요청을 보내 로드하는 방식입니다.
이렇게 하는 목적은 당연히 페이지의 초기 로딩 속도를 높이기 위함이지만, 추가적인 요청을 보내는 것은 피할 수 없습니다. 이 두 가지는 본래 양립하기 어려운 관계에 있으며, 여기서는 비동기 로딩의 세부 사항을 설명하겠습니다.
본문
구현은 주로 require.ensure([], callback)이라는 것을 통해 이루어집니다. 솔직히 말해 이 것을 주목하게 된 이유는 webpack.config.js의 output 필드에 chunkFilename이라는 필드가 있었기 때문입니다. 꼼꼼하게 파고드는 습관 때문에 이 필드와 filename 필드의 차이점을 알아보고 싶었고, 검색해 보니 filename (가정해 bundle.js)는 페이지에 필요한 모든 js를 번들링하여 최종적으로 생성된 전체 js을 생성합니다(물론 다중 페이지일 때 공통 모듈을 추출할 수 있지만, 이는 이번 주제의 핵심이 아닙니다). 반면 chunkFilename은 진입점이 아닌(entry에 나열된 필드) chunk 파일을 번들링하여 생성된 파일로, 주로 모듈을 비동기적으로 필요할 때 로드하는 데 사용됩니다.
이러한 파일들은 bundle.js에 번들링되지 않으며, 일부(전체가 아닌) 모듈에 의존하고 동시에 비동기 로딩이 필요하기 때문에 require.ensure을 사용하여 추가적인 js로 번들링됩니다. 그리고 이러한 js은 최종 bundle.js를 통해 script 태그를 생성한 후 append에 페이지에 로드됩니다:
1 | |
좋습니다, 이 모든 것은 이해하기 쉽지만, 공식 문서]를 확인할 때 몇 가지 주의할 세부 사항을 발견했습니다.
_CommonJS_와 AMD require 시의 차이점
CommonJS은 require.ensure([''], callback)을 사용하여 비동기 로딩 모듈을 처리합니다. AMD는 일반적인 AMD 모듈과 마찬가지로 require 배열 의존 형태로 처리됩니다 require([''], callback).
하지만 CommonJS이 배열의 모듈을 로드할 때는 로드만 하고 실행하지 않습니다. callback에서 다시 require을 호출해야 실행됩니다:
1 | |
require.ensure 메서드는 콜백 호출 시 dependencies의 모든 의존성이 동기적으로 요청될 수 있도록 보장합니다. require 함수의 구현이 콜백의 매개변수로 전송됩니다.
또한 이 callback의 매개변수는 require 인터페이스를 구현한 함수입니다(맞다면 단지 require 함수의 참조일 것입니다).
이 chunkFilename는 output의 chunkFilename 설정에 의해 덮어씌워질 수 있습니다.
반면 AMD는 일반적인 의존성 전치 방식이므로 require 시점에 모듈을 실행합니다:
1 | |
좋습니다, AMD의 예시는 익숙하지 않으니, CommonJS을 예로 들어 몇 가지 세부 사항을 설명하겠습니다.
먼저, require.ensure에 callback을 전달하면 콜백 함수에서 require로 가져온 모듈도 모두 최종 비동기 로딩 파일에 번들링됩니다.
chunk 번들링 최적화 전략
- 두
chunk이 동일한 모듈을 포함하는 경우, 이들은 하나로 병합됩니다. - 모듈이
chunk의 모든 상위chunk에서 사용 가능한 경우, 해당 모듈은chunk에서 제거됩니다. chunk이 다른chunk의 모든 모듈을 포함하는 경우, 최종적으로 더 많은module를 포함하는chunk이 번들링됩니다. 이 규칙은chunk이 여러chunk의 모든module을 포함하는 경우에도 적용됩니다.
두 번째 규칙은 이해하기 어려울 수 있습니다. 설명하자면, 진입 파일 A.js에 b 모듈이 포함되어 있고, require.ensure을 사용하여 생성된 chunk.js 파일에도 이 b 모듈이 포함되어 있는 경우입니다. require.ensure는 A.js 파일에서 호출되므로 A.js은 이 chunk.js의 상위 chunk로 간주됩니다. 따라서 최종적으로 생성된 chunk.js에 포함된 b 모듈 내용은 제거됩니다. 在所有父级 chunk 都可用은 첫 번째 규칙에서 설명한 경우를 가리킵니다: 여러 chunk이 동일한 module을 포함하는 경우, 최종적으로 하나의 bundle.js만 생성됩니다. 그러나 이로 인해 이 chunk이 여러 상위 chunk (즉, entry에 해당하는 chunk 파일)을 가질 수 있습니다.
검증해 보겠습니다:
진입 파일 app.js의 코드:
1 | |
다른 진입 파일 app2.js의 코드:
1 | |
ensure.js의 코드:
1 | |
ensure2.js의 코드:
1 | |
마지막으로, 하위 chunk와 상위 chunk가 모두 존재하는 if_be_remove.js의 코드:
1 | |
Chrome 브라우저 콘솔에서 Network에 로드된 js의 내용을 확인해보세요(여기서는 [id].[name].js 명명 규칙을 사용함)
app.js 페이지:

app2.js 페이지

보시다시피, if_be_remove.js이 두 개의 chunk 즉 1-love.js와 3.hate.js에서 참조되면서 동시에 이 두 chunk의 상위인 app.js와 app2.js에서도 참조되기 때문에, 이 두 chunk에서는 if_be_remove.js 코드가 나타나지 않습니다.
보충: chunk 개념과 정의
여기서 추가로 설명하자면, chunk이란 하나 또는 여러 개의 module로 구성된 독립적인 js 파일을 말하며, chunk은 다음과 같은 유형으로 나뉩니다:
Entry Chunks:Entry Chunks은 가장 흔히 접하는Chunks유형으로, 우리가 작성한 비즈니스 로직 관련 코드(대부분의 경우 공통chunks로 추출되지 않는 고유한 코드)를 포함하며, 일반적으로Initial Chunks로딩이 완료된 후에 실행됩니다(또는module번호가0인 모듈을 만날 때 실행됨).Normal Chunks:Normal Chunks은 주로 애플리케이션 런타임에 동적으로 로드되는 모듈을 의미하며,Webpack은JSONP과 같은 적절한 로더를 생성하여 동적 로드를 수행합니다.Initial Chunks:Initial Chunks은 근본적으로Normal Chunks이지만, 애플리케이션 초기화 시점에 로딩이 완료됩니다. 이 유형의Chunks은CommonsChunkPlugin에 의해 생성되며, 전역 모듈 위치 정보를 포함하고 있습니다.Entry chunks의 코드 실행은 이chunk에 의존하므로, 이js을 우선적으로 로드해야 합니다.
앞서 예시에서 공통 js으로 패키징되어 전체 또는 일부 페이지에서 사용되는 bundule.js은 Initial Chunks이며, 현재 페이지만 사용되는 chunk 예를 들어 app.xxxxxx.js는 Entry Chunks입니다. require.ensure을 통해 비동기적으로 로드되는 chunk 예를 들어 3-hate 1-love는 Normal Chunks입니다.
저는 인생의 중요한 선택의 기로에 섰을 때, 누군가 최선의 방법을 알려주어 소중한 시간을 헛되이 보내지 않기를 바라곤 합니다. 그런 마음으로 저는 자주 블로그를 쓰며, 광활한 인터넷의 이 작은 구석에 제게는 단 한 번뿐인 인생 경험을 기록하여 도움이 필요한 분들에게 도움이 되기를 바랍니다.