CommonJS
CommonJS는 JavaScript 모듈화의 중요한 이정표로, Node.js 생태계의 성장과 JavaScript의 서버 사이드 채택에 핵심적인 역할을 했다. 비록 ECMAScript 표준인 ES Modules가 점차 더 널리 사용되고 있지만, CommonJS는 여전히 많은 프로젝트와 라이브러리에서 사용되고 있다.
개발자는 프로젝트의 요구 사항, 타겟 환경, 사용하는 도구에 따라 CommonJS와 ES Modules 중 적절한 시스템을 선택하거나 두 시스템을 함께 사용하는 것이 좋다.
두 시스템의 차이점과 상호 운용성을 이해하는 것은 현대 JavaScript 개발에서 중요한 역량이다.
CommonJS의 역사와 배경
CommonJS는 JavaScript를 브라우저 외부 환경, 특히 서버 사이드에서 실행하기 위한 모듈 시스템으로 2009년에 개발되었다. Node.js가 CommonJS를 기본 모듈 시스템으로 채택하면서 널리 사용되기 시작했다.
CommonJS가 등장하기 전, JavaScript는 웹 브라우저에서 사용하는 언어로 모듈 시스템이 부재했다.
이로 인해 전역 네임스페이스 오염, 의존성 관리의 어려움, 코드 재사용성 문제 등이 발생했다.
CommonJS는 이러한 문제를 해결하여 JavaScript를 서버 사이드에서도 효과적으로 사용할 수 있게 만들었다.
CommonJS의 핵심 개념
모듈 정의 및 내보내기
CommonJS에서는module.exports
또는exports
객체를 사용하여 모듈에서 함수, 객체, 변수 등을 내보낸다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
// math.js // 단일 항목 내보내기 module.exports = function add(a, b) { return a + b; }; // 또는 여러 항목 내보내기 exports.add = function(a, b) { return a + b; }; exports.subtract = function(a, b) { return a - b; }; // 주의: exports = { … }와 같이 직접 할당하면 참조가 끊어집니다 // 아래 코드는 작동하지 않습니다 exports = { // ❌ 잘못된 방식 add: function(a, b) { return a + b; } }; // 대신 이렇게 사용해야 합니다 module.exports = { // ✅ 올바른 방식 add: function(a, b) { return a + b; } };
모듈 가져오기 (require)
CommonJS에서는require()
함수를 사용하여 모듈을 가져온다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
// app.js const math = require('./math'); // 전체 모듈 가져오기 console.log(math.add(2, 3)); // 5 // 구조 분해 할당으로 특정 함수만 가져오기 const { add, subtract } = require('./math'); console.log(add(5, 3)); // 8 console.log(subtract(10, 4)); // 6 // Node.js 내장 모듈 가져오기 const fs = require('fs'); const path = require('path'); // npm 패키지 가져오기 const express = require('express');
모듈 캐싱
CommonJS는 모듈을 한 번만 로드하고 캐싱한다. 이후에 같은 모듈을 가져오면 캐시된 인스턴스를 반환한다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
// counter.js let count = 0; module.exports = { increment: function() { return ++count; }, getCount: function() { return count; } }; // app.js const counter1 = require('./counter'); const counter2 = require('./counter'); // 같은 인스턴스를 참조 console.log(counter1.increment()); // 1 console.log(counter2.increment()); // 2 (공유된 상태) console.log(counter1.getCount()); // 2 (같은 count 변수를 참조)
모듈 해석 알고리즘
Node.js에서require()
함수는 다음과 같은 규칙으로 모듈을 찾는다:- 핵심 Node.js 모듈이면 해당 모듈 반환 (예:
fs
,path
) - 경로가
/
나./
,../
로 시작하면 로컬 파일 시스템에서 검색- 정확한 파일명 확인
.js
,.json
,.node
확장자 시도- 디렉토리인 경우
package.json
의main
필드 확인 또는index.js
검색
- 그 외의 경우
node_modules
디렉토리에서 모듈 검색- 현재 디렉토리의
node_modules
부터 시작해 상위 디렉토리로 이동하며 검색
- 현재 디렉토리의
- 핵심 Node.js 모듈이면 해당 모듈 반환 (예:
CommonJS와 Node.js
Node.js는 CommonJS를 기본 모듈 시스템으로 채택했으며, 다음과 같은 특징이 있다:
동기적 로딩
CommonJS는 모듈을 동기적으로 로드한다. 이는 서버 환경에서는 문제가 되지 않지만, 브라우저 환경에서는 성능 문제를 야기할 수 있다.순환 의존성 처리
CommonJS는 순환 의존성(circular dependencies)을 처리할 수 있지만, 일부 제한이 있다.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
// a.js console.log('a starting'); exports.done = false; const b = require('./b'); console.log('in a, b.done =', b.done); exports.done = true; console.log('a done'); // b.js console.log('b starting'); exports.done = false; const a = require('./a'); console.log('in b, a.done =', a.done); // false - 아직 완료되지 않은 a의 상태 exports.done = true; console.log('b done'); // main.js console.log('main starting'); const a = require('./a'); const b = require('./b'); console.log('in main, a.done =', a.done, 'b.done =', b.done);
실행 결과:
전역 객체
Node.js에서 CommonJS 모듈 내부에서 사용할 수 있는 특별한 전역 객체들이 있다:module
: 현재 모듈 객체exports
: 모듈에서 내보낼 객체 (module.exports
의 참조)require
: 모듈 가져오기 함수__dirname
: 현재 모듈 파일의 디렉토리 경로__filename
: 현재 모듈 파일의 전체 경로
브라우저에서의 CommonJS
CommonJS는 원래 브라우저용으로 설계되지 않았지만, 다음과 같은 방법으로 브라우저에서 사용할 수 있다:
번들러 사용 (Webpack, Browserify, Parcel)
RequireJS와 같은 모듈 로더 사용
실제 프로젝트에서 CommonJS 사용 예시
Express.js 애플리케이션
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
// app.js const express = require('express'); const path = require('path'); const cookieParser = require('cookie-parser'); const logger = require('morgan'); const indexRouter = require('./routes/index'); const usersRouter = require('./routes/users'); const app = express(); app.use(logger('dev')); app.use(express.json()); app.use(express.urlencoded({ extended: false })); app.use(cookieParser()); app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); app.use('/users', usersRouter); module.exports = app; // routes/users.js const express = require('express'); const router = express.Router(); const userController = require('../controllers/userController'); router.get('/', userController.getAllUsers); router.get('/:id', userController.getUserById); router.post('/', userController.createUser); module.exports = router;
테스트 코드 (Jest 프레임워크)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
// math.js module.exports = { add: (a, b) => a + b, subtract: (a, b) => a - b, multiply: (a, b) => a * b, divide: (a, b) => { if (b === 0) throw new Error('Cannot divide by zero'); return a / b; } }; // math.test.js const math = require('./math'); describe('Math functions', () => { test('adds 1 + 2 to equal 3', () => { expect(math.add(1, 2)).toBe(3); }); test('subtracts 5 - 3 to equal 2', () => { expect(math.subtract(5, 3)).toBe(2); }); test('throws error when dividing by zero', () => { expect(() => math.divide(10, 0)).toThrow('Cannot divide by zero'); }); });
CommonJS의 한계와 미래
한계
- 동기적 로딩: 브라우저 환경에서 성능 문제 발생
- 정적 분석 어려움: 동적
require()
호출로 인한 트리 쉐이킹 및 최적화 제한 - 비표준: ECMAScript 표준이 아닌 커뮤니티 규약
- 중복 코드: 일부 번들러에서 모듈 중복 발생 가능
미래 전망
JavaScript 생태계는 점진적으로 ES Modules로 이동하고 있지만, CommonJS는 여전히 널리 사용되고 있다:
- 하이브리드 접근법: 많은 프로젝트가 CommonJS와 ES Modules를 함께 사용
- 마이그레이션 패턴: 기존 프로젝트의 점진적 ES Modules 전환
- 번들러 지원 유지: Webpack, Rollup 등은 계속해서 CommonJS 지원
- 서버 사이드 유지: 많은 서버 사이드 코드가 여전히 CommonJS 형식