모듈 번들러 구축 방법: 8가지 핵심 기술에 대한 완전 가이드와 코드 예제
Source: Dev.to
모듈 해석
해석기는 엔트리 포인트(예: index.js)에서 시작하여 각 import에 대한 정확한 파일 위치를 결정합니다.
상대 경로인 경우 요청을 가져오는 파일의 디렉터리와 결합하고, 패키지 이름인 경우 디렉터리 트리를 위로 올라가면서 node_modules 폴더를 찾으며, 패키지의 package.json을 읽어 메인 엔트리를 확인하고 일반적인 확장자를 시도합니다.
// A simplified look at the resolution logic
async resolve(request, fromDir) {
// Is it a direct path?
if (request.startsWith('.')) {
let candidate = path.resolve(fromDir, request);
// Does it have an extension? If not, add one.
if (!path.extname(candidate)) {
candidate = await this.tryWithExtensions(candidate);
}
return candidate;
}
// It's a package name, look in node_modules
let dir = fromDir;
while (dir !== path.parse(dir).root) {
const pkgPath = path.join(dir, 'node_modules', request);
if (await this.exists(pkgPath)) {
// Found the package folder, now find its main file
return this.resolvePackageMain(pkgPath);
}
// Go up one directory and check again
dir = path.dirname(dir);
}
throw new Error(`Module not found: ${request}`);
}
의존성 그래프 구축
모듈 간의 의존 관계를 이해하기 위해 각 파일을 추상 구문 트리(AST)로 파싱합니다. AST를 사용하면 import와 export 구문을 안정적으로 찾을 수 있습니다.
const parser = require('@babel/parser');
const code = `import { formatDate } from './dateUtils.js';`;
const ast = parser.parse(code, { sourceType: 'module' });
console.log(ast.program.body[0].type); // 'ImportDeclaration'
console.log(ast.program.body[0].source.value); // './dateUtils.js'
그 다음 AST를 순회하면서 의존성을 수집합니다:
const traverse = require('@babel/traverse').default;
const dependencies = [];
traverse(ast, {
ImportDeclaration(path) {
// Record where this import is pointing to
dependencies.push(path.node.source.value);
},
ExportNamedDeclaration(path) {
// Also check for re-exports: export { x } from './module'
if (path.node.source) {
dependencies.push(path.node.source.value);
}
}
});
// Now 'dependencies' contains ['./dateUtils.js']
결과는 각 파일과 해당 파일이 요구하는 모듈들의 맵이며, 이를 통해 의존성 그래프가 형성됩니다.
Import와 Export 변환
번들링을 위해 ES 모듈 구문을 번들 런타임이 이해할 수 있는 형식으로 변환해야 합니다. import는 사용자 정의 __require__ 함수 호출로 바뀌고, export는 module.exports에 연결됩니다.
Import 변환
const t = require('@babel/types');
// Suppose we know the './stringLib' module has ID = 2
const moduleId = 2;
// Create the call to our runtime function: __require__(2)
const requireCall = t.callExpression(
t.identifier('__require__'),
[t.numericLiteral(moduleId)]
);
// Create the destructuring: const { capitalize } = __require__(2);
const newDeclaration = t.variableDeclaration('const', [
t.variableDeclarator(
t.objectPattern([
t.objectProperty(
t.identifier('capitalize'),
t.identifier('capitalize'),
false,
true // shorthand
)
]),
requireCall
)
]);
// 'path' is the AST node for the import statement
path.replaceWith(newDeclaration);
Export 변환
Exports are rewritten to assign values to module.exports:
// Example: export const version = '1.0';
module.exports.version = '1.0';
모듈 래핑
각 변환된 모듈은 함수로 래핑되어 개인 스코프를 제공하고 내부 코드가 module, exports, __require__에 접근할 수 있게 합니다.
function (module, exports, __require__) {
// The transformed module code lives here
const { capitalize } = __require__(2);
module.exports.greet = (name) => capitalize(name);
}
런타임 로더
번들은 모든 모듈 래퍼를 배열에 저장하고 필요할 때 로드할 수 있는 __require__ 함수를 제공하는 작은 런타임으로 마무리됩니다. 또한 순환 의존성을 처리하기 위해 실행된 모듈을 캐시합니다.
// The final bundle output starts with this runtime
(function (modules) {
// A cache for modules that have already been executed
const moduleCache = [];
// The require function our transformed code will call
function __require__(moduleId) {
// Return cached module if it exists
if (moduleCache[moduleId]) {
return moduleCache[moduleId].exports;
}
// Create a new module object for this id
const module = { exports: {} };
// Store it in cache immediately to handle circular dependencies
moduleCache[moduleId] = module;
// Get the module's wrapper function and execute it
// It receives the module object, its exports, and the __require__ function
modules[moduleId].call(module.exports, module, module.exports, __require__);
// Return the now-populated exports
return module.exports;
}
// Start the application by requiring the entry module (id 0)
__require__(0);
})([
// Our modules registry array starts here
// Each element is a wrapped module function
function (module, exports, __require__) {
// Transformed code for module ID 0 (entry point)
const utils = __require__(1);
console.log(utils);
},
// ...more wrapped modules...
]);