ENOENT는 그 경로 문자열이 지금 작업 폴더 기준으로 디스크에 없을 때 난다. npm 스크립트는 package.json 위치, 크론은 홈이 cwd다. __dirname으로 고정 파일을 열고 WorkingDirectory를 넣는다. 개발자, Node.js, npm, 깃허브 액션, 도커, 크론 기준. 2026년 8월 Node.js·npm·깃허브 문서.
열린 경로가 cwd에 상대적인지부터 보면 됩니다. 노드는 fs.readFile('data/foo.json')을 프로세스 작업 폴더 아래로 풀어 엽니다. 터미널에서 프로젝트로 들어가 실행하면 있고, 홈에서 같은 명령을 치면 없습니다.
메시지에는 보통 절대 경로가 찍힙니다. 홈 아래면 작업 폴더가 홈이었다는 뜻이고, 프로젝트 아래인데도 없으면 이름 오타나 깃에 안 올라간 파일입니다.
Cannot find module과 헷갈리기 쉽습니다. 모듈 이름은 node_modules 사다리로 찾고, ENOENT는 파일 시스템 호출이 그 경로에 실체가 없을 때입니다. 패키지를 지워 깔아도 이 줄은 안 사라집니다.
혼자 사이트를 운영하면 터미널에선 프로젝트 폴더에 들어간 채로 명령을 치고, 자동 실행은 로그인도 없이 홈에서 시작합니다. 같은 파일인데 낮에는 되고 새벽에만 없다고 느껴지는 이유가 거의 여기입니다. 상대 경로를 믿기 전에, 그 명령이 어디서 시작되는지부터 적어두세요. 경로를 메모장에 옮겨 놓고 낮과 밤 로그를 나란히 보면 원인이 빨리 보입니다.
먼저 기억할 것 | 에러에 찍힌 절대 경로를 파인더나 ls로 그대로 열어 보세요. 그 위치에 파일이 없으면 cwd나 오타고, 있으면 권한·심볼릭 링크를 봅니다.
같은 상대 경로라도 cwd가 홈이면 프로젝트 파일을 가리키지 않는다
npm 스크립트와 크론의 cwd는 다르다
npm은 package.json 폴더를 cwd로 두고, 크론은 홈을 씁니다. 표로 나눕니다.
실행 방법
기본 cwd
고치는 칸
npm run foo
package.json이 있는 폴더
스크립트 안 상대경로는 루트 기준
node scripts/foo.js
셸의 현재 폴더
cd 후 실행, 또는 __dirname
crontab / launchd
대개 홈
WorkingDirectory 또는 cd &&
깃허브 액션 run
워크스페이스 루트
defaults.run.working-directory
도커 ENTRYPOINT
WORKDIR
WORKDIR과 COPY 경로 일치
한국 1인 개발자가 맥 launchd로 새벽 잡을 넣으면, plist에 WorkingDirectory를 빼먹기 쉽습니다. 터미널에서 테스트할 때는 이미 프로젝트에 들어와 있어서 재현이 안 됩니다. 재현은 홈에서 같은 명령을 치는 것입니다.
액션은 받아온 저장소 루트가 작업 폴더라 로컬 자동 실행과 반대입니다. 로컬은 되고 통합 환경만 경로가 비면, 작업 폴더를 하위 앱으로 옮겼거나 리눅스 대소문자와 안 맞을 수 있습니다. 낮에 맥에서 확인했다고 밤 작업까지 같다고 보지 마세요.
홈에서 재현하고 경로를 찍기
cd ~
node /path/to/repo/scripts/foo.js
# 스크립트 맨 위
console.log('cwd', process.cwd())
console.log('file', require('path').join(process.cwd(), 'data', 'foo.json'))
상대 경로 대신 파일 옆 기준으로 열기
데이터 파일은 cwd가 아니라 스크립트 파일 위치에서 엽니다. CommonJS는 __dirname, ESM은 import.meta.url을 파일 URL로 바꿔 디렉터리를 얻습니다. 이렇게 하면 홈에서 실행해도 같은 파일을 엽니다.
설정 파일은 예외가 있습니다. 사용자가 지정한 --config ./mine.json은 cwd 상대가 맞습니다. 도구가 의도적으로 작업 폴더를 쓰는 경우는 문서에 그렇게 적혀 있습니다. 기본 데이터·템플릿·SQL은 저장소 안 고정 경로가 안전합니다.
도커는 작업 폴더가 앱 루트인데 데이터만 빠지고 코드만 복사되면 런타임에 경로가 비어 있습니다. 이미지 빌드 목록에 파일이 들어갔는지, 무시 목록이 빼지 않았는지도 같이 보세요. 컨테이너 안에서 경로를 열어보는 습관이 로컬 추측보다 빠릅니다.
새벽 자동 실행은 로그인 셸이 아니라서 평소 쓰던 경로 줄임이 없습니다. 맥 작업 목록 파일에 작업 폴더를 빼먹으면 홈에서 시작합니다. 테스트할 때는 일부러 홈으로 나가 같은 명령을 쳐 보세요. 프로젝트 안에서만 확인하면 낮과 밤이 다른 버그가 남습니다.
cwd가 맞아도 파일이 없으면 세 가지입니다. 이름 오타, 깃에 안 올라간 로컬 전용 파일, 맥/리눅스 대소문자입니다. .env는 보통 gitignore라 CI에 없습니다. 시크릿은 액션 Secrets나 호스트 환경으로 넣고, 샘플은 .env.example만 커밋합니다.
한글 파일명도 갈립니다. 맥 NFD와 리눅스 NFC 정규화가 다르면 같은 글자가 다른 바이트가 됩니다. 가능하면 영문 소문자와 하이픈만 쓰세요.
권한으로 거절되면 코드가 EACCES입니다. ENOENT로 보이면 중간 디렉터리가 없는 경우가 많습니다. 부모 폴더를 먼저 만들거나, 쓰기 전에 경로가 존재하는지 확인하세요.
한글이 들어간 폴더 이름은 맥과 리눅스에서 글자 모양이 같아 보여도 저장 방식이 다를 수 있습니다. 가능하면 영문 소문자와 하이픈만 쓰고, 공백과 특수문자는 빼는 편이 안전합니다. 백업 디스크를 붙였다가 경로가 달라진 경우도 같은 줄로 보입니다.
모듈 에러와 섞지 말 것 | require('lodash') 실패는 Cannot find module입니다. readFile('./lodash') 실패가 ENOENT입니다. 설치를 다시 해도 후자는 안 고칩니다.
도커와 CI에서만 경로가 비는 경우
이미지에 파일이 없거나 워크스페이스 하위만 체크아웃된 겁니다. 멀티 스테이지 빌드에서 builder의 data/를 runner로 COPY하지 않으면 런타임 ENOENT가 납니다. CI 캐시가 node_modules만 복구하고 생성 파일을 빼도 같습니다.
깃허브 액션 defaults.run.working-directory: app를 쓰면, 루트의 data/는 안 보입니다. 경로는 그 하위 기준이거나, 한 스텝만 working-directory를 루트로 되돌리세요. 공식 문법은 워크플로 working-directory입니다.
포트 거절은 EADDRINUSE, 모듈 이름은 앞 글, 노드 버전은 미즈입니다. 이 글은 경로 문자열만 봅니다.