<!doctype html>
2026년 6월 19일 | 개발 깨알 상식_Tips
포트 충돌은 서버를 자주 띄우는 개발 환경에서 꽤 얄밉게 튀어나온다. 로그에는 대개 EADDRINUSE, address already in use, bind: address already in use 같은 문장이 남는다. 나는 예전에는 이 메시지를 보면 바로 포트 번호를 하나 올리거나, 터미널을 다 닫고 다시 켰다. 빨리 넘어가긴 하는데, 나중에 같은 에러가 다시 나오면 아무것도 배운 게 없었다.
이럴 때 먼저 볼 것은 프레임워크 이름이 아니다. Flask인지 FastAPI인지, Vite인지 Next.js인지보다 그 포트를 이미 잡고 있는 프로세스가 누구인지가 먼저다. 포트 충돌은 코드 버그일 수도 있지만, 대부분은 이전 서버가 백그라운드에 남았거나, 테스트 러너가 자식 프로세스를 정리하지 못했거나, 운영 중인 로컬 도구와 개발 서버 포트가 겹친 상황이다.
에러 문장에서 포트 번호부터 떼어낸다
EADDRINUSE를 보면 제일 먼저 에러 문장 안의 포트 번호를 따로 적는다. 예를 들어 listen EADDRINUSE: address already in use 127.0.0.1:3000이라면 관심사는 일단 3000 하나다. 로그 전체를 검색하기 전에 포트 번호를 분리해 두면, 뒤의 진단 명령도 짧아지고 실수도 줄어든다.
리눅스나 WSL에서는 보통 ss가 빠르다. 내가 자주 쓰는 모양은 ss -ltnp에 포트 번호를 붙여 보는 방식이다. -l은 listen 중인 소켓, -t는 TCP, -n은 이름 해석 없이 숫자 출력, -p는 프로세스 정보를 뜻한다. 여기서 중요한 것은 명령어를 외우는 게 아니라 포트 번호와 PID를 같은 줄에서 확인하는 것이다.
ss -ltnp 'sport = :3000'
ss와 lsof는 서로 다른 각도에서 본다
ss가 네트워크 소켓 표면을 빠르게 보여 준다면, lsof는 파일 디스크립터 관점에서 프로세스를 찾아 준다. macOS까지 오가는 팀이면 lsof -iTCP:3000 -sTCP:LISTEN -n -P 형태가 더 익숙할 때도 있다. 나는 두 명령을 둘 다 기억해 두는 편이다. 하나가 권한이나 환경 차이 때문에 흐릿하게 나오면 다른 하나가 프로세스 이름을 더 잘 보여 줄 때가 있어서다.
lsof -iTCP:3000 -sTCP:LISTEN -n -P
여기서 바로 kill -9로 넘어가고 싶어지는 순간이 있다. 그런데 그 전에 최소한 프로세스 이름, 실행 사용자, 작업 디렉터리를 본다. 예를 들어 내가 방금 띄운 Node 개발 서버라면 정리해도 된다. 반대로 Docker 프록시, 데이터베이스, 동료가 붙여 둔 터널, 테스트 중인 백그라운드 worker라면 포트 충돌보다 더 큰 문제가 생길 수 있다.
주소도 같이 본다. 127.0.0.1:3000만 잡혀 있는지, 0.0.0.0:3000처럼 모든 인터페이스에 열려 있는지에 따라 충돌의 의미가 조금 다르다. 컨테이너를 붙여 쓰는 개발 환경에서는 host port와 container port가 다르게 적혀 있어 더 헷갈린다. 그래서 나는 포트 충돌 메모에 포트 번호만 쓰지 않고, 가능하면 주소와 프로세스 이름까지 한 줄로 같이 남긴다. 그래야 다음에 같은 번호가 보여도 “내가 띄운 웹 서버”인지 “컨테이너가 붙잡은 프록시”인지 바로 갈라진다.
죽일 프로세스와 바꿀 포트를 구분한다
포트 충돌을 만났을 때 선택지는 크게 두 가지다. 기존 프로세스를 종료하거나, 새 서버의 포트를 바꾸는 것이다. 기존 프로세스가 내 이전 실행 잔재라면 종료가 맞다. 하지만 의도적으로 떠 있는 서비스라면 새 서버 쪽 포트를 바꿔야 한다. 이 구분을 하지 않으면 로컬에서는 해결된 것처럼 보이다가 CI, 동료 머신, 배포 스크립트에서 다시 같은 충돌이 난다.
나는 종료할 때도 가능하면 순서를 둔다. 먼저 해당 터미널이나 프로세스 매니저에서 정상 종료를 시도하고, 그래도 안 되면 일반 kill, 마지막에만 강제 종료를 쓴다. 강제 종료가 나쁜 건 아니지만, 로그 flush나 임시 파일 정리가 끊길 수 있다. 특히 개발 서버가 파일 watcher, 테스트 DB, 브라우저 자동화 같은 자식 프로세스를 달고 있으면 더 그렇다.
kill 12345
# 그래도 남아 있을 때만
kill -9 12345
재발 방지는 포트 계약을 작게 남기는 쪽이다
한 번 해결한 뒤에는 포트 번호를 어딘가에 고정해 둔다. README의 개발 실행 섹션, .env.example, 테스트 설정 중 하나면 충분하다. 핵심은 이 프로젝트가 기본으로 기대하는 포트와 임시로 바꿔도 되는 포트를 나누는 것이다. 예를 들어 프론트 개발 서버는 바꿔도 되지만 OAuth redirect URI에 묶인 백엔드 포트는 함부로 바꾸면 다른 에러가 생긴다.
테스트 자동화에서는 시작 전에 포트를 확인하는 작은 preflight도 도움이 된다. 다만 모든 포트를 검사하는 거창한 스크립트보다, 충돌이 반복되는 한두 개 포트만 짧게 확인하는 쪽이 유지보수하기 쉽다. 실패 메시지도 “port 3000 busy” 정도가 아니라, 가능하면 현재 잡고 있는 PID와 프로세스 이름까지 남긴다. 그래야 다음 사람이 포트 번호를 바꿀지, 남은 프로세스를 치울지 바로 정할 수 있다.
반대로 포트를 자동으로 하나씩 올리는 기능은 조심해서 쓴다. 편하긴 하지만, 브라우저 콜백 주소나 웹훅 테스트 주소가 고정되어 있으면 서버만 다른 포트에서 떠 있고 나머지 도구는 예전 포트를 계속 바라본다. 그 결과 처음 에러는 사라졌는데 로그인, 콜백, 프록시에서 더 애매한 실패가 생긴다. 임시 포트 증가를 쓰더라도 콘솔에 실제 bind 주소를 크게 찍고, 테스트 설정이 그 값을 따라가게 만들어야 한다.
내가 남기는 최소 메모
이 에러를 처리한 뒤 내 메모에는 세 줄이면 충분하다. 어떤 포트가 막혔는지, 누가 잡고 있었는지, 이번에는 종료했는지 포트를 바꿨는지다. 가능하면 마지막에 “왜 남았나”도 한 문장 붙인다. 이전 터미널을 닫지 않았는지, 테스트가 실패하며 정리 훅을 타지 못했는지, 컨테이너가 재시작되며 host port를 다시 잡았는지 같은 이유가 쌓이면 다음 수정점이 보인다. 단순히 PID만 지우면 같은 충돌을 매번 새 문제처럼 다시 만난다.
이 세 줄이 있으면 다음 충돌은 훨씬 덜 당황스럽다. 포트 충돌은 대단한 장애라기보다, 실행 중인 것과 새로 띄우려는 것의 경계가 흐려진 신호에 가깝다. 그래서 해결도 포트 숫자를 찍어 맞히는 쪽보다, 이미 떠 있는 프로세스의 정체를 먼저 확인하는 쪽이 오래 간다. 나는 이 순서만 지켜도 불필요한 재부팅이 꽤 줄었다.
'[개발 깨알 상식_Tips]' 카테고리의 다른 글
| 압축 파일 안의 절대 경로 덮어쓰기 위험 (0) | 2026.06.23 |
|---|---|
| CSV 헤더 공백이 만든 컬럼 선택 오류 (0) | 2026.06.22 |
| JSONL 빈 줄과 BOM이 만든 배치 파싱 오류 (0) | 2026.06.18 |
| CSV 식별자 컬럼의 숫자 추론 오류 (0) | 2026.06.17 |
| 압축 파일 안의 절대경로와 덮어쓰기 (0) | 2026.06.16 |