빌드노트
CommonJS에서 exports에 객체를 넣었는데 require 결과는 빈 객체다
CommonJS에서 exports에 새 객체를 대입하면 지역 별칭만 바뀐다. 세 내보내기 방식의 키와 함수 호출을 비교하고, module.exports를 바꾼 뒤에도 남는 예전 exports 참조를 확인해 수정할 대상을 고른다.
CommonJS 파일 끝에 exports = { greet };를 썼는데 require()로 받은 객체에 greet가 없다면, 내보내는 쪽의 대입문부터 확인한다. 새 객체를 내보내려면 module.exports = { greet };로 바꾼다. exports에 새 값을 대입하면 파일 안의 지역 변수만 바뀐다. require()가 반환하는 대상인 module.exports는 그대로다. Node.js의 exports 별칭 설명 (새 탭에서 열림)
기존 객체에 함수를 붙이는 exports.greet = greet;도 가능하다. 다만 exports가 여전히 module.exports와 같은 객체를 가리킬 때의 이야기다. 두 이름이 언제나 함께 바뀌는 것은 아니다.
같은 함수에서 마지막 줄만 바꿔 본다
Windows 11, PowerShell 7.6.6, Node.js v22.23.2에서 새 파일로 비교했다. 실제 앱의 장애 기록이 아니라, 내보내기 대입문만 바꾼 별도 재현이다. 직접 확인하려면 Node.js가 설치된 환경의 새 빈 폴더에 아래 내용을 exports-reassign.cjs로 저장한다. .cjs는 CommonJS로 해석되므로 이 비교를 위해 package.json을 만들 필요는 없다. CommonJS 파일 조건 (새 탭에서 열림)
이 파일은 함수를 정의한 뒤 exports 자체에 객체를 대입한다.
function greet(name) {
return 'Hello, ' + name;
}
exports = { greet };
같은 내용을 두 파일로 더 복사하고, 함수는 그대로 둔 채 마지막 줄만 바꾼다. 두 수정 방식이 소비자에게 같은 함수를 전달하는지 비교할 것이다.
exports-property.cjs: 마지막 줄을exports.greet = greet;로 바꾼다.module-reassign.cjs: 마지막 줄을module.exports = { greet };로 바꾼다.
같은 폴더에 다음 내용을 inspect.cjs로 저장한다. 전달받은 파일을 읽고, 반환 객체의 키와 함수 타입을 확인한 뒤 함수가 있을 때만 호출한다.
const api = require(process.argv[2]);
console.log('keys=' + JSON.stringify(Object.keys(api)));
console.log('greet=' + typeof api.greet);
if (typeof api.greet === 'function') {
console.log(api.greet('Min'));
}
process.argv[2]에는 명령에서 넘긴 파일 경로가 들어간다. 그 폴더의 터미널에서 아래 세 명령을 실행하면, 매번 새 Node 프로세스가 같은 소비자 코드로 각 파일을 읽는다.
node ./inspect.cjs ./exports-reassign.cjs
node ./inspect.cjs ./exports-property.cjs
node ./inspect.cjs ./module-reassign.cjs
첫 번째 명령에서는 키가 없고 greet의 타입도 undefined였다.
keys=[]
greet=undefined
두 번째와 세 번째 명령은 각각 아래와 같이 출력했다. 키가 생겼을 뿐 아니라 실제 함수 호출도 같은 인사말을 반환했다.
keys=["greet"]
greet=function
Hello, Min
세 명령 모두 종료 코드는 0이었다. 첫 번째도 함수가 없으면 호출하지 않도록 했기 때문에 정상 종료했다. 별도로 첫 번째 파일의 greet('Min')을 검사 없이 호출하자 TypeError: require(...).greet is not a function과 종료 코드 1을 확인했다. 파일을 읽는 데 성공한 것과 필요한 함수를 받은 것은 다른 확인이다. 자기 코드에서도 이미 받은 객체의 키와 typeof api.greet를 보면 어느 단계까지 성공했는지 구별할 수 있다.
exports는 객체를 따라가는 이름이 아니다
Node.js는 모듈 본문을 실행하기 전에 exports에 module.exports의 값을 넣는다. 처음에는 두 이름이 같은 객체를 가리킨다. 그래서 exports.greet = greet는 그 객체에 속성을 추가하지만, exports = { greet }는 지역 변수만 새 객체를 가리키게 한다. 새 객체를 반환 대상으로 삼으려면 module.exports에 대입해야 한다. module.exports 설명 (새 탭에서 열림)
첫 번째 파일이 빈 객체를 반환한 것은 원래 객체에 아무 속성도 추가하지 않았기 때문이다. 추가 대조에서 exports.before = 'kept';를 먼저 실행한 뒤 exports = { greet };로 바꾸자, 소비자에게는 before 키만 남았다. 재대입이 기존 공개 속성을 지운 것은 아니며 새 객체의 greet가 전달되지 않은 것이다.
반대쪽 대입도 살펴볼 필요가 있다. module.exports를 새 객체로 바꿔도 지역 변수 exports는 예전 객체를 가리킨다. 앞의 module-reassign.cjs를 alias-after-module.cjs로 복사한 뒤 아래 세 줄을 마지막에 붙이면, 대입 이후에도 두 이름이 같은 객체인지 확인할 수 있다.
exports.extra = 'local';
console.log('same=' + (exports === module.exports));
console.log('local-keys=' + JSON.stringify(Object.keys(exports)));
같은 폴더에서 node ./inspect.cjs ./alias-after-module.cjs로 읽었을 때의 출력이다. 앞의 두 줄은 모듈 안에서, 나머지는 소비자에서 출력한 값이다.
same=false
local-keys=["extra"]
keys=["greet"]
greet=function
Hello, Min
extra는 지역 변수 exports가 가리키는 객체에 생겼지만 소비자가 받은 객체에는 없었다. 새 반환 객체에 속성을 더하려면 module.exports.extra = 'public';처럼 그 객체를 대상으로 삼는다. 이 수정으로 별도 실행한 파일에서는 keys=["greet","extra"]를 확인했다. 파일 끝에서 객체를 한 번에 내보낼 계획이라면 필요한 속성을 모두 모아 module.exports에 대입하는 방식도 선택할 수 있다.
수정 뒤에도 함수가 없다면 실제 require()가 읽는 파일 경로와 그 파일 뒤쪽에서 module.exports를 다시 덮어쓰는 코드가 있는지 확인한다. 여기서는 독립된 로컬 .cjs 파일을 동기적으로 읽는 동작을 검증했다. 순환 참조, 비동기 콜백에서의 내보내기 변경, 번들러가 변환한 결과는 실험하지 않았다.