모듈 번들러 구축 방법: 8가지 핵심 기술에 대한 완전 가이드와 코드 예제

발행: (2025년 12월 5일 오후 07:50 GMT+9)
5 min read
원문: Dev.to

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를 사용하면 importexport 구문을 안정적으로 찾을 수 있습니다.

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__ 함수 호출로 바뀌고, exportmodule.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...
]);
Back to Blog

관련 글

더 보기 »

JavaScript 첫 걸음: 간단한 정리

JavaScript에서 변수 let: 나중에 값이 변경될 수 있는 경우에 사용합니다. 예시: ```javascript let age = 20; age = 21; ``` const: 값이 절대로 변경되지 않아야 할 때 사용합니다.