BlueByte
EACCESFixed

npm: 전역 설치에서 EACCES permission denied

작성 Haneul Seo2026년 9월 6일 업데이트8 min

안녕하세요, BlueByte입니다. npm install -g가 EACCES: permission denied로 멈춘다면, 패키지나 네트워크에 문제가 있는 게 아닙니다 — npm이 여러분 계정 소유가 아닌 디렉터리에 쓰려 했고 운영체제가 거부한 것입니다. 증상은 전역 설치(npm install -g <무언가>)가 code EACCES와 /usr/local/lib/node_modules 같은 경로를 가리키는 permission denied 줄과 함께 실패하는 것입니다. 오늘은 이게 무슨 뜻인지, prefix 디렉터리 문제가 맞는지 확인하는 법, npm이 권장하는 방식으로 고치는 법, 그리고 sudo에 손대지 않고 재발을 막는 법까지 하나씩 짚어보겠습니다.

EACCES 오류가 알려주는 것

전역 설치가 실패하면 npm은 이런 블록을 출력합니다:

npm error code EACCES
npm error syscall mkdir
npm error path /usr/local/lib/node_modules/npm-check-updates
npm error errno -13
npm error Error: EACCES: permission denied, mkdir '/usr/local/lib/node_modules/npm-check-updates'

예전 npm(v9 이하)은 같은 실패를 npm error 대신 npm ERR! 접두사로 출력하고, syscall은 실패한 단계에 따라 access, open, unlink로 표시될 수 있습니다. 공통점은 code EACCES와 permission denied입니다: EACCES는 "접근 거부"를 뜻하는 POSIX 코드이고, errno -13은 같은 것을 숫자로 나타낸 것입니다. npm이 고장난 게 아니라 OS에 폴더 생성을 요청했고 OS가 거절한 것입니다.

전역 설치가 권한 벽에 부딪히는 이유

npm install -g는 npm의 prefix 디렉터리에 씁니다 — 전역 패키지와 그 bin 링크가 놓이는 전역 루트입니다. 시스템 전역 Node 설치(OS 패키지 매니저나 공식 설치 프로그램)에서 이 prefix는 보통 /usr/local(리눅스에서는 /usr)이며, 이곳은 root 소유입니다. 일반 계정은 여기에 파일을 만들 수 없으므로, root로 실행하거나 npm이 여러분 소유 디렉터리를 가리키게 하기 전까지 전역 설치는 실패합니다.

이것이 원인의 전부입니다. 의존성 충돌도, 손상된 캐시도, 잘못된 레지스트리도 아닙니다 — 그런 문제는 다른 오류를 냅니다. EACCES는 오직 대상 디렉터리를 누가 소유하느냐의 문제입니다.

프로젝트가 아니라 prefix 디렉터리 문제인지 확인하기

두 명령이 npm이 어디에 쓰려 하고 누가 그곳을 소유하는지 보여줍니다:

npm config get prefix
ls -ld $(npm config get prefix)/lib/node_modules

시스템 Node에서의 예상 출력 — root root 소유에 주목하세요:

/usr/local
drwxr-xr-x 3 root root 4096 Sep  6 10:12 /usr/local/lib/node_modules

그 디렉터리가 root 소유이고 whoami가 여러분 이름을 출력한다면, EACCES는 완전히 설명됩니다: root만 쓸 수 있는 곳에 쓰려 한 것입니다.

npm이 권장하는 방식으로 고치기: Node 버전 매니저

npm 자체 안내는, 가장 깔끔한 해결이 Node와 npm을 Node 버전 매니저로 다시 설치해 툴체인 전체가 홈 디렉터리 아래에 놓이게 하는 것이라고 말합니다. 그러면 어떤 전역 설치도 root가 필요 없습니다. 그렇게 Node를 설치하면 npm config get prefix가 홈 안쪽을 가리키고,

npm install -g npm-check-updates

...가 EACCES 없이 끝납니다. 새 장비에서는 이 방법을 우선하세요. 앞으로 설치할 모든 Node 버전에서 같은 문제를 함께 해결해 주기 때문입니다.

npm prefix를 내가 소유한 폴더로 옮겨 고치기

Node를 다시 설치할 수 없다면, npm prefix를 여러분이 소유한 디렉터리로 바꾸세요. 이것이 npm이 문서화한 수동 방법입니다(Windows에는 해당하지 않습니다):

npm config set prefix ~/.local

그런 다음 그 prefix의 bin을 PATH에 올리도록 ~/.profile(zsh를 쓰면 ~/.zprofile에도) 다음 줄을 추가합니다:

export PATH=~/.local/bin:$PATH

프로파일을 다시 읽고 설치합니다:

source ~/.profile
npm install -g npm-check-updates

이제 전역 패키지는 ~/.local/lib/node_modules에, 명령은 ~/.local/bin에 놓이며 모두 여러분 소유입니다.

실제 사례: 배포판 Node 뒤의 CLI 설치

배포판 패키지 매니저로 Node를 설치한 뒤 npm install -g typescript를 실행하니 EACCES: permission denied, mkdir '/usr/lib/node_modules/typescript'가 납니다. npm config get prefix를 실행하면 /usr가 나오고, ls -ld /usr/lib/node_modules는 root root를 보여줍니다. sudo 대신, npm config set prefix ~/.local을 실행하고 ~/.profile에 export PATH=~/.local/bin:$PATH를 추가한 뒤 source하고 npm install -g typescript를 다시 실행합니다. 설치가 되고 tsc --version도 동작합니다 — ~/.local/bin이 여러분 PATH에 있고 여러분 소유이기 때문입니다.

전역 설치가 내 디렉터리에 들어가는지 확인하기

위치와 명령이 실제로 잡히는지 함께 확인합니다:

npm root -g
which tsc
/home/you/.local/lib/node_modules
/home/you/.local/bin/tsc

npm root -g가 여전히 root 소유 경로를 보여주면 prefix 변경이 적용되지 않은 것입니다 — npm config get prefix를 다시 확인하세요. which가 아무것도 못 찾으면 PATH 줄이 아직 로드되지 않은 것입니다. 새 셸을 열거나 프로파일을 다시 source하세요.

EACCES를 다시 안 만나기 — 그리고 sudo가 잘못된 반사인 이유

sudo npm install -g는 오류를 사라지게는 하지만, 패키지를 root로 설치해 캐시와 설정 안에 root 소유 파일을 남길 수 있고, 이는 나중에 평범한 명령에서 새로운 EACCES를 유발합니다. npm의 안내는 이를 피하라는 것입니다. prefix를 한 번 설정해 dotfiles에 넣어 두고, 전역 패키지를 절대 root로 설치하지 마세요. 딱 한 번만 실행할 명령이라면 npx <명령>(npm 5.2+)이 전역 prefix를 전혀 건드리지 않고 실행해 줍니다.

EACCES와 ERESOLVE의 차이

EACCES는 파일시스템 권한 오류입니다 — npm이 대상 디렉터리에 쓰지 못한 것입니다. ERESOLVE는 의존성 해석 오류입니다 — npm은 어디에 쓸지는 정확히 알았지만 트리 안의 충돌하는 버전 요구를 조율하지 못한 것입니다. 메시지가 code EACCES / permission denied라면 소유권이나 prefix를 고치고, code ERESOLVE / could not resolve라면 버전 충돌이므로 권한을 아무리 고쳐도 풀리지 않습니다.

관련 질문

그냥 sudo로 해결하면 안 되나요?

한 번은 됩니다. 하지만 sudo npm install -g는 패키지를 root로 설치해 캐시와 설정에 root 소유 파일을 남기고, 이후 명령에서 새로운 EACCES를 일으킵니다. npm은 이를 피하라고 권합니다 — 대신 prefix를 여러분 소유 디렉터리로 옮기세요.

npm config set prefix는 어디에 저장되고, 계속 유지되나요?

사용자별 .npmrc(npm config get userconfig 참고)에 기록되므로, 그 사용자의 셸과 Node 업데이트를 넘어 유지됩니다. 현재 값은 언제든 npm config get prefix로 확인하세요.

prefix를 바꿨는데도 명령을 찾지 못합니다.

새 bin 디렉터리가 아직 PATH에 없습니다. ~/.profile(zsh는 ~/.zprofile)에 export PATH=~/.local/bin:$PATH를 추가한 뒤 새 셸을 열거나 파일을 다시 source하세요.

Windows에도 해당되나요?

아니요. 수동 prefix 방법은 macOS와 Linux용으로 문서화돼 있습니다. Windows는 전역 prefix가 이미 사용자 프로파일 아래에 있어 이 root 소유 디렉터리 문제가 생기지 않습니다.

prefix를 옮기면 예전에 sudo로 설치한 패키지가 안 보이게 되나요?

네 — 그것들은 예전 root 소유 prefix에 남아 새 prefix에서는 보이지 않습니다. 아직 쓰는 것만 새 prefix로 다시 설치한 뒤, 예전 root 소유 사본을 정리하세요.

참고 자료

Haneul Seo

Infrastructure engineer · 10+ years running Linux fleets

같은 카테고리 다른 글

detected dubious ownershipFixed

Git: fatal: detected dubious ownership in repository

Git 2.35.2의 CVE-2022-24765 수정 이후, Git은 작업 트리나 .git 디렉터리의 소유자가 명령을 실행한 사용자와 다르면 저장소를 읽지 않습니다. 컨테이너, CI 작업, sudo 세션, 공유 드라이브에서 주로 나타납니다. 저장소가 내 것이어야 한다면 소유권을 바로잡고, 아니라면 global 설정의 safe.directory에 정확한 경로를 추가하세요 — Git은 이 설정을 저장소 자신의 설정에서는 무시합니다.

Git
failed calling webhookFixed

Kubernetes: Internal error occurred: failed calling webhook

admission webhook이 쓰기 요청 앞에 서 있는데 API server가 응답을 받지 못했고, 기본값인 failurePolicy: Fail이 그 침묵을 거부로 바꾼 것입니다. 메시지 끝부분이 곧 진단입니다 — context deadline exceeded는 호출이 도달하지 못한 것, no endpoints available은 떠 있는 게 없는 것, x509 줄은 API server가 webhook 인증서를 신뢰하지 않는 것입니다. 원인마다 해결이 다르고, 어느 것도 매니페스트 문제가 아닙니다.

Kubernetes
1205Fixed

MySQL: ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

다른 트랜잭션이 쥐고 있는 행 잠금을 innodb_lock_wait_timeout 만큼 기다리다 포기한 것입니다. sys.innodb_lock_waits 가 막고 있는 세션을 지목하고 KILL 문까지 만들어 주며, blocking_query 가 NULL 이면 블로커가 열린 트랜잭션 위에서 놀고 있다는 뜻입니다. 재시도 로직이 가장 자주 틀리는 지점은 따로 있습니다. 기본값에서는 타임아웃된 문장 하나만 롤백되므로, 트랜잭션은 그대로 열린 채 앞서 잡은 잠금을 전부 쥐고 있습니다.

MySQL
MISCONFFixed

Redis: MISCONF Redis is configured to save RDB snapshots, but it's currently unable to persist to disk

읽기는 되는데 모든 쓰기가 거부됩니다. 마지막 백그라운드 저장이 실패했고 stop-writes-on-bgsave-error 기본값이 yes 이기 때문입니다. 진짜 원인은 로그에 적혀 있습니다 — 디스크 공간 부족, redis 사용자가 쓸 수 없는 dir, rename 시점의 read-only 마운트, 또는 fork 가 Cannot allocate memory 로 실패하는 경우입니다. 원인을 고치고 BGSAVE 한 번만 돌리면 재시작 없이 쓰기가 돌아옵니다. rdb_last_bgsave_status 가 err 에서 ok 로 바뀝니다. stop-writes-on-bgsave-error no 는 쓰기를 즉시 되살리지만 스냅샷은 여전히 실패한 상태로 두므로, 해결이 아니라 의도한 맞교환으로 다뤄야 합니다.

Redis
FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryFixed

Node.js: FATAL ERROR: Reached heap limit — JavaScript heap out of memory (종료 코드 134)

V8 힙에는 시스템 메모리와 Node 릴리스에서 유도되는 자기만의 천장이 있고, 가진 RAM 보다 훨씬 낮은 경우가 흔합니다. 빌드나 서버가 거기 닿으면 V8 은 FATAL ERROR: Reached heap limit 과 종료 코드 134 로 스스로 중단합니다. v8.getHeapStatistics().heap_size_limit 으로 실제 한도를 읽은 뒤, 큰 작업은 --max-old-space-size(MiB)나 NODE_OPTIONS 로 올리고, 컨테이너 안에서는 cgroup 한도보다 낮게 잡고, 오래 도는 프로세스의 누수는 --heapsnapshot-near-heap-limit 으로 잡으세요. FATAL ERROR 줄 없이 137 로 끝나면 컨테이너 kill 이지 이 오류가 아닙니다.

Node.js
exec /docker-entrypoint.sh: exec format errorFixed

Docker: 컨테이너 시작 직후 "exec format error" — 플랫폼이 다른 이미지, 에뮬레이터 없음, 셔뱅 없는 스크립트

컨테이너가 첫 명령에서 exec format error 로 끝납니다. 커널의 ENOEXEC, 즉 파일은 있지만 여기서는 실행할 수 없다는 뜻입니다. 실제로는 한 CPU 아키텍처에서 빌드한 이미지(Apple 실리콘 Mac은 linux/arm64 를 만듭니다)를 binfmt_misc 에 QEMU 핸들러가 없는 다른 아키텍처(x86_64 서버)에서 돌리거나, 엔트리포인트 스크립트의 첫 줄이 셔뱅이 아닌 경우입니다. uname -m, docker image inspect, ls /proc/sys/fs/binfmt_misc 로 원인을 가르고, docker buildx build --platform 을 명시(또는 두 플랫폼 매니페스트 목록 발행)하거나, 에뮬레이션이 목적이면 QEMU 등록과 --platform, 스크립트라면 #!/bin/sh 한 줄로 해결합니다.

Docker