본문 바로가기
빌드노트 검색

빌드노트

ES module로 바꿨더니 __dirname이 없어졌다

ES module에는 CommonJS의 __dirname이 없다. 실행 폴더를 바꿔 같은 이름의 파일을 읽어 보고, 모듈 기준 URL·import.meta.dirname·fileURLToPath 중 지원 버전과 API에 맞는 경로를 고른다. 공백 경로에서 URL.pathname을 그대로 쓰면 실패하는 이유도 확인한다.

package.json에 "type": "module"을 넣은 뒤, 파일을 읽던 .js에서 ReferenceError: __dirname is not defined in ES module scope가 날 수 있다. __dirname은 CommonJS가 제공하던 변수라서 ES module에는 없다. 모듈 옆의 자원을 읽으려면 new URL('./파일명', import.meta.url)을 기준으로 잡으면 된다. process.cwd()로 바꾸면 실행 폴더에 따라 다른 파일을 읽을 수 있다. Node.js의 ESM과 CommonJS 차이 (새 탭에서 열림)

먼저 실패한 파일에서 __dirname으로 찾으려던 대상이 무엇인지 본다. 코드와 함께 배포한 텍스트라면 모듈 위치가 기준이다. 사용자가 명령을 실행한 폴더에서 입력 파일을 고르는 도구라면 cwd, 즉 프로세스의 현재 작업 폴더가 기준일 수 있다. 이 둘을 같은 경로로 취급하면 오류가 사라져도 읽는 대상이 달라진다.

모듈 옆 파일은 URL 객체로 읽을 수 있다

기존의 readFileSync(join(__dirname, 'message note.txt'), 'utf8')를 고친다고 하자. 아래 코드는 read.js와 같은 폴더의 message note.txt를 읽는다. 두 로그로 실행 폴더와 모듈 주소도 함께 확인한다. 자신의 파일에서는 자원의 상대 위치에 맞게 이름을 바꾼다.

import { readFileSync } from 'node:fs';

console.log('cwd:', process.cwd());
console.log('module:', import.meta.url);
const resource = new URL('./message note.txt', import.meta.url);
console.log(readFileSync(resource, 'utf8').trim());

import.meta.url은 현재 모듈의 file: URL 문자열이다. 그 주소를 기준으로 자원 URL을 만들고, readFileSync에는 URL 객체 자체를 넘긴다. 여기서는 디렉터리 문자열을 만들 필요가 없다. import.meta.url과 상대 자원 읽기 (새 탭에서 열림), fs의 file URL 지원 (새 탭에서 열림)

이렇게 고친 뒤에도 ENOENT가 나면 로그의 모듈 주소를 먼저 읽는다. 실행 파일이 dist/read.js라면 위 코드는 dist/message note.txt를 찾는다. 소스 폴더에만 자원을 두고 배포 결과에 복사하지 않았다면 경로 기준을 고쳐도 읽을 파일은 없다.

실행 폴더만 바꾸자 읽는 내용이 달라졌다

이 차이를 Windows 11, Node.js v22.23.2에서 새 합성 파일로 비교했다. 실제 프로젝트의 장애 기록은 아니다. 모듈 폴더인 module space의 message note.txt에는 MODULE_RESOURCE, 다른 폴더인 other cwd의 같은 이름 파일에는 CWD_RESOURCE를 넣었다. 폴더 이름과 파일 이름에 모두 공백을 넣었다.

같이 확인하려면 빈 실습 폴더 아래에 두 폴더를 만들고 위 내용의 텍스트 파일을 각각 저장한다. module space/package.json에는 {"type":"module"}을, module space/read.js에는 앞의 코드를 저장한다. Node.js가 설치된 PowerShell에서 두 폴더의 부모를 현재 위치로 열고 다음 명령을 실행한다. $entry를 먼저 절대 경로로 고정하므로 두 실행은 같은 파일을 사용한다.

node --version
$entry = (Resolve-Path -LiteralPath './module space/read.js').Path
node "$entry"
Push-Location -LiteralPath './other cwd'
try { node "$entry" } finally { Pop-Location }

실제 실행에서 첫 cwd는 두 폴더의 부모였고 두 번째는 other cwd였다. module: 주소는 두 번 모두 같았으며 마지막 줄은 모두 MODULE_RESOURCE, 종료 코드는 모두 0이었다. 공백이 있는 모듈 폴더를 cwd로 삼아 실행해도 같은 내용을 읽었다.

반면 readFileSync('./message note.txt', 'utf8')로 읽는 대조군은 다음과 같았다.

  • module space에서 실행: MODULE_RESOURCE, 종료 코드 0.
  • other cwd에서 실행: CWD_RESOURCE, 종료 코드 0.
  • 같은 이름의 파일이 없는 별도 빈 폴더에서 실행: ENOENT, 종료 코드 1.

상대 경로 문자열은 모듈이 있는 폴더가 아니라 cwd에서 해석된다. 다른 폴더에 같은 이름의 파일이 있으면 실패조차 하지 않았다는 점이 중요하다. __dirname을 쓰던 원본은 두 cwd 모두 같은 ReferenceError와 종료 코드 1이었으므로, 터미널 위치를 바꾸는 것으로는 ESM의 변수 부재를 고칠 수 없었다. fs의 상대 문자열 경로 규칙 (새 탭에서 열림)

문자열 경로가 필요하면 지원 버전에 맞춰 고른다

경로 문자열을 받는 다른 API와 연결해야 한다면 import.meta.dirname을 쓸 수 있다. 다음은 그 디렉터리에서 같은 자원을 찾는 코드다.

import { readFileSync } from 'node:fs';
import { join } from 'node:path';

const resourcePath = join(import.meta.dirname, 'message note.txt');
console.log(readFileSync(resourcePath, 'utf8').trim());

공식 문서에서 import.meta.dirname은 v20.11.0과 v21.2.0에 추가됐으며, 22 계열에서는 v22.16.0부터 비실험 상태다. file: 모듈에서만 제공된다. 따라서 “Node 20을 쓴다”는 정보만으로 지원을 판단하지 말고 실제 실행 버전을 확인한다. 이번 v22.23.2 실행에서는 문자열이었고, 두 cwd 모두 MODULE_RESOURCE를 읽었다. 과거 버전별 실행은 하지 않았다. import.meta.dirname의 도입 버전과 조건 (새 탭에서 열림)

지원 대상에 import.meta.dirname이 없는 Node 버전이 포함된다면, 모듈 URL을 운영체제의 파일 경로로 변환한 뒤 디렉터리를 구하는 방법을 검토한다. 다음 코드도 같은 v22.23.2 환경에서 두 cwd의 결과를 확인했다.

import { readFileSync } from 'node:fs';
import { dirname, join } from 'node:path';
import { fileURLToPath } from 'node:url';

const moduleDir = dirname(fileURLToPath(import.meta.url));
const resourcePath = join(moduleDir, 'message note.txt');
console.log(readFileSync(resourcePath, 'utf8').trim());

이때 디렉터리는 이 코드를 적은 모듈의 위치다. 여러 파일에서 호출하는 도우미로 옮기면 도우미 모듈을 기준으로 계산한다는 점도 확인해야 한다.

pathname에는 공백 인코딩이 남아 있었다

변환을 줄이려고 new URL('./message note.txt', import.meta.url).pathname을 파일 경로로 쓰면 안 된다. 이번 Windows 실행에서 그 값은 /D:/로 시작했고 끝부분은 module%20space/message%20note.txt였다. 이를 readFileSync에 넘긴 결과는 두 cwd 모두 ENOENT였다.

동일한 URL 객체를 직접 넘기거나 fileURLToPath(resource)로 변환해 넘기면 두 cwd 모두 MODULE_RESOURCE를 읽었다. 후자는 D:\로 시작하고 실제 공백이 들어 있는 경로였다. fileURLToPath는 퍼센트 인코딩과 플랫폼 경로 형식을 처리한다. 단순히 %20을 지우거나 슬래시를 바꾸는 코드로 대신하지 않는다. fileURLToPath의 변환 규칙과 pathname 비교 (새 탭에서 열림)

검증한 범위는 Node.js v22.23.2의 기본 ESM 실행과 새 로컬 텍스트 파일 읽기다. Linux·macOS, 네트워크 공유 경로, 심볼릭 링크, 번들러가 변환한 결과는 실행하지 않았다. 자신의 코드에서는 먼저 자원이 모듈 소속인지 실행 폴더의 입력인지 정하고, 선택한 기준에서 실제 배포 파일이 존재하는지 확인한다.

← 전체 글 보기