·오류 문구 사전

오류 문구를 그대로 찾는 사전

104개의 오류 문구를 무슨 뜻이고 왜 났고 어떻게 하는지로 풀었습니다.

고치는 명령에는 값이 따릅니다. git reset --hard는 커밋하지 않은 일을 버리고, 강제 푸시는 남의 커밋을 지우며, docker system prune은 이름 없는 볼륨을 지웁니다. 그런 자리는 설명에 그 값을 적어 두었습니다.

Git29

git의 오류는 거의 다 "지금 상태에서 그 일을 하면 무언가 잃는다"는 거절이고, fatal:은 아무것도 하지 않고 멈췄다는 뜻이니 첫 줄보다 그 아래 hint: 줄이 실제 해법을 담고 있는 경우가 많습니다.

fatal: refusing to merge unrelated histories합치려는 두 쪽이 공통 조상을 하나도 갖고 있지 않다는 뜻입니다 — git이 보기에 서로 남인 두 역사입니다. 보통 로컬에서 git init으로 시작해 커밋을 몇 개 쌓은 뒤, 이미 커밋이 들어 있는 원격을 origin으로 붙여 pull할 때 나옵니다. --allow-unrelated-histories를 주면 합쳐지기는 하지만 남남인 두 역사가 커밋 하나에서 억지로 묶여 나중에 갈라내기가 어렵습니다. 로컬 커밋이 몇 개뿐이라면 원격을 새로 clone하고 파일만 옮겨 담는 편이 깔끔합니다.Your branch and 'origin/main' have diverged내 브랜치와 origin/main이 서로 없는 커밋을 각각 갖고 있다는 뜻입니다 — 역사가 두 갈래로 벌어졌습니다. 내가 로컬에서 커밋하는 사이 남이 원격에 push했거나, 이미 올린 커밋을 amend나 rebase로 고쳐 썼을 때 이렇게 됩니다. git pull --rebase는 내 커밋을 원격 것 위에 다시 얹어 역사를 한 줄로 만들지만 내 커밋의 해시가 새로 생기므로, 그 커밋을 이미 남과 나눠 썼다면 같은 작업이 두 번 남습니다. 그럴 때는 병합 커밋 하나를 남기는 --no-rebase가 안전합니다.fatal: Need to specify how to reconcile divergent branches.git 2.34부터 pull은 두 갈래가 벌어졌을 때 병합할지 rebase할지를 스스로 고르지 않고 멈춥니다 — 설정이 없어서 물어보는 것이지, 저장소가 상한 것이 아닙니다. 갈래가 벌어진 상태에서 pull.rebase를 한 번도 정해 두지 않았을 때 나옵니다. pull.ff only는 앞으로 갈 수 있을 때만 당기고 아니면 거절하니 아무것도 만들지 않아 가장 안전하고, pull.rebase true는 내 로컬 커밋의 해시를 매번 새로 쓰며, false는 병합 커밋을 남깁니다.CONFLICT (content): Merge conflict in src/app.tsx양쪽이 같은 줄을 서로 다르게 고쳐서 git이 어느 쪽이 맞는지 정할 수 없다는 뜻입니다. git은 그 파일 안에 <<<<<<<와 =======와 >>>>>>> 표시로 두 판을 함께 써 넣고 멈춰 있습니다. 나와 남이 같은 자리를 건드렸거나, 그 자리를 고친 커밋을 지나 rebase할 때 이렇게 됩니다. 파일을 열어 맞는 모양으로 고치고 표시를 지운 뒤 git add하고 commit하면 끝납니다. git merge --abort는 병합 전 자리로 완전히 돌아가지만, 이미 손으로 풀어 둔 것은 함께 사라집니다 — 커밋해 둔 것은 그대로 남습니다.Please commit your changes or stash them before you merge.받아 올 커밋이 건드리는 파일에 아직 커밋하지 않은 수정이 있어서, git이 그것을 덮어쓰기보다 멈추기를 골랐다는 뜻입니다. 작업 중인 파일을 그대로 두고 pull이나 merge를 했을 때 나옵니다. git stash push -u는 그 수정을 잠시 치워 두어 병합이 지나가게 하고 -u로 추적하지 않는 파일까지 함께 담습니다. 다만 git stash pop으로 돌려놓을 때 그 자리에서 다시 충돌할 수 있고, stash는 브랜치처럼 눈에 보이지 않아 잊고 잃기 쉬우므로 임시 커밋을 하나 만드는 편이 더 안전합니다.error: Your local changes to the following files would be overwritten by checkout:브랜치를 갈아타려는데 그 브랜치에서 내용이 다른 파일에 아직 커밋하지 않은 수정이 있다는 뜻입니다. 작업을 반쯤 해 놓고 다른 브랜치로 switch나 checkout을 할 때 나옵니다. git stash push -u로 치워 두거나 임시 커밋을 만들면 갈아탈 수 있고, 나중에 되돌릴 수 있습니다. 검색하면 흔히 나오는 git checkout --force나 git switch --discard-changes는 확실히 갈아타 주지만 커밋하지 않은 그 수정을 영구히 지웁니다 — reflog에도 남지 않아 되찾을 방법이 없습니다.You are currently rebasing branch 'feature' on '8a3f21c'.rebase가 시작되어 중간에 멈춰 있고, HEAD가 브랜치가 아니라 작업 중인 임시 상태에 놓여 있다는 뜻입니다. rebase 도중에 충돌이 나거나 git rebase -i의 edit·break에서 멈췄는데, 끝내지 않고 다른 일을 했을 때 이 줄을 보게 됩니다. 충돌을 풀고 git add한 뒤 git rebase --continue로 이어가거나, 그 커밋을 버릴 생각이면 --skip을 씁니다. git rebase --abort는 rebase를 시작하기 전 자리로 정확히 되돌리므로 커밋해 둔 것은 하나도 잃지 않지만, 지금까지 손으로 푼 충돌은 함께 사라집니다.You are in 'detached HEAD' state.HEAD가 브랜치 이름이 아니라 커밋 하나를 직접 가리키고 있다는 뜻입니다 — 여기서 새로 커밋하면 그 커밋에는 이름이 붙지 않습니다. 커밋 해시나 태그로 checkout했거나, 서브모듈 안에 있거나, CI가 특정 커밋을 checkout했을 때 이 상태가 됩니다. 여기서 한 일을 남기려면 git switch -c 이름으로 브랜치를 만들면 되고, 이것은 아무것도 잃지 않습니다. 그냥 git switch main으로 나가면 여기서 만든 커밋은 가리키는 이름이 없어져 reflog로 한동안 되찾을 수 있을 뿐이고, gc가 돌면 사라집니다.Warning: you are leaving 1 commit behind, not connected to any of your branches:detached HEAD 상태에서 커밋을 했고, 이제 그 자리를 떠나는데 그 커밋들을 가리키는 브랜치가 하나도 없다는 경고입니다. 커밋 해시로 checkout해 작업하고 커밋한 뒤, 다시 원래 브랜치로 돌아갈 때 나옵니다. 메시지에 함께 찍힌 해시를 그대로 써서 git branch rescue 8a3f21c 하면 이름이 붙어 안전해지고, 이 명령은 아무것도 바꾸지 않고 이름 하나만 더합니다. 이미 떠나 버렸다면 git reflog에 아직 남아 있지만, 가리키는 것이 없는 커밋은 gc가 돌면 지워집니다 — 기본은 대략 30일입니다.error: failed to push some refs to 'https://github.com/user/repo.git'서버가 push를 거절했고, 이 줄은 결과를 요약한 것일 뿐이라는 뜻입니다 — 진짜 이유는 이 줄 바로 위의 ! [rejected] 줄에 적혀 있습니다. 대개는 원격에 내가 갖고 있지 않은 커밋이 있어서지만, 보호된 브랜치나 서버 쪽 훅, 파일 크기 제한일 때도 같은 줄이 나옵니다. 먼저 위의 이유를 읽고, 흔한 경우라면 git pull --rebase && git push로 끝납니다. 이유를 읽지 않고 --force를 붙이는 것이 가장 위험합니다 — 그것은 서버에 있는 남의 커밋을 지울 수 있습니다.! [rejected] main -> main (fetch first)원격 브랜치에 내 저장소가 한 번도 본 적 없는 커밋이 있어서, 지금 push하면 앞으로 나아가는 것이 아니게 된다는 뜻입니다. 동료가 먼저 push했거나, 마지막으로 fetch한 지 오래되었을 때 나옵니다. git pull --rebase origin main으로 그 커밋을 받아들이고 내 것을 그 위에 다시 얹은 뒤 push하면 됩니다. 여기서 --force는 절대 쓰지 않습니다 — 덮어쓰게 될 커밋은 남의 것이고 내 저장소에는 아예 없으므로, 로컬의 무엇으로도 되찾을 수 없습니다.! [rejected] main -> main (non-fast-forward)내 브랜치가 원격 브랜치의 자손이 아니라는 뜻입니다 — 그대로 밀어 넣으면 원격에 있는 커밋이 역사에서 떨어져 나갑니다. rebase나 amend, reset으로 이미 push한 커밋을 고쳐 썼을 때 이렇게 됩니다. 고쳐 쓴 것이 의도였고 그 브랜치가 나 혼자 쓰는 것이라면 git push --force-with-lease를 씁니다. 이것은 내가 마지막으로 fetch한 뒤 원격이 움직였으면 거절하므로, 그냥 --force와 달리 그 사이에 들어온 동료의 push를 조용히 지우지 않습니다. 의도한 것이 아니라면 --force가 아니라 pull --rebase가 답입니다.Updates were rejected because the remote contains work that you do not have locally.push가 거절된 이유를 풀어 쓴 안내입니다 — 원격에 내 저장소가 갖고 있지 않은 작업이 들어 있으니, 지금 밀어 넣으면 그것이 없어진다는 뜻입니다. 같은 브랜치를 여러 사람이 쓰거나, 웹에서 파일을 고쳐 커밋한 것을 아직 받아 오지 않았을 때 나옵니다. git pull --rebase origin main으로 먼저 받고 나서 push하면 그대로 지나갑니다. 이 안내를 무시하고 --force를 붙이는 것이 정확히 안내가 막으려던 일입니다 — 서버에서 남의 커밋이 사라지고, 그 사람이 push한 뒤로 fetch한 사람이 없으면 어디에도 남지 않습니다.error: src refspec main does not match anypush하라고 준 이름이 로컬에 브랜치로도 태그로도 없다는 뜻입니다 — 없는 것을 보낼 수는 없습니다. 저장소를 막 만들어 커밋이 하나도 없어서 main이라는 이름이 아직 생기지 않았거나, 로컬 브랜치는 master인데 main이라고 쳤거나, 그저 오타일 때 나옵니다. git push -u origin HEAD는 지금 서 있는 브랜치를 그 브랜치의 실제 이름으로 올려 주므로 이름이 어긋난 경우를 한 번에 넘깁니다. 커밋이 아직 없다면 먼저 커밋을 하나 만들어야 합니다 — 빈 저장소에는 올릴 것이 없습니다.fatal: The current branch feature has no upstream branch.이 브랜치에 짝이 되는 원격 브랜치가 기록되어 있지 않아서, 인자 없는 git push나 git pull이 어디로 가야 하는지 모른다는 뜻입니다. git switch -c로 브랜치를 새로 만들고 한 번도 올린 적이 없을 때 나옵니다. git push --set-upstream origin feature는 서버에 그 브랜치를 만들고 짝을 한 번만 기록해 두므로, 그다음부터는 git push만 쳐도 됩니다. 이 명령은 로컬 설정 한 줄과 서버의 새 브랜치 하나만 남기니 무엇도 지우지 않고 여러 번 실행해도 안전합니다.error: cannot lock ref 'refs/remotes/origin/main': is at 8a3f21c but expected 1c2d3e4git이 원격 추적 참조를 새 값으로 바꾸려는 순간, 그 참조에 방금 읽은 것과 다른 값이 들어 있어서 덮어쓰기를 거절했다는 뜻입니다. 편집기가 뒤에서 자동으로 fetch하는 중에 내가 또 fetch했거나, 상대가 force push해서 원격 추적 참조가 어긋나 있을 때 흔히 납니다. 대개는 fetch를 한 번 더 하면 그냥 지나갑니다. git remote prune origin은 서버에서 사라진 브랜치의 원격 추적 참조를 정리하는데, 지우는 것은 내 저장소 안의 사본뿐이고 서버의 브랜치는 건드리지 않습니다.fatal: not a git repository (or any of the parent directories): .git지금 디렉터리에도, 그 위 어느 상위 디렉터리에도 .git이 없다는 뜻입니다 — git이 저장소를 못 찾았습니다. 저장소보다 한 칸 위나 아래에 서 있거나, clone이 다른 폴더에 들어갔거나, git init을 아직 안 했을 때 나옵니다. 먼저 pwd로 자기가 어디에 서 있는지 확인하고, 정말 여기서 저장소를 시작할 생각일 때만 git init을 씁니다. 이미 저장소인 곳의 하위 폴더에서 git init을 하면 안쪽에 두 번째 저장소가 생겨 바깥 저장소를 조용히 가려 버립니다 — 그때부터 그 폴더의 파일은 바깥 커밋에 들어가지 않습니다.fatal: remote origin already exists.이 저장소에 origin이라는 이름이 이미 쓰이고 있어서 같은 이름을 또 만들 수 없다는 뜻입니다. clone으로 받은 저장소에는 origin이 처음부터 있는데, 새 저장소를 만드는 안내문을 그 안에서 그대로 따라 git remote add origin을 쳤을 때 나옵니다. 먼저 git remote -v로 지금 origin이 어디를 가리키는지 보고, 주소를 바꾸려는 것이었다면 git remote set-url origin으로 가리키는 곳만 고칩니다. 이 설정은 .git/config 한 줄이라 언제든 되돌릴 수 있고 커밋에는 영향이 없습니다.error: pathspec 'featue' did not match any file(s) known to git그 이름으로 된 파일도 브랜치도 태그도 git이 찾지 못했다는 뜻입니다 — 오류의 따옴표 안에 든 문자열이 git이 실제로 찾아본 이름입니다. 오타이거나, 그 브랜치가 서버에만 있고 아직 fetch하지 않았거나, 파일이 생각한 것과 다른 디렉터리에 있을 때 나옵니다. 브랜치라면 git fetch 뒤 git branch -a로 이름을 눈으로 맞춰 보고, 파일이라면 git status --short로 실제 경로를 봅니다. git 명령의 경로는 저장소 뿌리가 아니라 지금 서 있는 디렉터리를 기준으로 읽히니, 그것만으로 어긋날 때도 많습니다.git@github.com: Permission denied (publickey).ssh가 서버에는 닿았지만, 서버가 받아 주는 열쇠를 하나도 내놓지 못했다는 뜻입니다 — 네트워크 문제가 아니라 인증 문제입니다. 열쇠를 아직 만들지 않았거나, 만들었어도 ssh-agent에 올리지 않았거나, 공개키를 그 서비스 계정에 등록하지 않았을 때 나옵니다. ssh-add ~/.ssh/id_ed25519로 이번 세션 동안 열쇠를 agent에 올리고, ssh -T git@github.com으로 어느 열쇠가 시도되었고 누구로 인식되는지 확인합니다. 두 명령 모두 읽기만 하니 잃을 것이 없습니다.remote: Support for password authentication was removed on August 13, 2021.GitHub이 2021년에 https로 계정 비밀번호를 받는 것을 끊었다는 뜻입니다 — 비밀번호가 틀린 것이 아니라 비밀번호라는 방식 자체를 더 받지 않습니다. 저장해 둔 자격 증명이 옛 비밀번호이거나, 물음에 비밀번호를 그대로 입력했을 때 나옵니다. https를 계속 쓸 것이면 비밀번호 칸에 personal access token을 넣고, 아니면 git remote set-url origin으로 ssh 주소로 바꿉니다. 원격 주소를 바꾸는 것은 내 저장소의 설정 한 줄일 뿐이어서 커밋에 손대지 않고 언제든 되돌릴 수 있습니다.fatal: Authentication failed for 'https://github.com/user/repo.git/'보낸 자격 증명이 거절되었다는 뜻입니다 — 틀렸거나, 기간이 지났거나, 그 저장소에 필요한 권한이 없는 토큰입니다. 흔한 함정은 만료된 토큰이 자격 증명 도우미에 그대로 남아 있는 경우로, 물어보지도 않고 그 토큰을 보내 매번 같은 줄이 나옵니다. fix에 적힌 git credential reject 한 줄은 저장된 그 항목만 지워서 다음번에 다시 물어보게 만드니, 새 토큰을 붙여 넣을 수 있습니다. 지우는 것은 저장된 자격 증명뿐이고 저장소의 어떤 데이터도 건드리지 않습니다.fatal: Unable to create '/repo/.git/index.lock': File exists.git은 색인을 쓸 때 .git/index.lock을 잠금 삼아 만드는데, 그 파일이 이미 있다는 것은 다른 git이 지금 돌고 있거나 아까 죽으면서 남겨 두었다는 뜻입니다. 편집기나 IDE가 뒤에서 git을 돌리고 있을 때, 또는 명령을 Ctrl+C나 강제 종료로 끊었을 때 남습니다. 돌고 있는 git이 없는지 확인한 뒤 rm -f .git/index.lock으로 지웁니다. 정말로 일하고 있는 git이 있는데 지우면 색인이 깨질 수 있으니 먼저 확인하고, 색인이 어긋났다면 git reset으로 HEAD에서 다시 만들 수 있습니다 — --hard를 붙이지 않으면 파일은 그대로입니다.nothing to commit, working tree clean오류가 아니라, 마지막 커밋과 다른 것이 하나도 없다고 git이 말하는 것입니다 — 커밋할 것이 없어서 아무 일도 하지 않았습니다. 고친 파일이 .gitignore에 걸려 있거나, 편집한 곳이 지금 서 있는 클론이나 워크트리와 다른 자리이거나, 이미 커밋했는데 잊었을 때 이 줄을 보게 됩니다. git check-ignore -v 경로는 그 파일을 가리고 있는 .gitignore의 줄 번호까지 찍어 주고, git log -1로는 그 변경이 이미 들어갔는지 확인합니다. 둘 다 읽기만 하는 명령입니다.error: unable to unlink old 'dist/main.js': Permission deniedcheckout이 파일을 새 판으로 갈아 끼우려는데 운영체제가 옛 파일을 지우지 못하게 막았다는 뜻입니다. 다른 프로그램이 그 파일을 열어 두고 있거나(윈도우에서 특히 흔합니다), 그 디렉터리에 내가 쓸 권한이 없을 때 나옵니다. 그 파일을 붙잡고 있는 것 — 개발 서버, 편집기, 백신 — 을 닫고 같은 명령을 다시 실행하면 되고, 유닉스라면 디렉터리 권한을 고칩니다. 같은 명령을 되풀이해도 안전합니다. 다만 git이 중간에 멈춘 상태라 성공할 때까지 작업 디렉터리는 반쯤 갈린 채로 남아 있습니다.fatal: bad object 8a3f21c준 이름이 git이 읽을 수 있는 것으로 풀리지 않는다는 뜻입니다 — 그 객체가 이 저장소에 없거나, 있는데 상해 있습니다. 다른 클론이나 shallow clone에서 복사해 온 해시, 잘려서 자릿수가 모자란 해시, 또는 디스크 사고 뒤 정말로 망가진 객체일 때 나옵니다. git fsck --full은 없는 객체와 깨진 객체를 읽기만 하며 보고하고, 남에게서 받은 해시라면 먼저 git fetch를 해야 그 객체가 들어옵니다. fsck가 손상을 보고하면 고치려 들지 말고 다시 clone하는 편이 빠르고 확실합니다 — 커밋하지 않은 파일만 먼저 다른 곳에 복사해 두면 됩니다.warning: LF will be replaced by CRLF in package.json.오류가 아니라 알림입니다 — core.autocrlf가 켜져 있어서 커밋에는 LF로 넣지만 작업 디렉터리의 사본에는 CRLF를 써 넣겠다는 뜻입니다. 윈도우에서 git을 기본값으로 설치하면 autocrlf가 true가 되므로, 그 컴퓨터에서 add할 때마다 나옵니다. git config core.autocrlf input으로 두면 커밋에는 LF로 넣고 꺼낼 때는 바꾸지 않으며, 더 나은 방법은 저장소에 .gitattributes를 두고 * text=auto eol=lf를 적어 모든 클론이 같게 동작하게 하는 것입니다. 설정을 바꾸면 모든 파일이 한 번 수정된 것처럼 보일 수 있는데, 이는 한 번만 다시 꺼내면 끝나는 일이고 작업이 사라지는 것은 아닙니다.The file will have its original line endings in your working directory앞의 CRLF 알림과 한 쌍인 뒷줄로, 디스크에 있는 파일은 지금 가진 줄 끝을 그대로 유지하고 커밋에 들어가는 사본만 정규화된다는 뜻입니다. 같은 autocrlf 설정 때문에 나오는 것이라 그 자체로 고장난 것은 없습니다. 다만 diff에 파일 전체가 바뀐 것처럼 보인다면 사람마다 줄 끝이 다르다는 신호이니, 저장소에 .gitattributes를 하나 두어 규칙을 못 박고 git add --renormalize . 를 한 번 실행합니다. 이 명령은 줄 끝을 저장하는 방식만 다시 쓰고 파일의 내용을 바꾸지 않습니다.husky - pre-commit hook exited with code 1 (error)커밋이 만들어지지 않았다는 뜻입니다 — lint나 테스트, 서식 검사 같은 훅이 0이 아닌 코드로 끝나서 git이 커밋을 중단했습니다. 진짜 이유는 이 줄이 아니라 그 위에 있는 훅 자신의 출력에 있고, 대개 lint 규칙이나 타입 오류입니다. 훅이 지적한 것을 고쳐서 다시 커밋하는 것이 유일한 정답입니다. git commit --no-verify는 훅을 전부 건너뛰고 커밋을 만들어 주지만, 막힌 검사를 통과시킨 것이 아니라 CI로 미룬 것뿐이고 서식이 어긋난 코드가 동료에게 그대로 갑니다.

npm20

npm ERR!로 시작하는 줄은 대개 마지막 여섯 줄이 아니라 첫 code XXXX 한 줄에 원인이 적혀 있고, 오류가 내 코드가 아니라 의존성 나무나 네이티브 빌드에서 온 것이면 node_modules를 지우고 다시 설치하는 것으로 절반이 풀립니다.

npm ERR! ERESOLVE unable to resolve dependency treenpm 7부터는 peerDependencies의 버전 범위를 강제하는데, 모든 범위를 동시에 만족시키는 버전 조합을 찾지 못했다는 뜻입니다. 새로 설치하려는 패키지의 peer 범위가 내가 이미 쓰고 있는 react나 typescript 버전을 배제할 때 — 특히 메이저 버전을 올린 직후에 — 흔히 납니다. 출력의 Found:와 Could not resolve: 줄을 읽어 누가 무엇을 요구하는지 보고 둘 중 하나를 맞는 버전으로 옮기는 것이 진짜 해결입니다. npm install --legacy-peer-deps는 peer 범위를 아예 무시하고 설치해 당장 막힌 것을 풀어 주지만, 패키지들이 동의하지 않은 나무를 남기므로 버전이 어긋나서 나는 실행 시 오류는 내가 감당해야 합니다.npm ERR! Conflicting peer dependency: react@18.3.1ERESOLVE 보고 안에서 실제로 부딪힌 짝을 집어 준 줄입니다 — 이 패키지는 그 peer의 그 버전을 요구하는데, 내 나무에는 다른 버전이 들어 있다는 뜻입니다. 대개 본체 라이브러리가 메이저 버전을 올렸는데 그것을 쓰는 플러그인이 아직 따라오지 못했을 때 이렇게 됩니다. npm ls react처럼 그 이름으로 물어보면 누가 어느 버전을 요구했는지 나무 모양으로 나오고, 둘 중 무엇을 움직여야 하는지가 그 자리에서 보입니다. 플러그인을 새 버전으로 올리는 것이 진짜 해결이고, --force는 어긋난 나무를 그대로 써 버려 문제를 실행할 때까지 감춥니다.npm WARN react-dom@17.0.2 requires a peer of react@17.0.2 but none is installed.npm 6은 peer 의존성을 경고만 하고 대신 설치해 주지 않았다는 뜻입니다 — 그 패키지는 들어왔지만 그것이 필요하다고 말한 짝은 없습니다. npm 6을 쓰는 프로젝트이거나, 그 시절에 만들어진 오래된 lock 파일을 그대로 쓰고 있을 때 나옵니다. 경고에 적힌 범위 안에 드는 버전으로 그 peer를 직접 설치하면 끝납니다. 지금은 경고일 뿐이어서 아무것도 실패하지 않았지만, 나중에 Cannot find module로 나타나거나 같은 라이브러리가 두 벌 들어가 이상한 오류를 내는 식으로 되돌아옵니다.npm WARN deprecated request@2.88.2: request has been deprecated그 패키지의 만든 사람이 그 버전을 더 이상 권하지 않는다고 표시해 두었다는 뜻입니다 — 설치는 되었고 지금도 돌아갑니다. 대개 내 package.json에 적은 것이 아니라, 내가 쓰는 어떤 패키지가 딸려 데려온 것입니다. npm ls request처럼 그 이름으로 물어보면 어느 직접 의존성이 그것을 끌고 오는지 나오고, 고치는 자리는 그 직접 의존성을 올리는 쪽이지 이 패키지 자체가 아닙니다. 오늘 당장 할 일은 없습니다 — deprecated는 보안 취약점이 아니고, 그것은 npm audit이 따로 말해 줍니다.npm ERR! npm ci can only install packages when your package.json and package-lock.json or npm-shrinkwrap.json are in sync.npm ci는 lock 파일에 적힌 그대로만 설치하는데, package.json이 lock 파일에 기록되지 않은 것을 요구하고 있어서 시작조차 하지 않았다는 뜻입니다. 누군가 package.json을 손으로 고치거나 그 파일의 충돌을 풀고서 install을 돌리지 않았거나, package.json만 커밋하고 lock 파일을 함께 올리지 않았을 때 나옵니다. 로컬에서 npm install을 한 번 돌려 lock 파일을 맞춘 뒤 그 lock 파일을 커밋하면 그것이 고치는 전부입니다. package-lock.json을 지워서 넘기면 모든 의존성이 더 새 버전으로 다시 풀려, 빌드에 들어가는 내용이 조용히 달라집니다.npm ERR! The `npm ci` command can only install with an existing package-lock.json or npm-shrinkwrap.jsonnpm ci는 lock 파일 자체가 없으면 아예 돌지 않는다는 뜻입니다 — 어느 버전을 넣어야 하는지 적힌 곳이 없기 때문입니다. package-lock.json이 .gitignore에 걸려 있거나, 한 번도 커밋된 적이 없거나, 지금 서 있는 디렉터리가 프로젝트 뿌리가 아닐 때 나옵니다. npm install --package-lock-only는 아무것도 설치하지 않고 lock 파일만 만들어 주므로, 그것을 커밋하면 CI가 돕니다. package-lock.json을 .gitignore에 넣지 않습니다 — CI가 같은 것을 두 번 짓는다는 보장이 바로 그 파일 하나에 달려 있습니다.npm ERR! Invalid Version:package.json의 version 값이 올바른 semver가 아니어서 npm이 그 패키지를 아예 읽어 들일 수 없다는 뜻입니다. 1.0이나 v1.0.0, 빈 문자열처럼 손으로 고쳐 넣은 값일 때, 특히 그 파일의 충돌을 풀다가 어긋났을 때 나옵니다. npm pkg set version=1.0.0으로 세 자리 버전을 다시 써 주면 됩니다. 이 명령은 package.json만 고치고 node_modules나 lock 파일을 건드리지 않으므로, 저장한 뒤에는 그 파일을 읽는 모든 명령이 곧바로 다시 돌아갑니다.npm ERR! 404 Not Found - GET https://registry.npmjs.org/@acme/ui - Not found레지스트리에 그 이름의 패키지가 없다는 뜻입니다 — 404는 이름에 대한 답이지, 네트워크나 자격 증명에 대한 답이 아닙니다. 이름을 잘못 적었거나, 그 패키지가 unpublish되었거나, 로그인하지 않은 비공개 scope일 때 나옵니다. 비공개 패키지는 권한이 없는 사람에게 존재하지 않는 패키지와 똑같이 보이기 때문에 이 셋이 한 줄로 겹칩니다. npm view @acme/ui version으로 그 이름이 공개적으로 있는지 먼저 확인하고, 비공개 scope라면 .npmrc에 그 scope의 레지스트리와 토큰이 적혀 있는지 봅니다. 두 확인 모두 읽기만 합니다.npm ERR! request to https://registry.npmjs.org/express failed, reason: unable to verify the first certificateTLS 연결이 실패했다는 뜻입니다 — 서버가 내놓은 인증서 사슬이 node가 믿지 않는 기관의 서명으로 이어져 있습니다. 회사 네트워크에서 프록시가 트래픽을 열어 보고 자기 인증서로 다시 서명하는 경우가 압도적으로 많고, 드물게는 서버가 중간 인증서를 함께 보내지 않은 경우입니다. npm config set cafile로 회사의 루트 인증서를 npm에게 알려 주는 것이 올바른 해결입니다. strict-ssl false로도 이 줄은 사라지지만 그것은 인증서 검사를 아예 끄는 것이어서, 경로에 있는 누구든 바뀐 패키지를 내려 줄 수 있게 됩니다 — 쓰지 않습니다.npm ERR! code EINTEGRITY내려받은 압축 파일의 해시가 lock 파일에 적힌 값과 다르다는 뜻입니다 — npm이 받은 것을 믿을 수 없다고 판단해 멈췄습니다. 캐시에 든 사본이 깨졌거나, 프록시가 내려오는 파일을 손댔거나, 드물게는 lock 파일을 손으로 고치거나 병합을 잘못 풀어 integrity 값이 어긋난 경우입니다. npm cache clean --force로 로컬 캐시를 비우면 다음 설치가 다시 받아 오고, 이것은 캐시 사본만 지우니 되풀이해도 안전합니다. 특정 패키지에서만 계속 난다면 기록된 integrity 값 자체가 틀린 것이므로 npm install로 그 항목을 다시 풀어야 합니다.npm ERR! Error: EACCES: permission denied, access '/usr/local/lib/node_modules'npm이 내 사용자가 소유하지 않은 시스템 디렉터리에 쓰려 했다는 뜻입니다 — 패키지가 잘못된 것이 아니라 그 자리에 쓸 권한이 없습니다. 운영체제의 패키지 관리자가 /usr/local 같은 곳에 깔아 둔 node로 npm install -g를 했을 때 거의 언제나 이것입니다. npm config set prefix ~/.npm-global로 전역 설치 자리를 홈 디렉터리로 옮기고 그 안의 bin을 PATH에 넣으면 이 오류가 다시 나지 않습니다. sudo npm install -g도 되기는 하지만 캐시와 node_modules에 root 소유 파일을 남겨 나중에 평범한 설치에서 같은 EACCES를 만들고, nvm 같은 버전 관리자를 쓰면 이 부류 전체가 사라집니다.npm ERR! enoent ENOENT: no such file or directory, open '/home/me/package.json'npm이 지금 디렉터리와 그 위쪽을 뒤져 package.json을 찾았지만 하나도 없었다는 뜻입니다 — 프로젝트가 아닌 곳에서 npm 명령을 쳤습니다. 모노레포의 뿌리에 서 있는데 프로젝트가 하위 폴더에 있거나, clone한 폴더 옆에 서 있거나, 그냥 cd를 잊었을 때 나옵니다. package.json이 있는 폴더로 옮기는 것이 답이고, ls로 눈으로 확인하는 것이 가장 빠릅니다. npm init -y는 정말로 여기서 새 프로젝트를 시작할 생각일 때만 씁니다 — 엉뚱한 폴더에서 돌리면 남는 package.json 하나가 나중에 도구들을 헷갈리게 만듭니다.npm ERR! code ELIFECYCLEpackage.json의 script 하나가 0이 아닌 코드로 끝났다는 뜻입니다 — ELIFECYCLE은 npm이 그것을 감싼 껍데기이지 원인이 아닙니다. 원인은 내 build나 test script가 한 일이고, 진짜 오류 줄은 이 줄보다 위에 있습니다. 위로 올라가 첫 번째 오류를 찾거나, npm ERR! <pkg>@<ver> <script>: 뒤에 찍힌 실제 명령을 그대로 손으로 실행해 보면 npm의 껍데기 없이 원래 출력을 볼 수 있습니다. 뒤에 붙는 종료 코드도 조금 알려 줍니다 — 1은 평범한 실패이고, 137은 메모리 때문에 프로세스가 강제 종료된 것입니다.gyp ERR! build error의존성 안에 C나 C++로 된 부분이 있어서 설치할 때 그 자리에서 컴파일해야 하는데 그 컴파일이 실패했다는 뜻입니다 — node-gyp는 컴파일러와 python을 필요로 합니다. 빌드 도구가 아예 깔려 있지 않거나, 그 패키지가 내 node 버전보다 오래되어 헤더가 맞지 않을 때 나옵니다. macOS에서는 xcode-select --install로 명령줄 도구를, 데비안·우분투에서는 build-essential과 python3을, 윈도우에서는 Visual Studio의 C++ 워크로드를 깔면 됩니다. 그 패키지의 새 버전이 내 node용 미리 빌드된 바이너리를 제공한다면, 빌드 환경을 고치기보다 패키지를 올리는 편이 훨씬 빠릅니다.Error: Cannot find module 'express'node가 그 이름을 어떤 파일로도 풀어내지 못했다는 뜻입니다 — node_modules 안에 없거나, node_modules가 없는 디렉터리에서 실행했습니다. 설치를 아예 안 했거나, 설치가 중간에 실패해 나무가 반쯤만 만들어졌거나, 그 패키지가 devDependencies에 있는데 --omit=dev로 설치했을 때 나옵니다. rm -rf node_modules && npm install은 lock 파일에서 나무를 처음부터 다시 세우며, 안전하고 잃을 것이 없고 드는 값은 내려받는 시간뿐입니다. 다만 따옴표 안의 이름이 ./로 시작하는 상대 경로라면 패키지가 없는 것이 아니라 내 코드의 경로가 틀린 것입니다.Module not found: Error: Can't resolve './components/Button' in '/app/src'번들러가 그 경로에서 그 파일을 찾지 못했다는 뜻입니다 — 이것은 패키지가 아니라 내가 쓴 import입니다. 상대 경로가 한 칸 어긋났거나, 확장자가 빠졌거나 잘못되었거나, 파일 이름의 대소문자가 import와 다를 때 나옵니다. 마지막 것이 가장 고약합니다 — macOS와 윈도우의 파일시스템은 대소문자를 구분하지 않아 내 컴퓨터에서는 돌아가고 리눅스 CI에서만 깨집니다. 그 디렉터리를 ls로 찍어 import와 한 글자씩, 대소문자까지 맞춰 봅니다. 이름이 경로가 아니라 패키지라면 그것을 설치하면 되고, 어느 쪽이든 지워지는 것은 없습니다.Error: error:0308010C:digital envelope routines::unsupportednode 17이 OpenSSL 3을 싣게 되면서 옛 해시 알고리즘이 기본에서 빠졌고, 그것을 쓰는 도구가 해시를 부르는 자리에서 그대로 넘어졌다는 뜻입니다. 오래된 webpack 4나 그것을 안에 품은 도구를 요즘 node에서 돌릴 때 거의 언제나 이것입니다. export NODE_OPTIONS=--openssl-legacy-provider는 그 프로세스에 한해 옛 알고리즘을 다시 켜 주고, 빌드에 쓰는 데는 해가 없습니다. 다만 그것은 죽은 도구를 억지로 살려 두는 것이라, webpack 5나 프레임워크의 현재 버전으로 올리면 이 옵션 자체가 필요 없어집니다.FATAL ERROR: Reached heap limit Allocation failed - JavaScript heap out of memoryV8의 힙이 상한에 닿아 node가 스스로 포기했다는 뜻입니다 — 운영체제가 죽인 것이 아니라 node가 더 늘릴 수 없다고 판단해 멈춘 것입니다. 큰 프로젝트의 타입 검사나 번들, 소스맵을 켠 빌드, 또는 모든 것을 배열에 쌓아 두는 식의 새는 코드가 원인입니다. export NODE_OPTIONS=--max-old-space-size=4096으로 상한을 4GB까지 올리면 대개 넘어가지만, 기계에 그만큼 실제 메모리가 있어야 하고, 컨테이너의 한도가 더 낮으면 이번에는 운영체제가 프로세스를 죽여 종료 코드 137이 나옵니다. 숫자를 계속 키워야 한다면 원인은 상한이 아니라 새는 곳입니다.npm WARN EBADENGINE Unsupported engine어떤 패키지가 engines에 적어 둔 node나 npm 버전 범위를 내 환경이 만족하지 못한다는 뜻입니다 — npm은 경고만 하고 그대로 설치합니다. 시스템 패키지로 깔린 낡은 node를 쓰고 있거나, 프로젝트가 내 셸의 node보다 새 버전으로 옮겨 갔을 때 나옵니다. 버전 관리자로 node를 그 범위 안의 버전으로 바꾸면 되고, 믿을 기준은 프로젝트의 .nvmrc나 package.json의 engines 항목입니다. 기본값에서는 경고일 뿐이지만, .npmrc에 engine-strict=true가 있으면 같은 조건이 설치를 멈추는 오류로 바뀝니다.zsh: command not found: tsc셸이 PATH를 훑었지만 그 이름의 실행 파일을 찾지 못했다는 뜻입니다 — 설치 자체는 성공했을 수 있습니다. npm의 전역 bin 디렉터리가 PATH에 없거나, 전역이 아니라 로컬로 설치해서 실행 파일이 node_modules/.bin에 들어갔거나, 셸이 예전 PATH를 캐시에 물고 있을 때 나옵니다. npx tsc --version은 PATH를 손대지 않고 로컬 사본을 그대로 실행하므로, 설치가 되어 있는지 없는지를 그 한 줄로 갈라 줍니다. 정말로 전역에 설치한 것이라면 npm prefix -g의 출력 뒤에 /bin을 붙인 경로를 PATH에 더하고 셸을 새로 열면 됩니다.

JavaScript15

브라우저와 node의 오류는 대개 무엇이 잘못되었는지만 알려 주고 왜 그 값이 그렇게 되었는지는 알려 주지 않아서 — undefined를 읽었다, 함수가 아니었다, JSON이 아니었다 — 고칠 자리는 터진 줄이 아니라 그 값을 만든 위쪽에 있고, ?.와 기본값으로 그 줄만 조용하게 만들면 같은 문제가 더 먼 곳에서 더 알아보기 어려운 꼴로 다시 나타납니다.

Uncaught TypeError: Cannot read properties of undefined (reading 'name')점 왼쪽의 값이 undefined인데 그 속성을 읽으려 했다는 뜻이고, 괄호 안의 이름이 읽으려던 속성이니 잘못된 것은 그 앞의 값입니다. 아직 도착하지 않은 응답, 옵션으로 넘기지 않은 prop, 한 겹 깊이를 잘못 짚은 data.user.name 같은 자리에서 나며, 배열이 비어 있을 때 arr[0].id도 여기로 옵니다. 값이 없을 수 있는 자리라면 user?.name으로 짧게 끊을 수 있지만, 반드시 있어야 하는 값이었다면 ?.는 오류를 undefined로 바꿔 다음 줄로 미루는 것일 뿐입니다 — 왜 undefined인지를 위에서 찾으세요. Chrome 78 이전에는 같은 오류가 Cannot read property 'name' of undefined로 적혔습니다.TypeError: items.map is not a function그 이름이 있기는 하지만 함수가 아니라는 뜻이고, 이름이 아예 없으면 문구가 undefined 쪽으로 나옵니다. 배열이라고 생각한 것이 실은 객체나 NodeList일 때, API가 배열 대신 {data: [...]}를 돌려줄 때, 또는 import한 모듈의 기본 내보내기를 잘못 짚었을 때 납니다. 무엇인지 먼저 찍어 보세요 — console.log(typeof items, items) 한 줄이 이 오류의 절반을 그 자리에서 끝냅니다. NodeList나 Set이면 Array.from(items)로 감싸면 되고, {data: [...]}였다면 감싸지 말고 items = res.data로 바로잡는 것이 맞습니다.RangeError: Maximum call stack size exceeded호출이 쌓이는 깊이가 엔진의 한도를 넘었다는 뜻이고, 대개 깊은 재귀가 아니라 끝나지 않는 재귀입니다. 멈추는 조건이 없거나 닿지 않는 함수, 서로를 부르는 두 함수, 자기 자신을 담고 있는 객체를 JSON.stringify로 넘기는 경우, React라면 렌더 중에 자기가 의존하는 상태를 바꾸는 경우가 원인입니다. 콘솔의 스택에서 되풀이되는 두세 프레임을 보면 고리가 바로 보이니, 거기서 멈추는 조건을 고치세요. 깊이가 정말 필요한 계산이라면 재귀를 반복문이나 명시적인 스택으로 바꾸는 것이 유일한 길입니다 — 브라우저의 스택 한도는 늘릴 수 없습니다.SyntaxError: Unexpected token '<', "<!DOCTYPE "... is not valid JSONJSON.parse에 넘긴 것이 JSON이 아니라 HTML이었다는 뜻입니다. 문구에 남은 <!DOCTYPE 조각이 증거이고, 그것은 서버가 404나 502, 로그인 리다이렉트 페이지를 HTML로 돌려주었다는 뜻입니다 — 그러니 진짜 잘못은 파싱이 아니라 그 앞의 요청에 있습니다. 주소가 틀렸거나, 개발 서버의 프록시가 API 대신 index.html을 내주었거나, 인증이 만료되어 로그인 페이지로 넘어간 경우입니다. res.json()을 부르기 전에 res.ok를 보고, 아니면 res.text()로 실제로 무엇이 왔는지 읽어 보세요 — curl -s로 그 주소를 직접 쳐 보는 것이 가장 빠릅니다. Chrome 111과 Node 20 이전에는 같은 오류가 Unexpected token < in JSON at position 0으로 적혔습니다.SyntaxError: Unexpected end of JSON inputJSON을 읽다가 문서가 끝나 버렸다는 뜻이고, 거의 언제나 몸통이 아예 비어 있었던 것입니다. 204 No Content나 본문 없는 오류 응답에 res.json()을 부른 경우, 응답 본문을 이미 한 번 읽어 두 번째에는 빈 것이 온 경우, 또는 쓰다 말고 끊긴 파일을 읽은 경우입니다. 먼저 텍스트로 받아서 비었는지 보고 나서 파싱하면 정확한 진단이 됩니다 — const t = await res.text(); if (!t) return null;. JSON.parse(text || 'null')처럼 기본값으로 덮는 것은 빈 응답을 정상으로 만들어 버리니, 서버가 왜 빈 몸통을 보냈는지 먼저 확인하세요.TypeError: Failed to fetch요청이 응답을 받지 못하고 끝났다는 뜻이고, 브라우저가 이유를 일부러 감춘 문구입니다 — CORS로 막힌 것과 주소가 죽은 것과 인증서가 잘못된 것과 광고 차단 확장이 끊은 것이 모두 같은 다섯 글자로 나옵니다. fetch가 던지는 이 오류는 상태 코드와 상관이 없어서 404나 500은 여기까지 오지 않으므로, 서버는 아예 응답을 시작하지 못한 것입니다. 콘솔의 이 줄 위나 Network 탭에 실제 이유가 한 줄 더 있는 경우가 많으니 그것을 먼저 읽고, curl -i로 그 주소를 직접 쳐 보면 서버가 살아 있는지가 갈립니다. 파이어폭스에서는 같은 상황이 NetworkError when attempting to fetch resource로 나옵니다.Access to fetch at 'https://api.example.com/data' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.요청은 서버에 닿았고 응답도 왔지만, 그 응답에 이 출처를 허락하는 머리글이 없어 브라우저가 자바스크립트에게 넘기기를 거부했다는 뜻입니다. 브라우저만 이 규칙을 지키므로 같은 주소를 curl로 치면 잘 됩니다 — 그래서 서버가 멀쩡한데 브라우저에서만 막히는 것으로 보입니다. 고칠 곳은 언제나 서버이고, 응답에 Access-Control-Allow-Origin: http://localhost:3000을 붙이는 것입니다. 프런트엔드에서는 아무것도 할 수 없고, 남의 서버를 고칠 수 없다면 내 서버를 거쳐 부르는 프록시가 유일한 길입니다. Allow-Origin: *는 쿠키를 함께 보내는 요청에서는 통하지 않고, 그 경우에는 출처를 정확히 적고 Allow-Credentials까지 켜야 합니다.SyntaxError: Cannot use import statement outside a modulenode나 브라우저가 그 파일을 CommonJS 스크립트로 읽었는데 그 안에 ESM 문법인 import가 있었다는 뜻입니다. .js 파일은 package.json에 type 표시가 없으면 CommonJS로 읽히고, 브라우저라면 <script>에 type="module"이 없을 때 같은 일이 납니다. npm pkg set type=module로 그 패키지 전체를 ESM으로 선언하면 풀리지만, 그 순간부터 그 패키지의 모든 .js에서 require와 __dirname이 사라지므로 남아 있는 CommonJS 파일들을 함께 옮겨야 합니다 — 파일 하나만 옮기고 싶다면 확장자를 .mjs로 바꾸는 편이 좁고 안전합니다.ReferenceError: require is not defined in ES module scope, you can use import instead앞의 오류와 정확히 반대편입니다 — 이 파일은 ESM으로 읽히고 있는데 그 안에 CommonJS의 require가 있는 것입니다. package.json에 "type": "module"을 넣은 직후에 옛 스크립트가 그대로 남아 있을 때 거의 언제나 이렇게 됩니다. 그 파일 하나만 옛 방식으로 두고 싶으면 mv script.js script.cjs로 확장자를 바꾸는 것이 가장 좁은 수정이고, ESM으로 옮기려면 require를 import로 바꾸면서 __dirname과 require.main === module 같은 CommonJS 전용 값들도 함께 손봐야 합니다.Error [ERR_MODULE_NOT_FOUND]: Cannot find module '/app/src/util' imported from /app/src/index.jsESM으로 실행할 때는 import 경로를 브라우저처럼 그대로 파일 이름으로 쓰기 때문에, './util'은 './util.js'가 아니라 없는 파일입니다. CommonJS에서는 node가 확장자를 붙여 보며 찾아 주었고 TypeScript로 쓴 코드는 컴파일 뒤에도 확장자 없는 import를 그대로 남기기 때문에, tsc로 빌드한 프로젝트를 node로 처음 돌릴 때 이 오류가 무더기로 납니다. 상대 경로 import에 .js 확장자를 붙이면 됩니다 — TypeScript 파일 안에서도 './util.js'라고 적는 것이 맞습니다(컴파일 결과가 .js이기 때문입니다). node_modules의 패키지 이름에는 확장자를 붙이지 않습니다.ReferenceError: window is not defined그 코드가 브라우저가 아니라 서버의 node 안에서 돌았다는 뜻입니다. node에는 window도 document도 localStorage도 없기 때문에, Next.js나 Nuxt처럼 먼저 서버에서 한 번 렌더하는 틀에서 모듈을 읽는 시점에 window를 만지면 여기서 멈춥니다 — 컴포넌트 바깥에 놓인 한 줄이나, 브라우저 전용 라이브러리의 import 자체가 원인인 경우가 많습니다. 브라우저에서만 할 일이라면 typeof window !== 'undefined'로 감싸거나, 더 낫게는 그 코드를 useEffect 안으로 옮기세요 — 효과는 브라우저에서만 돌기 때문입니다. 라이브러리 전체가 브라우저 전용이면 Next.js에서는 dynamic(() => import('./C'), { ssr: false })로 서버 렌더에서 아예 빼는 것이 답이고, 그 대신 그 부분은 서버가 만든 HTML에 담기지 않습니다.Hydration failed because the initial UI does not match what was rendered on the server서버가 만들어 보낸 HTML과 브라우저가 첫 렌더에서 만든 결과가 달라 React가 둘을 이어 붙이지 못했다는 뜻입니다. 렌더 안에서 new Date()나 Math.random()을 쓰거나 localStorage를 읽거나 window의 폭에 따라 다르게 그리면, 서버와 브라우저의 답이 다를 수밖에 없습니다 — <p> 안에 <div>를 넣은 것처럼 브라우저가 태그를 고쳐 버리는 경우도 같은 결과가 됩니다. 첫 렌더에서는 서버와 같은 것을 그리고, 달라지는 부분은 useEffect가 돈 뒤에 바꾸는 것이 정석입니다. 그 대가로 그 부분은 잠깐 늦게 나타나고, 서버가 만든 HTML에는 담기지 않습니다. React 19에서는 같은 상황이 "the server rendered HTML didn't match the client"로 적히고, 어느 쪽이 달랐는지 diff까지 보여 줍니다.Objects are not valid as a React child (found: object with keys {name, id})화면에 그리라고 넘긴 값이 문자열이나 숫자가 아니라 객체라는 뜻이고, 괄호 안의 열쇠 목록이 그 객체가 무엇인지 알려 줍니다. {user} 대신 {user.name}을 적어야 하는 자리이거나, 응답 전체를 그대로 그리려 한 경우입니다. 열쇠 목록 대신 found: [object Promise]가 보이면 원인이 다릅니다 — await를 빠뜨린 채 async 함수의 결과를 그리려 한 것입니다. 그릴 필드를 골라 적는 것이 답이고, 디버깅 중이라면 JSON.stringify(user)로 잠깐 볼 수 있지만 그것을 화면에 남기지는 마세요.Each child in a list should have a unique "key" prop.목록으로 그린 형제 요소들에 key가 없어, 다음 렌더에서 어느 것이 어느 것인지 React가 짝지을 수 없다는 뜻입니다. items.map(...)으로 요소를 만들면서 key를 빼먹은 것이고, 이것은 경고라 화면은 일단 그려지지만 목록이 바뀔 때 조용한 버그로 돌아옵니다 — 입력 칸의 값이나 애니메이션이 엉뚱한 줄에 남는 것이 그 증상입니다. 데이터에 있는 안정된 id를 key로 주세요. 배열의 인덱스를 key로 쓰는 것은 순서가 절대 바뀌지 않고 중간에 끼워 넣거나 지우지 않는 목록에서만 안전하고, 그렇지 않으면 key가 없는 것과 사실상 같은 문제를 냅니다. React 19부터는 앞머리에 붙던 Warning:이 사라졌습니다.Too many re-renders. React limits the number of renders to prevent an infinite loop.렌더가 상태를 바꾸고 그 상태가 다시 렌더를 부르는 고리에 빠졌고, React가 무한 반복을 막으려고 끊었다는 뜻입니다. 거의 언제나 함수를 넘겨야 할 자리에 함수를 호출해 둔 것입니다 — onClick={setOpen(true)}는 렌더할 때마다 즉시 실행되므로 onClick={() => setOpen(true)}여야 합니다. 다른 흔한 꼴은 렌더 본문에서 곧바로 상태를 바꾸는 것과, useEffect의 의존성 목록에 자기가 바꾸는 값을 넣는 것입니다. 상태를 바꾸는 일은 이벤트 처리기나 useEffect 안으로 옮기고, 렌더 중에 계산만 하면 되는 값이라면 상태로 둘 필요가 없는지 되돌아보세요.

Python18

파이썬의 오류는 Traceback의 마지막 한 줄에 무엇이 틀렸는지, 그 위의 프레임들에 어디서 틀렸는지가 나뉘어 적혀 있어서 마지막 줄만 읽으면 이름은 알고 자리는 놓치게 되고, 특히 NoneType과 KeyError처럼 값이 없어서 나는 오류는 터진 자리보다 그 값을 만들어 준 위쪽 프레임에 원인이 있습니다.

ModuleNotFoundError: No module named 'requests'파이썬이 sys.path에 있는 디렉터리를 다 훑었지만 그 이름의 모듈이 없다는 뜻입니다. 아직 설치하지 않았거나, 설치는 했는데 다른 인터프리터에 넣은 경우가 대부분입니다 — 가상환경 밖에서 pip install하고 안에서 실행하면 꼭 이렇게 됩니다. python -m pip install은 지금 돌리는 그 인터프리터에 직접 넣으므로 이 어긋남을 없앱니다.error: externally-managed-environment이 파이썬은 운영체제나 Homebrew가 관리하는 것이라 pip이 그 자리에 손대기를 거부한 것입니다(PEP 668). 시스템 파이썬으로 곧바로 pip install한 것이 원인이며, 이 오류는 "가상환경을 만들어라"는 안내입니다. --break-system-packages는 이름 그대로 시스템 패키지를 깨뜨릴 수 있고 apt가 관리하는 파일과 부딪히니, .venv를 만들어 그 안에 넣는 것이 답입니다.ImportError: cannot import name 'User' from partially initialized module 'models' (most likely due to a circular import)두 모듈이 서로를 import해서, 아직 반쯤만 읽힌 모듈에서 이름을 꺼내려 한 것입니다. A가 B를 부르고 B가 다시 A를 부르는 구조에서 나며, 파일을 나누다가 타입 힌트 하나를 위해 import를 되돌려 붙일 때 흔합니다. from y import Thing 대신 import y로 바꿔 함수 안에서 y.Thing으로 쓰면 이름을 꺼내는 시점이 뒤로 밀려 고리가 풀립니다.IndentationError: unexpected indent이 줄이 앞줄보다 더 들어가 있는데, 더 들어갈 이유가 되는 콜론이 앞에 없다는 뜻입니다. 붙여 넣은 코드가 들여쓰기를 한 칸 물고 왔거나, if 줄을 지우면서 그 아래 블록을 그대로 남겨 둔 경우가 흔합니다. 오류가 가리키는 줄 앞의 공백을 지우면 되고, 눈에 안 보이면 cat -A로 그 줄을 찍어 보면 공백과 탭이 드러납니다.TabError: inconsistent use of tabs and spaces in indentation한 블록 안에서 탭과 공백이 섞여, 파이썬이 어느 줄이 더 깊은지 판정할 수 없다는 뜻입니다. 탭을 쓰는 편집기에서 붙여 넣거나 두 사람이 서로 다른 설정으로 같은 파일을 만졌을 때 나며, 화면으로는 완전히 같아 보이기 때문에 눈으로는 찾을 수 없습니다. python -m tabnanny app.py가 어긋난 줄 번호를 찍어 주고, 그다음은 편집기에서 그 파일을 공백 4칸으로 통일하는 것으로 끝납니다.SyntaxError: invalid syntax파서가 그 자리에서 문장을 더 읽을 수 없었다는 뜻으로, 원인은 대개 오류가 가리키는 줄이 아니라 그 앞줄에 있습니다. 닫지 않은 괄호나 따옴표, 빠뜨린 콜론, 파이썬 2의 print "x"가 흔한 원인이며, 괄호를 닫지 않으면 파서는 몇 줄이나 더 나아간 뒤에야 포기합니다. python -m py_compile app.py로 실행 없이 문법만 확인할 수 있고, 3.10부터는 문구가 더 구체적으로 나오니 파이썬을 올리는 것 자체가 진단을 돕습니다.TypeError: 'NoneType' object is not subscriptable[...]로 꺼내려 한 그 값이 None이라는 뜻입니다. 값을 만들어 준 쪽이 조용히 None을 돌려준 것으로, dict.get()이 없는 열쇠에 None을 주거나 re.match가 못 맞혔거나 아무것도 return하지 않는 함수를 쓴 경우입니다. 그 자리에서 None을 검사하지 말고 왜 None이 왔는지 위로 올라가 보세요 — None을 건너뛰는 if 하나로 덮으면 다음 줄에서 같은 오류가 다시 납니다.AttributeError: 'NoneType' object has no attribute 'get'점 왼쪽의 그 객체가 None이라 요청한 속성이나 메서드가 없다는 뜻입니다. 앞의 nonetype-not-subscriptable과 뿌리가 같고, 특히 BeautifulSoup의 find()나 re.search()처럼 못 찾으면 None을 주는 함수의 결과를 곧바로 이어 쓸 때 납니다. 그 함수가 무엇을 못 찾았는지를 먼저 찍어 보고, 못 찾는 경우가 정상이라면 그 갈래를 따로 처리해 주세요.KeyError: 'name'그 열쇠가 dict에 없다는 뜻입니다. 따옴표 안의 것이 곧 없는 열쇠이니 그것부터 보세요 — 오타나 대소문자, 아니면 API 응답의 모양이 생각과 다른 경우입니다. 없을 수도 있는 값이면 d.get("name")으로 바꾸면 None이 오고 d.get("name", 0)으로 기본값을 줄 수 있지만, 반드시 있어야 할 값에까지 .get을 쓰면 오류가 사라지는 대신 None이 코드 속으로 흘러 훨씬 먼 곳에서 터집니다.IndexError: list index out of range리스트에 없는 자리를 번호로 부른 것입니다. 길이가 n이면 마지막 번호는 n-1이라 range(len(x) + 1)이나 x[len(x)]는 언제나 여기서 멈추고, 비어 있는 리스트에 x[0]을 하는 것도 같은 오류입니다 — 앞선 필터나 split이 아무것도 남기지 않았을 때 잘 납니다. 번호로 도는 대신 for item in items:로 돌면 이 오류는 구조적으로 사라지고, 번호가 꼭 필요하면 enumerate(items)를 쓰세요.ValueError: invalid literal for int() with base 10: '3.5'int()에 정수로 읽을 수 없는 문자열을 넘긴 것이고, 따옴표 안이 바로 그 문자열입니다. '3.5'처럼 소수점이 있거나 빈 문자열, 줄바꿈이 붙은 입력, 쉼표가 들어간 '1,000'이 흔한 원인입니다. 소수점이면 int(float(s))로 두 단계를 거치면 되지만 이때 소수점 아래가 버림으로 사라지고, 사람이 넣는 값이라면 애초에 try/except ValueError로 감싸는 편이 맞습니다.UnicodeDecodeError: 'utf-8' codec can't decode byte 0xff in position 0: invalid start byte파일이나 바이트열을 UTF-8로 읽으려 했는데 UTF-8이 아닌 바이트가 나왔다는 뜻입니다. 위치와 바이트 값이 문구에 적혀 있고, 윈도우에서 나온 cp1252·euc-kr CSV나 실제로는 이미지·zip인 파일, gzip을 풀지 않고 읽은 경우가 대부분입니다. 진짜 인코딩을 찾아 encoding= 으로 지정하는 것이 정답이고, errors="replace"는 읽히기는 하지만 그 바이트가 물음표로 바뀌어 데이터가 조용히 손상된 채 지나갑니다.ZeroDivisionError: division by zero0으로 나누었다는 뜻입니다. 대개 상수 0이 아니라 세어 본 개수가 0인 경우이고, 평균을 내는 코드에서 목록이 비었거나 필터가 아무것도 남기지 않았을 때 납니다 — 개발용 데이터에서는 항상 통과하고 운영에서 처음 터지는 종류입니다. 나누기 앞에서 분모가 0인지 보고, 0일 때 무엇을 돌려줄지 정하세요. 0을 답으로 두는 것과 None을 두는 것과 예외를 그대로 올리는 것은 서로 다른 결정이고, 세 가지가 다 맞는 상황이 따로 있습니다.RecursionError: maximum recursion depth exceeded함수가 자기를 부르는 깊이가 기본 한도 1000을 넘었다는 뜻입니다. 진짜로 깊은 계산인 경우보다 멈추는 조건이 없거나 닿지 않는 경우가 훨씬 많고, __getattr__이나 property 안에서 자기 속성을 다시 읽는 식의 간접 재귀도 여기로 옵니다. 먼저 스택에서 되풀이되는 두세 프레임을 찾아 멈추는 조건을 고치고, 정말 깊이가 필요한 계산이라면 sys.setrecursionlimit()로 올릴 수 있지만 이것은 C 스택까지 함께 넘겨 파이썬이 예외 대신 통째로 죽는 결과를 부릅니다 — 반복문으로 바꾸는 편이 안전합니다.UnboundLocalError: cannot access local variable 'count' where it is not associated with a value그 함수 안에 그 이름에 대입하는 줄이 하나라도 있으면 파이썬은 그 이름을 함수의 지역 변수로 정하는데, 대입보다 먼저 읽었다는 뜻입니다. 바깥에 같은 이름이 있어서 그것이 읽힐 것으로 기대했을 때, 또는 count += 1처럼 읽고 쓰는 연산을 처음 쓸 때 납니다. 함수 안에서 먼저 count = 0으로 시작하는 것이 보통의 답이고, 정말 모듈 차원의 값을 고쳐야 한다면 global count를 적어야 하지만 그 값을 누가 바꾸는지 추적하기 어려워지는 값을 치릅니다. 3.10까지는 같은 오류가 'local variable referenced before assignment'로 적혔습니다.TypeError: greet() takes 1 positional argument but 2 were given받기로 한 인자 수보다 하나 더 넘겼다는 뜻이고, 숫자가 딱 하나 차이 나면 거의 언제나 self입니다. 클래스 안의 메서드를 def greet(name):으로 정의하면 obj.greet("x")를 부를 때 파이썬이 obj를 첫 인자로 먼저 넣기 때문에 인자가 둘이 됩니다. 메서드라면 def greet(self, name):으로 self를 받게 고치고, 인스턴스가 필요 없는 함수라면 @staticmethod를 붙이세요.PermissionError: [Errno 13] Permission denied운영체제가 그 파일이나 디렉터리에 대한 그 작업을 거절했다는 뜻입니다. /var/log나 /usr처럼 남의 소유인 자리에 쓰려 했거나, 컨테이너에서 호스트 볼륨의 UID가 컨테이너 사용자와 다르거나, 파일이 아니라 그 파일이 담긴 디렉터리에 쓰기 권한이 없는 경우입니다 — 새 파일을 만들 때는 디렉터리의 권한이 필요합니다. ls -ld로 소유자와 모드를 먼저 확인하고, sudo로 넘기기 전에 애초에 쓸 수 있는 자리로 경로를 옮기는 편을 고려하세요. sudo로 만든 파일은 다음에 sudo 없이 열 때 다시 같은 오류를 냅니다.OSError: [Errno 98] Address already in use그 포트를 이미 다른 프로세스가 붙잡고 있어 bind가 실패했다는 뜻입니다. 앞서 띄운 서버가 완전히 죽지 않았거나, 자동 재시작이 두 벌을 띄웠거나, 같은 포트를 도커 컨테이너가 이미 쓰는 경우입니다. lsof -i :8000으로 붙잡은 PID를 찾아 그것만 정리하면 되고, kill -9로 아무거나 지우기 전에 그 프로세스가 무엇인지 이름을 확인하세요. Errno 번호는 운영체제마다 달라 리눅스에서는 98, macOS에서는 48로 찍힙니다.

빌드·타입10

빌드가 내는 오류는 실행 전에 도구가 거절한 것이라 그 자리에서 프로그램을 망가뜨리지는 않지만 대신 두 가지를 물어봅니다 — 타입이나 경로를 실제 모양에 맞게 고칠 것인가, 아니면 as any나 인용 부호 하나로 그 줄만 통과시킬 것인가 — 그리고 대소문자와 확장자, 환경 변수처럼 내 컴퓨터에서만 맞는 것들은 여기서 처음 드러납니다.

error TS2307: Cannot find module 'lodash' or its corresponding type declarations.tsc가 그 모듈의 자리도, 그 모듈의 타입 선언도 찾지 못했다는 뜻이고, 문구가 둘을 함께 말하는 것이 중요합니다 — 패키지가 아예 없을 수도 있고, 패키지는 설치되어 있는데 타입만 없을 수도 있습니다. 설치는 했는데 이 말이 나오면 타입 쪽이니 npm i -D @types/lodash처럼 짝이 되는 타입 패키지를 넣으면 되고, 최근 패키지들은 타입을 안에 담고 있어 @types가 아예 없는 경우도 많습니다. 상대 경로 import에서 이 오류가 나면 대소문자와 tsconfig의 paths를 보세요 — 자기 파일을 가리키는 경로는 @types와 아무 상관이 없습니다.error TS2339: Property 'user' does not exist on type 'Request'.그 타입에 그 속성이 선언되어 있지 않다는 뜻입니다. 라이브러리 타입에 없는 필드를 얹으려 할 때(Express의 Request에 user를 붙이는 것이 대표적), 오타, 또는 좁혀지지 않은 유니온에서 한쪽에만 있는 속성을 읽을 때 납니다. 타입을 실제 모양에 맞게 고치는 것이 답이고, 유니온이면 'x' in y나 판별용 필드로 좁히면 그 안에서는 접근이 열립니다. as any로 지우는 것은 컴파일러의 입만 막는 것이라, 그 자리는 이제 잘못된 속성 이름도 통과시킵니다.error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.함수가 받기로 한 타입과 넘긴 값의 타입이 다르다는 뜻이고, 문구가 어느 쪽이 무엇인지 순서대로 적어 줍니다 — 앞이 넘긴 것, 뒤가 받기로 한 것입니다. 폼 입력값이나 URL의 조회 문자열, JSON에서 온 값이 문자열인 채로 숫자를 받는 자리에 들어갈 때가 가장 흔합니다. 경계에서 한 번 변환하는 것이 답이지만, Number(value)는 실패했을 때 예외 대신 NaN을 돌려주므로 그 NaN이 그대로 흘러가지 않게 검사해야 합니다 — Number.isFinite(n)이 그 자리입니다.error TS18048: 'user' is possibly 'undefined'.그 값이 undefined일 수도 있는데 그 가능성을 처리하지 않았다는 뜻이고, 이것은 tsc가 잡아 준 미래의 Cannot read properties of undefined입니다. 선택적 필드나 배열의 find() 결과, 없을 수 있는 환경 변수를 곧바로 이어 쓸 때 납니다. 없을 때 무엇을 할지 정하는 것이 답이니, 함수 앞머리에서 if (!user) return null로 끊거나 user?.name으로 짧게 넘기세요. 느낌표를 붙여 user!.name으로 지우는 것은 "내가 보증한다"는 선언이고, 그 보증이 틀리면 런타임에 예외로 돌아옵니다 — 검사와 달리 아무 안전망을 남기지 않습니다.error TS7006: Parameter 'req' implicitly has an 'any' type.매개변수에 타입이 적혀 있지 않고 tsc가 추론할 근거도 없어서 암묵적으로 any가 되었다는 뜻이고, noImplicitAny가 켜져 있으면 그것을 오류로 봅니다. 자바스크립트에서 옮겨 온 파일이나 콜백을 따로 떼어 변수에 담을 때 나며, 반대로 items.map(x => ...)처럼 문맥이 있는 자리에서는 tsc가 알아서 추론하므로 이 오류가 나지 않습니다. 그 자리에 타입을 적어 주는 것이 답이고, any를 명시적으로 적어 넘길 수도 있지만 그것은 검사를 그 매개변수에서 완전히 끄는 선택입니다 — 무엇인지 모를 때는 unknown이 더 정직하고, 쓰기 전에 좁히도록 강제합니다.Parsing error: Unexpected tokenESLint가 그 파일을 규칙에 걸어 보기도 전에, 문법을 읽는 단계에서 멈췄다는 뜻입니다. 코드가 정말 깨진 경우보다, 파서가 그 문법을 모르는 경우가 훨씬 많습니다 — TypeScript 파일을 기본 파서로 읽거나, JSX를 켜지 않았거나, 데코레이터나 아주 새로운 문법을 옛 파서로 읽을 때입니다. npx eslint --print-config app.ts를 그 파일에 대고 돌리면 어떤 parser와 어떤 parserOptions가 실제로 적용되는지 그대로 나오니, 짐작하지 말고 그것부터 보세요. TypeScript면 @typescript-eslint/parser가 필요하고, 그 파일이 아예 검사 대상이 아니어야 한다면 ignores에 넣는 것이 맞습니다.You may need an appropriate loader to handle this file type, currently no loaders are configured to process this file.webpack이 Module parse failed 뒤에 붙이는 줄로, 그 파일을 자바스크립트로 읽으려 했지만 자바스크립트가 아니었다는 뜻입니다. TypeScript나 JSX, CSS, 이미지, .vue처럼 변환이 필요한 파일을 module.rules에 규칙 없이 import했거나, node_modules에 있는 패키지가 컴파일되지 않은 소스를 내보내면서 그 폴더가 exclude에 걸려 있는 경우입니다. 그 확장자에 맞는 loader 규칙을 module.rules에 더하는 것이 답이고, 어느 파일에서 멈췄는지는 문구 위에 적힌 경로가 알려 줍니다. exclude: /node_modules/를 그냥 지워 해결하면 전체 빌드가 눈에 띄게 느려지므로, 그 패키지 하나만 예외로 두는 편이 낫습니다.Failed to resolve import "./utils" from "src/main.ts". Does the file exist?그 경로에서 파일을 찾지 못했다는 뜻이고, 함께 나오는 "Does the file exist?"가 실제로 확인해야 할 세 가지를 가리킵니다 — 이름의 대소문자, 확장자, 그리고 tsconfig나 vite.config의 별칭입니다. macOS와 윈도우의 파일시스템은 대소문자를 구별하지 않으므로 ./Utils라고 적어도 내 컴퓨터에서는 잘 돌아가고, 리눅스에서 도는 CI와 배포에서만 처음 깨집니다 — 이 오류가 "내 컴퓨터에서는 되는데"의 가장 흔한 정체입니다. git ls-files src | grep -i utils로 저장소에 실제로 어떤 철자로 들어가 있는지 확인하세요. 별칭(@/utils)이면 vite.config의 resolve.alias와 tsconfig의 paths를 둘 다 맞춰야 하고, 하나만 고치면 편집기는 조용한데 빌드만 깨집니다.You're importing a component that needs useState. This React hook only works in a client component.App Router에서는 모든 컴포넌트가 기본으로 서버 컴포넌트인데, useState처럼 브라우저에서만 뜻이 있는 훅을 쓰는 파일을 서버 컴포넌트가 import했다는 뜻입니다. 지시어는 파일 맨 위에 'use client' 한 줄이고, 그것을 붙이면 그 파일과 그것이 import하는 모든 것이 클라이언트 묶음으로 들어갑니다 — 그래서 페이지 전체 대신 상태가 필요한 가장 작은 조각에만 붙이는 것이 값이 싼 선택입니다. 반대 방향으로, 클라이언트 컴포넌트가 자식으로 서버 컴포넌트를 받는 구조로 나누면 데이터 가져오기는 서버에 남겨 둘 수 있습니다. Next.js 13에서는 같은 상황이 "It only works in a Client Component but none of its parents are marked with \"use client\""로 적혔습니다.The engine "node" is incompatible with this module. Expected version ">=20"설치하려는 패키지가 package.json의 engines에 필요한 node 판을 적어 두었고, 지금 쓰는 판이 그 범위 밖이라는 뜻입니다. 뒤에 붙는 Expected와 Got이 요구 범위와 내 판을 나란히 보여 주니 그 둘만 보면 됩니다 — CI에서만 이 오류가 난다면 그곳의 node 판이 내 컴퓨터와 다른 것입니다. nvm install 20 && nvm use 20처럼 node를 올리는 것이 정면의 답이고, .nvmrc나 CI 설정에도 같은 판을 적어 두어야 다음에 또 갈라지지 않습니다. yarn의 --ignore-engines로 넘길 수는 있지만 그것은 경고를 지우는 것일 뿐 호환성을 만들어 주지 않아, 그 패키지가 새 문법을 쓰면 실행 중에 문법 오류로 터집니다. npm은 같은 상황을 EBADENGINE 경고로 알리고 기본적으로는 설치를 막지 않습니다.

Docker12

docker의 오류는 어느 층에서 났는지를 먼저 갈라야 읽히는데 — 클라이언트가 데몬에 닿지 못한 것, 레지스트리가 거절한 것, 빌드 중에 RUN이 실패한 것, 컨테이너가 뜨자마자 죽은 것이 서로 다른 문제입니다 — 특히 빌드와 실행 단계의 오류는 문구 자체가 이유가 아니라 그 안에서 돌던 명령이 남긴 출력에 이유가 있고, 고치는 명령의 값도 층마다 다릅니다.

Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?docker 명령은 자기가 아무것도 하지 않고 데몬에게 부탁하는 클라이언트인데, 그 소켓에 아무도 응답하지 않았다는 뜻입니다. 데몬이 꺼져 있거나, macOS·윈도우에서 Docker Desktop이 아직 뜨지 않았거나, 리눅스에서 내 사용자가 docker 그룹에 없어 소켓을 열 권한이 없는 경우입니다 — 셋 다 같은 한 줄로 나옵니다. 리눅스면 sudo systemctl start docker로 띄우고, Desktop이면 앱을 먼저 켜세요. 권한 쪽이면 sudo usermod -aG docker $USER 뒤에 다시 로그인해야 하며, 그 그룹은 사실상 root와 같은 힘을 줍니다.Bind for 0.0.0.0:8080 failed: port is already allocated-p로 호스트의 그 포트를 잡으려 했지만 이미 다른 컨테이너나 프로세스가 쓰고 있다는 뜻입니다. 앞서 띄운 컨테이너를 --rm 없이 돌려서 멈춘 채로 남아 있거나, compose가 옛 컨테이너를 아직 붙잡고 있는 경우가 대부분입니다. docker ps --filter publish=8080이 그 포트를 게시한 컨테이너를 바로 짚어 주고, 컨테이너가 아니라 호스트의 다른 프로그램이라면 lsof -i :8080으로 찾습니다. 급하면 -p 8081:80처럼 호스트 쪽 번호만 바꿔도 되지만, 남아 있는 옛 컨테이너를 정리하지 않으면 같은 일이 다음에도 납니다.Conflict. The container name "/api" is already in use by container--name으로 준 이름을 이미 다른 컨테이너가 쓰고 있다는 뜻입니다. 이름은 돌고 있는 컨테이너뿐 아니라 멈춘 컨테이너까지 통틀어 하나뿐이라, 어제 죽은 컨테이너가 그 이름을 계속 붙잡고 있는 경우가 대부분입니다 — docker ps에는 안 보이고 docker ps -a에만 보입니다. docker rm -f api로 그 컨테이너를 지우면 이름이 풀리지만, 그 컨테이너에만 있던 쓰기 계층은 함께 사라집니다(이름 붙은 볼륨은 남습니다). 한 번만 쓰고 버릴 컨테이너라면 처음부터 docker run --rm으로 띄우면 이 일이 생기지 않습니다.failed to register layer: Error processing tar file(exit status 1): no space left on device이미지 계층을 풀어 놓을 자리가 디스크에 없다는 뜻입니다. 대개는 프로젝트가 큰 것이 아니라 도커가 몇 달치 옛 이미지와 빌드 캐시, 그리고 지운 컨테이너의 볼륨을 계속 들고 있는 것입니다. docker system df가 이미지·컨테이너·볼륨·빌드 캐시별로 얼마씩 쓰고 무엇이 회수 가능한지 먼저 보여 주니 그것부터 읽으세요. docker system prune은 멈춘 컨테이너와 떠 있는 이미지, 안 쓰는 네트워크와 빌드 캐시를 지우는데 캐시가 사라진 다음 빌드는 눈에 띄게 느려지고, --volumes를 붙이면 컨테이너에 붙어 있지 않은 볼륨까지 지웁니다 — 사람들이 데이터베이스를 잃는 자리가 정확히 여기입니다.pull access denied for myapp, repository does not exist or may require 'docker login'레지스트리가 그 저장소를 보여 주지 않았다는 뜻이고, 문구가 두 가지 원인을 한꺼번에 말하는 것이 핵심입니다 — 없는 이름이거나, 있지만 내게 권한이 없는 것입니다. 비공개 저장소에 로그인하지 않았거나, 이름에 사용자·조직을 빼먹었거나(myapp과 myorg/myapp은 다른 저장소입니다), 존재하지 않는 이름을 적은 경우입니다. 비공개라면 docker login으로 그 레지스트리에 로그인하고, 공개 이미지인데도 이 말이 나오면 이름의 철자를 다시 보세요. 레지스트리는 없는 것과 못 보는 것을 일부러 구별해 알려 주지 않습니다 — 이름의 존재 여부가 정보가 되기 때문입니다.manifest for myapp:v2 not found: manifest unknown저장소는 찾았지만 그 태그가 그 안에 없다는 뜻입니다. 앞의 pull access denied와 달리 이쪽은 이름은 맞았다는 신호이니, 이제 볼 것은 태그뿐입니다 — 지우거나 옮긴 태그, latest만 올려 두고 v2를 만들지 않은 파이프라인, 또는 그 태그가 내 아키텍처용 이미지를 갖고 있지 않은 경우입니다. docker manifest inspect myapp:v2로 그 태그가 실제로 풀리는지, 어느 platform이 들어 있는지 확인할 수 있습니다. latest로 바꿔 넘어가는 것은 재현이 안 되는 빌드를 만드는 값을 치르니, 태그를 고치는 편이 낫습니다.unauthorized: incorrect username or password레지스트리가 보낸 자격 증명을 거절했다는 뜻입니다. 비밀번호를 틀린 경우보다, 요즘은 계정 비밀번호로는 로그인할 수 없게 된 레지스트리에 비밀번호를 넣은 경우가 많습니다 — Docker Hub는 2단계 인증을 켜면 액세스 토큰만 받고, GitHub·GitLab의 레지스트리는 처음부터 토큰이나 배포 키만 받습니다. docker logout으로 저장된 자격 증명을 지우고 토큰으로 다시 docker login하세요. echo $TOKEN | docker login -u user --password-stdin으로 넣으면 토큰이 셸 기록에 남지 않습니다.failed to solve: process "/bin/sh -c npm ci" did not complete successfully: exit code: 1Dockerfile의 그 RUN 줄이 0이 아닌 코드로 끝났다는 뜻입니다. 여기에 적힌 것은 실패한 명령과 종료 코드뿐이고 진짜 이유는 그 위에 흐른 그 명령의 출력에 있는데, BuildKit이 성공한 단계를 접어 두기 때문에 잘 보이지 않습니다. docker build --progress=plain으로 다시 돌리면 모든 단계의 출력이 그대로 펼쳐지고, 캐시된 단계가 옛 오류를 감추고 있다면 --no-cache까지 붙이세요 — 대신 처음부터 다시 빌드하므로 시간이 많이 듭니다. exit code: 1은 이유가 아니라 그 명령이 실패했다는 사실일 뿐입니다.COPY failed: file not found in build context or excluded by .dockerignoreCOPY는 빌드 컨텍스트 안의 것만 가져올 수 있는데 그 자리에 그 파일이 없다는 뜻입니다. docker build 뒤의 마지막 인자가 컨텍스트이므로, 상위 폴더의 파일을 ../x로 가리키거나 .dockerignore가 그 파일을 걸러 낸 경우가 대부분입니다 — node_modules나 *.env를 걸러 두고 정작 그 안의 파일을 COPY하려 할 때 그렇습니다. 문구가 두 원인을 함께 말하니 cat .dockerignore를 먼저 읽고, 경로는 컨텍스트의 뿌리를 기준으로 다시 적으세요. 컨텍스트를 넓혀 해결하려 하면 그 폴더 전체가 데몬으로 전송되어 빌드가 느려집니다.exec /usr/local/bin/entrypoint.sh: exec format error커널이 그 실행 파일의 헤더를 알아보지 못했다는 뜻이고, 요즘은 거의 언제나 아키텍처가 어긋난 것입니다 — Apple Silicon에서 만든 arm64 이미지를 amd64 서버에서 돌리거나 그 반대입니다. 셸 스크립트라면 첫 줄의 #!/bin/sh가 빠졌을 때도 같은 문구가 나옵니다. docker build --platform=linux/amd64로 굽는 것이 흔한 대처지만, 그것은 어긋남을 없애는 것이 아니라 에뮬레이션으로 덮는 것입니다 — QEMU를 거치는 빌드와 실행은 원래 아키텍처보다 몇 배 느려집니다. 오래 쓸 이미지라면 buildx로 두 아키텍처를 함께 굽는 것이 값을 치르지 않는 길입니다.standard_init_linux.go: exec user process caused: no such file or directory컨테이너의 진입점을 실행하려 했지만 커널이 "그런 파일이 없다"고 답했다는 뜻이고, 파일은 분명히 그 자리에 있는데도 이 말이 나오는 것이 함정입니다. 스크립트의 줄 끝이 CRLF라서 첫 줄이 #!/bin/sh\r로 읽히고, 커널은 이름이 sh\r인 실행 파일을 찾다가 실패하는 것입니다 — 윈도우에서 클론했거나 git의 autocrlf가 켜져 있으면 이렇게 됩니다. dos2unix entrypoint.sh로 그 파일만 되돌릴 수 있고(없으면 sed -i 's/\r$//' entrypoint.sh), 다시 나지 않게 하려면 저장소에 .gitattributes를 두고 *.sh text eol=lf를 적으세요. alpine 이미지에 없는 인터프리터를 #!에 적었을 때도 같은 문구가 납니다.OCI runtime create failed: exec: "bash": executable file not found in $PATH: unknown컨테이너는 만들어졌지만 그 안에서 실행할 프로그램을 $PATH에서 찾지 못했다는 뜻입니다. alpine 계열 이미지에는 bash가 없고 sh만 있어, docker run -it myapp bash나 CMD ["bash", ...]가 정확히 이 오류로 끝납니다 — 도구가 컨테이너 안에 있다고 생각한 것이 실은 호스트에만 있는 경우도 같습니다. docker run --rm -it myapp sh로 먼저 들어가 보면 그 이미지에 무엇이 있는지 바로 알 수 있습니다. 굳이 bash가 필요하면 Dockerfile에 RUN apk add --no-cache bash를 넣으면 되지만, 이미지가 커지는 값을 치릅니다.

오류 문구를 읽는 법

  • 첫 줄부터 읽습니다. 아래로 갈수록 도구 내부 이야기이고, 정작 원인은 맨 위에 적혀 있습니다.
  • 파일 이름과 줄 번호가 있으면 거기가 시작점입니다 — 스택의 가장 위가 아니라, 내가 쓴 파일이 나오는 가장 위 줄입니다.
  • 문구를 그대로 검색합니다. 다만 내 경로와 변수 이름은 지웁니다 — 그 부분이 검색을 방해합니다.
  • 같은 문구가 도구 판마다 다르게 나옵니다. 검색 결과가 안 맞으면 판 번호를 함께 넣어 봅니다.
  • 고치는 명령을 붙여 넣기 전에 그 명령이 무엇을 버리는지 확인합니다. 되돌릴 수 없는 것이 섞여 있습니다.

자주 묻는 질문

Q. 오류 문구를 번역하지 않는 이유가 무엇인가요?

검색할 것이기 때문입니다. 도구는 영어로 출력하고, 번역한 문구로는 아무것도 찾을 수 없습니다. 언어를 따르는 것은 그 뜻과 대처뿐입니다.

Q. 문구가 제 화면과 조금 다릅니다.

도구 판마다 표현이 바뀝니다. 경로와 변수 이름을 지운 뒤 남는 뼈대가 같으면 같은 오류입니다. 판이 다르면 검색에 판 번호를 함께 넣어 봅니다.

Q. 고치는 명령을 그대로 실행해도 되나요?

무엇을 버리는지 먼저 봅니다. git reset --hard와 강제 푸시, docker system prune은 되돌릴 수 없는 것을 지웁니다 — 그런 자리는 설명에 적어 두었습니다.

Q. 여기 없는 오류는 어떻게 찾나요?

문구에서 제 경로·이름·숫자를 지우고 남는 부분만 검색합니다. 그 뼈대가 도구가 정해 둔 문장이고, 그것으로 찾으면 결과가 맞습니다.