Node.js process.getBuiltinModule 가이드: 런타임 선택 의존성을 더 안전하게 처리하는 법
Node.js 코드를 여러 런타임과 도구 환경에서 함께 쓰다 보면, 어떤 모듈은 있을 수도 있고 없을 수도 있는 상황을 자주 만나게 됩니다.
특히 내장 모듈을 참조해야 하지만 실행 환경에 따라 접근 방식이 달라질 때, 무심코 import나 require()를 고정하면 번들링·테스트·호환성에서 예상 밖 문제가 생길 수 있습니다.
이럴 때 process.getBuiltinModule()은 꽤 실용적입니다.
핵심은 Node.js 내장 모듈을 “있으면 가져오고, 아니면 안전하게 포기하는 방식”으로 다룰 수 있다는 점입니다.
process.getBuiltinModule이 필요한 이유
H3. 런타임 선택 의존성은 import 단계에서 바로 깨질 수 있다
실무에서는 아래 같은 상황이 자주 생깁니다.
- 일부 기능은 Node.js에서만 활성화하고 싶다
- 테스트 러너나 번들러가 정적 import를 과하게 해석한다
- 같은 코드베이스를 서버와 엣지 환경에서 함께 쓴다
- 내장 모듈 사용 여부를 런타임에서 늦게 결정하고 싶다
문제는 import fs from 'node:fs' 같은 정적 선언이 너무 이른 시점에 평가된다는 점입니다.
실제로 그 모듈을 쓰기 전에 이미 로딩 단계에서 제약이 발생할 수 있습니다.
H3. Node.js 내부 기능을 “존재 확인 후 사용” 패턴으로 바꿀 수 있다
process.getBuiltinModule(name)은 이름에 해당하는 내장 모듈이 있으면 반환하고, 없으면 undefined를 반환합니다.
덕분에 아래처럼 흐름을 바꾸기 쉬워집니다.
- 지금 런타임에서 이 내장 모듈이 필요한가?
- 실제로 접근 가능한가?
- 가능할 때만 기능을 켠다
이런 방식은 입력과 옵션을 늦게 해석하는 Node.js util.parseArgs 가이드: CLI 인자 파싱을 표준 기능으로 정리하는 법과도 결이 비슷합니다.
기본 사용법
H3. 내장 모듈을 안전하게 가져오는 가장 단순한 형태
const fs = process.getBuiltinModule?.('node:fs');
if (fs) {
const content = fs.readFileSync('README.md', 'utf8');
console.log(content);
}
여기서 중요한 포인트는 두 가지입니다.
process.getBuiltinModule자체가 없는 런타임도 있을 수 있으므로 optional chaining을 붙인다- 반환값이 없을 때를 정상 경로로 처리한다
즉, “실패”가 아니라 지원하지 않는 환경을 자연스럽게 통과시키는 설계가 중요합니다.
H3. CommonJS와 ESM을 섞는 호환성 문제를 줄일 수 있다
레거시 코드나 도구 환경에서는 ESM/CJS 경계가 생각보다 자주 문제를 만듭니다. 이때 내장 모듈 접근만큼은 런타임 함수 호출로 늦추면 초기 로딩 리스크를 줄일 수 있습니다.
export function readConfigIfPossible(filePath) {
const fs = process.getBuiltinModule?.('node:fs');
if (!fs) {
return null;
}
return fs.readFileSync(filePath, 'utf8');
}
모듈 해석보다 실제 실행 시점에 결정을 미루는 패턴은 Node.js import.meta.dirname 가이드: ESM에서 파일 경로를 안정적으로 다루는 법처럼 런타임 맥락을 명확히 다루는 글과도 함께 보면 좋습니다.
어떤 상황에서 특히 유용한가
H3. Node 전용 기능을 선택적으로 붙이는 라이브러리 코드
라이브러리나 공유 유틸리티는 “항상 Node에서만 실행된다”고 단정하기 어렵습니다. 예를 들어 로깅 기능만 Node에서 강화하고 싶다면 아래처럼 작성할 수 있습니다.
export function createDebugLogger() {
const util = process.getBuiltinModule?.('node:util');
if (!util) {
return console.log;
}
return (...args) => {
console.log(util.format('[debug] %O', args));
};
}
이 접근은 브라우저 번들에서 굳이 Node 전용 경로를 강제로 살리지 않아도 된다는 장점이 있습니다.
H3. 옵셔널 기능 플래그와 함께 쓰기 좋다
운영 도구에서는 특정 기능을 켜는 조건이 여러 개일 수 있습니다. 예를 들면 아래와 같습니다.
- 런타임이 Node.js인가?
- 관련 내장 모듈이 접근 가능한가?
- 사용자가 해당 기능을 활성화했는가?
export function maybeLoadEnvFile(enabled = true) {
if (!enabled) {
return false;
}
const processModule = process.getBuiltinModule?.('node:process');
if (!processModule?.loadEnvFile) {
return false;
}
processModule.loadEnvFile();
return true;
}
이처럼 기능 감지 후 활성화하는 방식은 Node.js loadEnvFile 가이드: built-in 환경변수 파일 관리를 단순하게 하는 법과도 자연스럽게 연결됩니다.
주의할 점
H3. 존재 확인이 곧 버전 호환성 보장을 의미하진 않는다
process.getBuiltinModule()이 편리하다고 해서 아무 버전에서나 동일하게 동작한다고 가정하면 위험합니다.
실무에서는 아래를 함께 확인하는 편이 안전합니다.
- 현재 Node.js 버전 범위
- 호출하려는 내장 API의 안정성 상태
- 폴리필 또는 우회 경로 필요 여부
- 테스트 환경에서의 지원 여부
즉, 이 함수는 호환성 전략을 대체하는 만능 열쇠가 아니라, 런타임 분기를 더 깔끔하게 만드는 도구에 가깝습니다.
H3. 없는 환경을 억지로 에러로 만들 필요는 없다
선택 기능이라면 아래처럼 조용히 빠지는 편이 더 낫습니다.
export function getCpuCount() {
const os = process.getBuiltinModule?.('node:os');
if (!os) {
return null;
}
return os.availableParallelism?.() ?? os.cpus().length;
}
핵심 기능이 아닌데도 지원되지 않는 환경에서 예외를 던지면, 재사용 가능한 코드가 오히려 좁아집니다. 에러를 던져야 하는 지점과 우아하게 포기해야 하는 지점을 구분하는 습관은 Node.js error cause 가이드: 래핑된 에러에서도 원인을 잃지 않는 법과 함께 익혀 두면 좋습니다.
실무 적용 패턴
H3. 접근 함수 하나로 감싸두면 코드베이스가 단정해진다
프로젝트 전역에서 직접 호출하기보다, 헬퍼 함수로 감싸 두면 일관성을 유지하기 쉽습니다.
export function getBuiltin(name) {
return process.getBuiltinModule?.(name);
}
export function getReadableFs() {
return getBuiltin('node:fs');
}
이렇게 두면 테스트에서 모킹하기도 쉬워지고, 지원 정책이 바뀌었을 때 수정 범위도 줄어듭니다.
H3. 분기 이유를 주석보다 코드 구조로 드러내는 편이 낫다
아래처럼 “왜 optional 경로가 필요한지”가 함수 이름만으로 드러나게 만드는 편이 유지보수에 유리합니다.
export function maybeCreateNodeOnlyCache() {
const fs = process.getBuiltinModule?.('node:fs');
const path = process.getBuiltinModule?.('node:path');
if (!fs || !path) {
return null;
}
const cacheDir = path.join(process.cwd(), '.cache');
fs.mkdirSync(cacheDir, { recursive: true });
return cacheDir;
}
여러 선택 경로가 얽힐 때는 너무 낙관적으로 병렬 처리하기보다, 어떤 분기가 실패해도 전체 기능이 안전하게 유지되도록 설계하는 편이 좋습니다. 이 관점은 Node.js Promise.allSettled 가이드: 부분 실패를 안전하게 처리하는 법과도 맞닿아 있습니다.
실전 체크리스트
H3. 도입 전에 이 네 가지를 먼저 확인한다
process.getBuiltinModule()을 넣기 전에 아래를 점검하면 시행착오를 줄일 수 있습니다.
- 이 기능이 정말 선택 기능인가?
- 미지원 환경에서 반환값을 무엇으로 할 것인가?
- Node 버전 조건을 문서나 테스트로 고정했는가?
- 직접 import보다 런타임 분기가 더 이득인가?
정적 import가 더 단순한 코드라면 굳이 바꿀 필요는 없습니다. 중요한 건 새 API를 쓰는 것 자체가 아니라, 환경 차이를 코드에 무리 없이 반영하는 것입니다.
마무리
process.getBuiltinModule()은 화려한 기능이라기보다, 런타임 차이를 조용히 흡수하는 작은 안전장치에 가깝습니다.
특히 Node 전용 기능을 선택적으로 붙여야 하는 라이브러리·CLI·공유 유틸리티에서는 코드의 생존 범위를 넓혀 줍니다.
내장 모듈을 무조건 정적으로 가져오기보다, 정말 필요한 시점에 존재를 확인하고 쓰는 흐름으로 바꾸면 호환성과 테스트 안정성이 함께 좋아질 수 있습니다.
함께 보면 좋은 글
이 글은 AI가 작성/정리하고, 오너가 방향을 결정한 프로젝트 운영 로그입니다.