CommonJS 모듈과 require

CommonJS 모듈과 require

Node.js에는 두 가지 모듈 시스템이 있어요. 파일과 폴더를 논리적으로 묶어 재사용하는 CommonJS, 그리고 브라우저 표준을 따르는 ECMAScript 모듈(ESM)이죠. 이 글에서는 CommonJS를 중심으로 어떤 파일이 CommonJS로 취급되는지, require로 어떻게 불러오는지, 그리고 module 객체가 하는 일을 살펴볼게요.

출처: Modules: CommonJS modules — Node.js 공식 문서

어떤 파일이 CommonJS로 취급될까요

기본적으로 아래의 파일을 CommonJS 모듈로 취급해요.

  • 확장자가 .cjs인 파일.
  • .js나 확장자가 없는 파일인데, 가장 가까운 package.json의 최상위 "type" 필드 값이 "commonjs"인 경우.

예를 들어 circle.js라는 모듈이 있다면, 함수나 객체를 특별한 exports 객체의 프로퍼티로 추가해서 모듈의 루트로 내보내요. 이때 module.exports를 쓰면 이 모듈이 require로 무엇을 받게 될지 정의할 수 있고요.

require로 모듈을 불러와요

require(id)는 모듈, JSON, 로컬 파일을 가져올 때 써요. node_modules에서 모듈을 가져올 수도 있고, 로컬 모듈이나 JSON 파일은 상대 경로(./, ./foo, ../bar/baz 등)로 불러와요. 상대 경로는 __dirname(정의되지 않았다면 현재 작업 디렉터리)을 기준으로 해석돼요. 경로 해석은 OS와 무관하게 일관되게 동작해서, POSIX 스타일 예시가 Windows에서도 그대로 동작해요.

// __dirname(또는 현재 작업 디렉터리)을 기준으로 상대 경로로 로컬 모듈 가져오기
const myLocalModule = require('./path/myLocalModule');
// JSON 파일 불러오기
const jsonData = require('./path/filename.json');
// node_modules 또는 내장 모듈 가져오기
const crypto = require('node:crypto');

내장 모듈은 node: 접두사를 써서 명확하게 구분하는 걸 권장해요. 몇몇 내장 모듈(node:sqlite, node:test, node:ffi 등)은 node: 접두사가 필수인데, 이 목록은 module.builtinModules에서 확인할 수 있어요.

module 객체와 module.exports

module 객체는 현재 모듈을 가리키는 참조예요. 특히 module.exports는 이 모듈이 require로 무엇을 내보낼지 정의해요.

module.exports에 새 값을 대입할 땐 주의가 필요해요. 콜백처럼 지연된 시점에 대입하면 안 되고, 모듈이 처음 실행될 때 바로 대입해야 해요.

const EventEmitter = require('node:events');
module.exports = new EventEmitter();
// 모듈에서 스스로 시간이 지나면 'ready' 이벤트를 내보냄
setTimeout(() => {
  module.exports.emit('ready');
}, 1000);

이렇게 하면 다른 파일에서 require('./a')로 받은 객체에 이벤트를 연결할 수 있어요.

순환 의존(cycle)은 조심해야 해요

두 모듈이 서로를 require하면 순환 의존이 생겨요. 잘 동작하게 만들려면 신중한 설계가 필요해요. a가 b의 의존이고 b가 a의 의존인 상황이라면, 어느 쪽이 먼저 실행되는지에 따라 동작이 갈리기 때문이에요. 순서를 염두에 두고 모듈을 구성하는 게 좋아요.

더 알아보기