
이 오류는 3000번 포트를 이미 다른 프로세스가 쓰고 있어서 새 서버가 자리를 못 잡았다는 뜻이에요. lsof -nP -iTCP:3000 -sTCP:LISTEN 으로 그 프로세스의 PID를 찾아 kill 하면 바로 풀려요.
대부분은 예전에 띄운 서버가 아직 살아 있는 경우예요. 특히 터미널에서 Ctrl+C 대신 Ctrl+Z를 누르면 서버가 멈춘 채로 포트를 계속 쥐고 있어서 같은 오류가 나요. 이 경우도 직접 재현해 봤어요.
테스트 환경: macOS 26.6.2, Node.js v26.5.0
오류가 나는 순간을 그대로 재현해 봤어요
아래처럼 3000번 포트로 뜨는 아주 짧은 서버 파일을 만들었어요.
const http = require('http');
const server = http.createServer((req, res) => res.end('ok'));
server.listen(3000, () => console.log('listening on 3000, pid', process.pid));
첫 번째 실행은 정상으로 떴고, 이 상태에서 터미널을 하나 더 열어 같은 파일을 한 번 더 실행했어요. 두 번째 실행에서 나온 출력이에요.
node:events:487
throw er; // Unhandled 'error' event
^
Error: listen EADDRINUSE: address already in use :::3000
at Server.setupListenHandle [as _listen2] (node:net:2167:16)
at listenInCluster (node:net:2224:12)
at Server.listen (node:net:2344:7)
at Object.<anonymous> (/private/tmp/ea/server.js:3:8)
at Module._compile (node:internal/modules/cjs/loader:1934:14)
at Object..js (node:internal/modules/cjs/loader:2074:10)
at Module.load (node:internal/modules/cjs/loader:1656:32)
at Module._load (node:internal/modules/cjs/loader:1448:12)
at wrapModuleLoad (node:internal/modules/cjs/loader:261:19)
at Module.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:154:5)
Emitted 'error' event on Server instance at:
at emitErrorNT (node:net:2203:8)
at process.processTicksAndRejections (node:internal/process/task_queues:90:21) {
code: 'EADDRINUSE',
errno: -48,
syscall: 'listen',
address: '::',
port: 3000
}
Node.js v26.5.0
맨 끝 :::3000 은 IPv6 주소 표기예요. 앞의 :: 는 모든 주소라는 뜻이고 마지막 3000이 포트 번호예요. 오류 코드 EADDRINUSE는 Address in use, 주소가 이미 쓰이고 있다는 말이에요.
누가 3000번을 쥐고 있는지 찾기
맥과 리눅스에서는 lsof로 바로 찾을 수 있어요. -nP 를 붙여야 포트가 숫자로 나와요. 이걸 빼면 3000 대신 hbci 같은 서비스 이름으로 표시돼서 헷갈려요(실제로 그렇게 나왔어요).
$ lsof -nP -iTCP:3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 33967 kimyongnam 12u IPv6 0x548d36f809d66d8e 0t0 TCP *:3000 (LISTEN)
COMMAND가 node이고 PID가 33967이에요. 이 번호를 kill 하면 돼요. PID만 뽑고 싶으면 lsof -ti :3000 처럼 -t 옵션을 쓰면 숫자만 나와요.
프로세스를 끄고 다시 띄우기
$ kill 33967
$ lsof -nP -iTCP:3000 -sTCP:LISTEN
(출력 없음, exit=1)
$ node server.js
listening on 3000, pid 34058
kill 뒤에 lsof를 다시 치면 아무것도 안 나오고, 그 상태에서 서버를 띄우니 정상으로 올라왔어요. 기본 kill로 안 꺼질 때만 kill -9 를 쓰세요. -9는 정리 작업 없이 바로 끊어 버리기 때문에 처음부터 쓰지 않는 편이 낫습니다.
윈도우는 lsof가 없어서 명령이 달라요. 이 PC에서 직접 돌려 보지는 못했고 마이크로소프트 공식 문서 기준으로 적어요. netstat -ano 의 -o 옵션이 연결마다 PID를 보여 주고, taskkill /pid 번호 로 그 프로세스를 끝내요. 강제로 끝낼 때는 /f 를 붙여요.
netstat -ano | findstr :3000
taskkill /pid 12345 /f
Ctrl+Z로 멈춘 서버가 포트를 쥐고 있는 경우
터미널에서 서버를 끌 때 Ctrl+C 대신 Ctrl+Z를 누르면 프로세스가 종료되지 않고 일시정지 상태로 남아요. 이걸 확인하려고 실행 중인 서버를 kill -STOP 으로 멈춰 놓고 확인해 봤어요.
$ lsof -nP -iTCP:3000 -sTCP:LISTEN
COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME
node 33967 kimyongnam 12u IPv6 0x548d36f809d66d8e 0t0 TCP *:3000 (LISTEN)
$ curl -m 2 localhost:3000
curl exit=28
서버는 멈춰서 응답을 못 하는데(curl이 2초 제한에 걸려 종료 코드 28), 포트는 여전히 LISTEN 상태로 잡혀 있어요. 그래서 새 서버를 띄우면 EADDRINUSE가 나요. 같은 터미널이라면 jobs 로 멈춘 작업을 확인하고 fg 로 되살린 뒤 Ctrl+C로 끄면 돼요.
다음부터 안 겪으려면
포트가 차 있을 때 다른 포트로 넘어가게 만들 수도 있어요. Node.js 공식 문서(net 모듈, server.listen)에도 EADDRINUSE를 error 이벤트로 받아 처리하는 예제가 있고, 첫 listen이 오류로 끝난 경우에는 listen을 다시 호출할 수 있다고 나와 있어요. 아래는 3001번으로 넘어가게 바꾼 코드와 실제 출력이에요.
const http = require('http');
const server = http.createServer((req, res) => res.end('ok'));
server.on('error', (err) => {
if (err.code === 'EADDRINUSE') {
console.log(`3000번 포트 사용 중, 3001번으로 다시 시도`);
server.listen(3001);
} else throw err;
});
server.listen(3000, () => console.log('listening on', server.address().port));
$ node server2.js
3000번 포트 사용 중, 3001번으로 다시 시도
listening on 3001
다만 프런트엔드 개발 서버처럼 주소가 고정돼야 하는 경우에는 포트가 몰래 바뀌는 게 더 헷갈릴 수 있어요. 그럴 때는 포트를 넘기지 말고, 남은 프로세스를 찾아 끄는 습관이 낫습니다.
| 상황 | 확인 명령 | 해결 |
|---|---|---|
| 예전에 띄운 서버가 살아 있음 | lsof -nP -iTCP:3000 -sTCP:LISTEN | kill PID |
| Ctrl+Z로 멈춘 서버 | jobs | fg 후 Ctrl+C |
| 윈도우 | netstat -ano 와 findstr :3000 | taskkill /pid PID /f |
| 포트를 자동으로 넘기고 싶음 | 코드에서 error 이벤트 처리 | 다른 포트로 listen |
관련 글: 서버 포트가 열려있는지 확인 방법 정리
이 글의 코드와 명령어는 직접 실행해 확인했고, 정리에는 AI 도구를 활용했습니다.
출처: Node.js 공식 문서 net 모듈 server.listen, Microsoft Learn netstat, taskkill 명령 문서 (2026년 10월 18일 확인)
'Script > Javascript' 카테고리의 다른 글
| 자바스크립트 깊은 복사, structuredClone vs JSON.parse(JSON.stringify) vs 스프레드 비교 (Node 26 실행 결과) (0) | 2026.10.18 |
|---|---|
| SyntaxError: Cannot use import statement outside a module 해결, Node 26에서 4가지 경우를 직접 돌려 봤어요 (0) | 2026.10.17 |
| javascript 문자열이 유효한지 체크하는 함수 ( validation ) (0) | 2023.04.19 |
| Javascript replace 역참조 치환 예제 (0) | 2023.02.06 |
| javascript 현재날짜 기준 과거 미래 체크 (0) | 2023.02.03 |
댓글