CJS와 ESM은 단순히 문법만 다른 것이 아니라, 로딩 방식, 최적화 가능성, 트리 쉐이킹 여부, 의존성 관리 방식까지 전반적으로 구조가 크게 다르다는 점을 공부하면서 새롭게 느꼈습니다. 특히 ESM은 정적 분석이 가능하기 때문에 번들링 과정에서 더 효율적인 최적화가 이루어진다는 점이 인상적이었습니다. 실제로 프로젝트를 진행하면서 모듈 시스템이 어떻게 코드의 구조를 좌우하는지 체감했고, 전역 스코프가 오염되던 시절과 비교하면 개발 환경이 얼마나 안정적으로 변했는지도 이해할 수 있었습니다.
또한 모듈을 어떻게 나누고 어떤 방식으로 가져올지를 설계하는 것이 단순한 파일 분리가 아니라, 전체 애플리케이션의 유지보수성과 확장성에 직결된다는 것을 경험했습니다. 작은 예시 코드에서도 export 방식 하나 바꾸는 것만으로 의존성 흐름이 더 명확해지는 것을 보고 모듈 시스템의 중요성을 다시 느끼게 됐습니다. 이런 이유로 앞으로 프로젝트를 설계할 때는 기능을 어떻게 모듈화할지부터 먼저 고민해보고, 상황에 맞는 모듈 시스템을 선택하려고 합니다.
전역 스코프 오염부터 모듈 시스템까지
자바스크립트를 공부하다 보면 전역 스코프 오염(Global Scope Pollution)이라는 말을 자주 듣습니다. 함수, 변수, 라이브러리 모두가 하나의 전역 공간에 존재하는 구조에서는 코드가 조금만 복잡해져도 충돌이나 예측 불가능한 동작이 쉽게 발생합니다. 이 문제를 해결하기 위해 ‘모듈 시스템’이라는 개념이 발전해왔는데요, 이 글에서는 전역 스코프 오염이 왜 문제가 되는지, 그리고 이를 해결하기 위한 CommonJS와 ECMAScript Modules(ESM)에 대해 차근차근 정리해보겠습니다.
1. 전역 스코프 오염이란 무엇인가요?
자바스크립트 초기에는 파일 간의 모듈 개념이 존재하지 않았습니다. 그래서 스크립트 파일을 여러 개 추가하면, 모두 같은 전역 스코프(window) 위에서 실행되었지만 이 구조에서는 아래와 같은 문제가 발생한다는 단점이 있었습니다.
- 이름 충돌(Name Collision)
- 의존성 파악의 어려움
- 로딩 순서에 따라 동작이 달라짐
아래처럼 두 스크립트가 동일한 변수 이름을 전역에 선언했을 때를 가정해보겠습니다.
<script>
var count = 1;
</script>
<script>
var count = 999; // 이전 count를 덮어씁니다.
</script>
여기서 중요한 점은 각 파일이 독립된 공간이 아니라 하나의 전역 객체(window)에 붙는다는 점입니다. 따라서 둘 중 어떤 코드가 마지막에 실행되었는지에 따라 전역 변수의 값이 뒤바뀌며, 이런 충돌은 규모가 커질수록 디버깅하기 어려운 버그로 이어집니다.
모듈 시스템이 필요한 이유
이러한 혼란을 방지하기 위해 필요한 것이 바로 모듈입니다. 모듈은 파일 하나를 독립된 스코프로 처리하며, 필요한 기능만 내보내고 가져올 수 있게 해줍니다.
2. CommonJS(CJS) – Node.js의 기본 모듈 시스템
Node.js가 등장하면서 서버 사이드 자바스크립트를 위해 만들어진 모듈 시스템이 CommonJS입니다. CJS에서는 require()로 외부 모듈을 가져오고, module.exports로 기능을 내보낼 수 있습니다.
// utils.js
function add(a, b) {
return a + b;
}
module.exports = { add };
여기서 주목해야 할 부분은 module.exports입니다. 모듈에서 어떤 기능을 외부에 공개할지 결정하는 역할을 하는데, 이 객체에 어떤 값이 담기든 require()로 가져가는 파일은 그 값을 그대로 받을 수 있습니다.
// main.js
const { add } = require('./utils');
console.log(add(2, 3)); // 5
require()는 실행 시점(runtime) 에 불러오는 동기적 로딩 방식입니다. 서버 환경에서는 파일을 동기적으로 읽어도 큰 문제가 없기 때문에 CJS는 이런 구조를 기반으로 하고 있습니다.
3. ECMAScript Modules(ESM)
CJS가 Node.js 내부에서만 사용되던 방식이라면, ESM은 브라우저와 Node.js 모두가 지원하는 자바스크립트 공식 표준 모듈 시스템입니다.
// utils.js
export function add(a, b) {
return a + b;
}
여기서 export 키워드는 '이 기능을 외부에서 사용할 수 있게 내보낸다'는 의미입니다. ESM은 파일 자체가 하나의 모듈이기 때문에, export 된 것만 바깥에서 접근할 수 있습니다. 이 때 전역 스코프는 전혀 오염되지 않습니다.
// main.js
import { add } from './utils.js';
console.log(add(2, 3)); // 5
import는 컴파일 단계에서 정적으로 분석되는 문법이라 트리 쉐이킹과 같은 최적화가 가능합니다. CJS와 비교해 중요한 장점입니다.
4. export / import 패턴
ESM에서는 다양한 방식으로 기능을 내보내고 가져올 수 있습니다.
1. Named Export
export const PI = 3.14;
export function double(x) {
return x * 2;
}
이렇게 '이름을 가진 값'을 그대로 외부화할 수 있습니다. 가져올 때도 이름을 정확히 맞춰야 합니다.
import { PI, double } from './utils.js';
2. Default Export
파일에서 가장 기본이 되는 기능 하나를 지정할 때 사용합니다.
export default function greet() {
console.log('Hello!');
}
가져올 때는 이름을 마음대로 정할 수 있습니다.
import greeting from './greet.js';
greeting();
여기서 default export는 ‘모듈의 대표 기능 하나’를 지정하는 개념이라고 이해할 수 있습니다.
3. 전체 모듈 가져오기
import * as Utils from './utils.js';
console.log(Utils.PI);
console.log(Utils.double(10));
이 방식은 여러 유틸 함수를 하나의 '네임스페이스 객체'로 묶어 쓰는 방식입니다. 전역 스코프를 맞는 이름 하나로 모듈 내부 기능을 감싸서 관리하는 방식이라 충돌 위험이 없습니다.
CommonJS vs ESM 비교하기
| 항목 | CommonJS (CJS) | ECMAScript Modules (ESM) |
| 문법 | require / module.exports | import / export |
| 로딩 방식 | 동기적 | 비동기적, 정적 분석 가능 |
| 표준 여부 | 비표준 | 자바스크립트 공식 표준 |
| 브라우저 지원 | 번들러 필요 | 네이티브 지원 |
| 트리 쉐이킹 | 불가능 | 가능 |
회고
CJS와 ESM은 단순히 문법만 다른 것이 아니라, 로딩 방식, 최적화 가능성, 트리 쉐이킹 여부, 의존성 관리 방식까지 전반적으로 구조가 크게 다르다는 점을 공부하면서 새롭게 느꼈습니다. 특히 ESM은 정적 분석이 가능하기 때문에 번들링 과정에서 더 효율적인 최적화가 이루어진다는 점이 인상적이었습니다. 실제로 프로젝트를 진행하면서 모듈 시스템이 어떻게 코드의 구조를 좌우하는지 체감했고, 전역 스코프가 오염되던 시절과 비교하면 개발 환경이 얼마나 안정적으로 변했는지도 이해할 수 있었습니다.
또한 모듈을 어떻게 나누고 어떤 방식으로 가져올지를 설계하는 것이 단순한 파일 분리가 아니라, 전체 애플리케이션의 유지보수성과 확장성에 직결된다는 것을 경험했습니다. 작은 예시 코드에서도 export 방식 하나 바꾸는 것만으로 의존성 흐름이 더 명확해지는 것을 보고 모듈 시스템의 중요성을 다시 느끼게 됐습니다. 이런 이유로 앞으로 프로젝트를 설계할 때는 기능을 어떻게 모듈화할지부터 먼저 고민해보고 상황에 맞는 모듈 시스템을 선택하려고 합니다.
'📚 CS > JavaScript' 카테고리의 다른 글
| [JavaScript] 함수 (Function) (0) | 2025.11.11 |
|---|---|
| [JavaScript] 프로그래밍 패러다임 (0) | 2025.11.06 |
| [JavaScript] 객체, 속성, 메서드, 클래스, 네임스페이스란? (1) | 2025.11.05 |
| [JavaScript] 브라우저 렌더링 파이프라인 (0) | 2025.10.15 |
| [JavaScript] Promise와 async / await (0) | 2025.09.08 |