Module
모듈
관련 코드를 묶은 파일 단위. import/export로 사용. 코드 재사용.
모듈
관련 코드를 묶은 파일 단위. import/export로 사용. 코드 재사용.
모듈(Module)은 관련된 코드, 함수, 클래스, 변수 등을 하나의 파일 또는 논리적 단위로 캡슐화한 것입니다. 모듈의 핵심 목적은 코드의 재사용성과 캡슐화입니다. 외부에 공개할 인터페이스(export)와 내부 구현을 분리함으로써, 다른 코드에서 모듈의 세부 구현을 몰라도 정의된 API만으로 기능을 활용할 수 있습니다.
JavaScript 모듈 시스템은 복잡한 역사를 가지고 있습니다. 초기 JavaScript는 모듈 개념이 없었고, Node.js가 등장하면서 CommonJS(require/module.exports)가 서버 사이드 표준이 되었습니다. 브라우저에서는 비동기 로딩을 지원하는 AMD(Asynchronous Module Definition)가 사용되었습니다. 2015년 ES6에서 ES Modules(import/export)가 표준으로 채택되면서 현재는 브라우저와 Node.js 모두에서 통일된 모듈 시스템을 사용할 수 있게 되었습니다.
모듈화는 소프트웨어 개발의 여러 측면에서 장점을 제공합니다. 유지보수 측면에서 각 모듈이 독립적이므로 특정 기능 수정이 다른 부분에 미치는 영향을 최소화합니다. 테스트 측면에서는 개별 모듈을 격리하여 단위 테스트하기 쉽고, 의존성을 목(mock)으로 대체할 수 있습니다. 협업 측면에서는 팀원들이 서로 다른 모듈을 병렬로 개발할 수 있어 생산성이 향상됩니다.
실무에서는 배럴 패턴(Barrel Pattern)과 순환 의존성 관리가 중요합니다. 배럴 패턴은 index.js 파일을 통해 여러 모듈을 재내보내어 import 경로를 단순화하는 기법입니다. 순환 의존성은 모듈 A가 B를, B가 A를 참조할 때 발생하며, 런타임 오류나 예측 불가능한 동작을 유발할 수 있어 의존성 방향을 한쪽으로 정리하거나 공통 모듈을 분리하여 해결해야 합니다.
// ===== ES Modules (import/export) =====
// math.js - Named Export
export const PI = 3.14159;
export function add(a, b) {
return a + b;
}
export function multiply(a, b) {
return a * b;
}
// calculator.js - Default Export
export default class Calculator {
constructor() {
this.history = [];
}
calculate(operation, a, b) {
const result = operation(a, b);
this.history.push({ a, b, result });
return result;
}
}
// utils/index.js - 배럴 패턴 (Barrel Pattern)
// 여러 모듈을 하나의 진입점으로 재내보내기
export { add, multiply, PI } from './math.js';
export { default as Calculator } from './calculator.js';
export { formatNumber } from './format.js';
// app.js - 다양한 import 방식
// Named import
import { add, multiply, PI } from './utils/index.js';
// Default import
import Calculator from './calculator.js';
// Namespace import (모든 export를 객체로)
import * as MathUtils from './math.js';
// Rename import (별칭 사용)
import { add as addNumbers } from './math.js';
// 동적 import (코드 분할, 조건부 로딩)
async function loadFeature() {
if (needsAdvancedMath) {
const { advancedCalculate } = await import('./advanced-math.js');
return advancedCalculate(data);
}
}
// 사용 예시
const calc = new Calculator();
console.log(calc.calculate(add, 5, 3)); // 8
console.log(calc.calculate(multiply, 4, 7)); // 28
console.log(`원주율: ${PI}`); // 원주율: 3.14159
console.log(MathUtils.add(10, 20)); // 30
"이 서비스 로직은 너무 비대해졌어요. 도메인별로 모듈을 분리하는 게 좋겠습니다. user 모듈, order 모듈, payment 모듈로 나누고, 공통 유틸리티는 shared 모듈로 빼면 각 팀이 독립적으로 개발할 수 있고, 나중에 마이크로서비스로 전환하기도 수월해요."
"여기 userService와 orderService 사이에 순환 의존성이 있네요. userService에서 orderService를 호출하고, orderService에서도 userService를 호출하고 있어요. 두 서비스가 공통으로 필요로 하는 로직을 별도의 userOrderResolver 모듈로 분리하거나, 이벤트 기반으로 통신하도록 변경해야 합니다."
"번들 사이즈가 2MB가 넘어요. lodash를 전체 import 하지 말고 lodash-es에서 필요한 함수만 개별 import 해야 Tree Shaking이 제대로 동작합니다. 그리고 이 차트 라이브러리는 동적 import로 바꿔서 초기 로딩에서 제외합시다."
"ES Modules와 CommonJS의 차이를 설명해주세요. ES Modules는 정적 분석이 가능해서 Tree Shaking이 되고, 비동기 로딩과 Top-level await도 지원합니다. CommonJS는 동적 require가 가능하지만 번들 최적화가 어렵습니다. 저는 새 프로젝트에서는 ES Modules를 사용하고, package.json에 'type': 'module'을 설정합니다. Node.js 환경에서 CommonJS 라이브러리가 필요하면 createRequire로 호환성을 유지합니다."
모듈 A가 B를, B가 A를 참조하면 초기화 순서에 따라 undefined 오류나 예측 불가능한 동작이 발생합니다. ESLint의 import/no-cycle 규칙이나 madge 같은 도구로 순환 의존성을 감지하고, 의존성 방향을 단방향으로 정리하세요.
import _ from 'lodash'처럼 전체 라이브러리를 가져오면 사용하지 않는 코드까지 번들에 포함됩니다. import { debounce } from 'lodash-es'처럼 필요한 것만 named import 하거나, babel-plugin-lodash를 사용하여 자동 최적화하세요.
CommonJS(require)는 동적 특성 때문에 Tree Shaking이 어렵습니다. ES Modules를 사용하고, package.json에 "sideEffects": false를 설정하여 번들러가 사용하지 않는 코드를 안전하게 제거할 수 있도록 하세요.
좋은 모듈은 높은 응집도(관련 기능이 모여있음)와 낮은 결합도(다른 모듈과의 의존성이 적음)를 가집니다. 한 모듈이 너무 많은 역할을 하거나, 너무 많은 외부 모듈에 의존한다면 분리를 고려하세요.