본문으로 바로가기
Lab

ERR_CONNECTION_REFUSED 원인과 해결, 크롬에서 직접 해 보니

크롬에서 ERR_CONNECTION_REFUSED가 날 때 서버가 떠 있는지, 포트와 주소가 맞는지를 하나씩 확인한 기록입니다. 서버를 띄우자 바로 열렸고, 포트를 하나 틀리자 같은 오류가 다시 났습니다. Claude Code에게 원인을 맡긴 단계는 14.9초 걸렸습니다.

·

여러 개의 닫힌 문 가운데 한 문에만 서버가 켜져 요청을 받아들이는 개념 삽화
ERR_CONNECTION_REFUSED 원인과 해결, 크롬에서 직접 해 보니 표지 삽화

아무 서버도 띄우지 않은 http://localhost:8000/을 크롬으로 열면 net::ERR_CONNECTION_REFUSED가 찍힙니다. 그런데 같은 주소에 python3 서버를 띄우자 페이지가 바로 열렸고, 포트를 8001로 하나 틀리자 다시 같은 ERR_CONNECTION_REFUSED가 났습니다. AI 에이전트가 띄운 개발 서버가 갑자기 안 열린다면, 아래 기록을 따라 서버·포트·주소를 순서대로 짚어 보세요.

이 오류는 무슨 뜻이고 먼저 볼 곳은?

이번 로컬 재현에서는 연결하려는 주소와 포트에 요청을 받아 줄 서버가 없을 때 ERR_CONNECTION_REFUSED가 났습니다. 이 글은 그 범위 안에서 서버, 포트, 주소를 차례로 확인하는 진단을 다룹니다. 그래서 가장 먼저 볼 곳은 서버가 정말 떠 있는지, 그리고 주소창의 포트 번호가 서버 포트와 같은지예요.

이번 실행의 결론도 이 두 가지로 모입니다. 크롬이 오류를 낸 바로 그 주소에 서버를 띄우자 열렸고, 서버는 그대로 둔 채 포트만 틀리자 같은 오류가 다시 났습니다. 서버가 시작하다 죽은 경우와 듣는 주소가 다른 경우도 curl에서는 같은 연결 거절(종료 코드 7)이 났습니다.

ERR_CONNECTION_REFUSED가 날 때 포트, 듣는 프로그램, 시작 로그, 듣는 주소를 차례로 확인하는 네 단계 도식
이번 실행에서는 서버가 없거나 포트가 다를 때 같은 오류가 났고, 서버를 띄우자 바로 열렸습니다.
도식출처: 직접 실행한 순서

함께 읽을 글

크롬과 터미널에서 같은 오류 내 보기

이번 실행은 macOS(darwin arm64)에서 했습니다. 크롬은 화면 없이(--headless=new) 새 임시 프로필로 띄웠고, 오류 화면 대신 크롬이 남긴 오류 이름(net::ERR_...)을 기록했습니다. 먼저 크롬을 감싼 실행 파일을 만들고 버전을 확인했습니다.

$ printf '#!/bin/sh\nexec "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" --headless=new --disable-gpu --no-first-run --no-default-browser-check --user-data-dir="$PWD/chrome-profile" "$@"\n' > chrome && chmod +x chrome && ./chrome --version && curl --version | head -1
Google Chrome 156.0.8078.48
curl 8.7.1 (x86_64-apple-darwin26.0) libcurl/8.7.1 (SecureTransport) LibreSSL/3.3.6 zlib/1.2.12 nghttp2/1.69.0

아무도 듣지 않는 8000번 포트를 크롬으로 열면 이렇게 나옵니다.

$ ./chrome --dump-dom http://localhost:8000/ 2>&1 | grep -o 'Page load failed: net::ERR_[A-Z_]*'
Page load failed: net::ERR_CONNECTION_REFUSED

브라우저가 없는 환경이라면 어떻게 확인할까요? 같은 주소를 curl로 부르면 종료 코드 7과 함께 연결 실패 문구가 나옵니다. 에이전트가 터미널에서 직접 확인하기에도 이쪽이 편하죠.

$ curl -sS http://localhost:8000/
curl: (7) Failed to connect to localhost port 8000 after 0 ms: Couldn't connect to server

일반 크롬 창에서 본다면 개발자 도구의 네트워크 패널도 쓸 수 있습니다. Chrome DevTools 네트워크 참조에 따르면 상태 열에는 실패한 요청이 (failed)와 오류 메시지로 표시됩니다. 원격 주소 열을 켜면 요청한 IP와 포트도 볼 수 있어요.

그 포트를 듣는 프로그램이 있을까요?

오류를 확인했다면 다음은 그 포트에 연결을 기다리는 프로그램이 있는지 볼 차례입니다. lsof로 8000번에서 듣고 있는(LISTEN) 프로그램을 찾았습니다.

$ lsof -nP -iTCP:8000 -sTCP:LISTEN || echo '8000번을 듣는 프로그램 없음'
8000번을 듣는 프로그램 없음

아무것도 나오지 않으면 브라우저나 네트워크 설정을 의심하기 전에 서버부터 띄워 볼 차례입니다.

서버를 띄우면 바로 열릴까요?

그래서 같은 8000번에 python3 -m http.server를 띄우고 다시 열었습니다. 이 단계는 1초 기다리는 시간까지 넣어 1.5초 걸렸습니다.

$ mkdir -p site && printf '<meta charset="utf-8"><h1>열렸다</h1>\n' > site/index.html && (python3 -m http.server 8000 --directory site > server.log 2>&1 &) && sleep 1 && lsof -nP -iTCP:8000 -sTCP:LISTEN | awk '{print $1, $9}' && ./chrome --dump-dom http://localhost:8000/ 2>/dev/null
COMMAND NAME
Python *:8000
<html><head><meta charset="utf-8"></head><body><h1>열렸다</h1>
</body></html>

lsof의 NAME 칸이 *:8000인 점도 눈여겨보세요. Python http.server 문서에 따르면 이 서버는 기본으로 8000번을 듣고 모든 인터페이스에 바인딩합니다. 다만 --bind 127.0.0.1을 주면 localhost에만 바인딩합니다.

포트 번호를 하나 틀리면 어떻게 될까요?

서버를 켠 채로 주소창의 포트만 8001로 바꿔 보면 결과는 처음과 같습니다.

$ ./chrome --dump-dom http://localhost:8001/ 2>&1 | grep -o 'Page load failed: net::ERR_[A-Z_]*'
Page load failed: net::ERR_CONNECTION_REFUSED

서버가 떠 있다는 사실만으로는 부족하다는 뜻입니다. 에이전트가 "서버를 3000번에 띄웠다"고 보고했는데 브라우저는 다른 포트를 열고 있다면, 이 경우와 똑같은 오류를 보게 되죠.

같은 서버인데 주소에 따라 다를 때는?

이번에는 Node.js 서버를 127.0.0.1의 3000번에만 열고, IPv6 루프백 주소 [::1]로 불렀습니다. lsof에는 서버가 분명히 보이는데도 연결은 거절됐습니다.

$ (node -e "require('http').createServer((q, s) => { s.setHeader('content-type', 'text/plain; charset=utf-8'); s.end('127.0.0.1 서버\n'); }).listen(3000, '127.0.0.1')" > node.log 2>&1 &) && sleep 1 && lsof -nP -iTCP:3000 -sTCP:LISTEN | awk '{print $1, $9}' && curl -sS 'http://[::1]:3000/'
COMMAND NAME
node 127.0.0.1:3000
curl: (7) Failed to connect to ::1 port 3000 after 0 ms: Couldn't connect to server
127.0.0.1 쪽 문만 열린 서버 상자에 IPv6 쪽에서 온 요청이 되돌아가는 개념 삽화
서버가 127.0.0.1에서만 들으면 [::1]로 온 요청은 받을 프로그램이 없는 관계를 그린 개념 그림입니다.

그렇다면 localhost로 부르면 어떨까요? 같은 서버가 curl과 크롬 양쪽에서 열렸습니다.

$ curl -sS http://localhost:3000/ && ./chrome --dump-dom http://localhost:3000/ 2>/dev/null | head -1
127.0.0.1 서버
<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">127.0.0.1 서버

이 측정만으로는 localhost가 이 컴퓨터에서 어느 주소로 먼저 연결됐는지 가를 수 없습니다. 대신 lsof 결과의 NAME 칸을 보면 서버가 어떤 주소에서 듣는지 알 수 있습니다. 그 주소와 브라우저에 입력한 주소가 같은지 맞춰 보는 쪽이 확실합니다.

서버가 시작하다 죽었을 때 보는 곳

에이전트에게 "서버 띄웠어"라는 답을 받았는데도 안 열릴 때가 있습니다. 예를 들어 PORT 환경 변수가 없으면 바로 예외를 던지는 app.js를 실행하면, 서버는 포트를 열기 전에 죽고 5050번은 거절됩니다.

$ printf "const http = require('http');\nconst port = process.env.PORT;\nif (!port) throw new Error('PORT 환경 변수가 없습니다');\nhttp.createServer((q, s) => s.end('ok')).listen(port);\n" > app.js && node app.js; sleep 0.5; curl -sS http://localhost:5050/
./app.js:3
if (!port) throw new Error('PORT 환경 변수가 없습니다');
           ^

Error: PORT 환경 변수가 없습니다
    at Object.<anonymous> (./app.js:3:18)
    at Module._compile (node:internal/modules/cjs/loader:1781:14)
    at Module._extensions..js (node:internal/modules/cjs/loader:1913:10)
    at Module.load (node:internal/modules/cjs/loader:1505:32)
    at Module._load (node:internal/modules/cjs/loader:1309:12)
    at wrapModuleLoad (node:internal/modules/cjs/loader:254:19)
    at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:171:5)
    at node:internal/main/run_main_module:36:49

Node.js v22.23.1
curl: (7) Failed to connect to localhost port 5050 after 0 ms: Couldn't connect to server

curl로 부르면 앞의 경우와 똑같이 종료 코드 7로 연결에 실패합니다. 클라이언트 쪽에서는 구별되지 않고, 원인은 서버 출력에만 남죠. 에이전트가 서버를 백그라운드로 띄웠다면 출력이 어디로 갔는지 먼저 찾아보세요. 앞 단계에서는 server.log, node.log로 보냈습니다.

AI 에이전트에게 원인 찾기를 맡기면

같은 상황을 Claude Code 2.1.309에 맡겼습니다. 파일 읽기만 허용하고 고치지 말라고 했습니다.

$ claude --model opus -p "이 폴더의 app.js를 실행하면 브라우저에서 localhost:5050이 ERR_CONNECTION_REFUSED로 열리지 않는다. 파일을 읽고 원인과 고치는 실행 명령을 두 줄로만 답해. 파일은 고치지 마." --allowedTools "Read"
원인: `app.js`는 `PORT` 환경 변수가 없으면 바로 예외를 던지고 종료됩니다. 그래서 5050번 포트에서 대기하는 서버가 없어 `ERR_CONNECTION_REFUSED`가 납니다.
고치는 실행 명령: `PORT=5050 node app.js`

요청부터 답을 받기까지 단계 전체 시간은 14.9초였습니다. 답의 원인은 앞 절의 오류 로그와 같은 지점을 짚었습니다. 다만 제안된 명령을 다시 실행해 열리는지까지 확인하지는 않았습니다. 에이전트에게 맡길 때도 증상(주소와 오류 이름)과 범위(읽기만)를 함께 주면 이처럼 짧은 답을 받을 수 있습니다.

바로 해 볼 점검 순서와 이번 실행의 한계

이번 기록을 점검 순서로 바꾸면 다음과 같습니다.

  • 크롬과 curl로 같은 주소에서 같은 거절이 나는지 확인합니다.
  • lsof -nP -iTCP:포트 -sTCP:LISTEN으로 그 포트를 듣는 프로그램이 있는지 봅니다.
  • 없으면 lsof -nP -iTCP -sTCP:LISTEN으로 서버가 다른 포트에서 듣고 있는지 먼저 보고, 있으면 주소창의 포트 번호를 그 번호로 맞춥니다. 거기에도 없으면 서버를 띄운 터미널이나 로그 파일에서 시작 오류를 찾습니다.
  • 있으면 서버가 듣는 주소(NAME 칸)와 부르는 주소(127.0.0.1, [::1], localhost)를 맞춥니다.
  • 다시 열어 페이지가 뜨는지 확인합니다.
lsof 결과가 없음일 때와 있음일 때 다음에 확인할 것을 두 줄로 나눈 비교표
이번 기록을 바탕으로 정리한 점검 갈래입니다. 그 포트를 듣는 프로그램이 없으면 다른 포트와 시작 로그를, 있으면 듣는 주소를 맞춘 뒤 다시 엽니다.
도식출처: 근거를 바탕으로 코드가 그린 도식

마지막으로 띄운 서버는 아래 명령으로 모두 껐습니다.

$ kill $(lsof -tiTCP:8000 -sTCP:LISTEN) $(lsof -tiTCP:3000 -sTCP:LISTEN) && sleep 0.5 && (lsof -nP -iTCP:8000 -iTCP:3000 -sTCP:LISTEN || echo '8000, 3000번 모두 꺼짐')
8000, 3000번 모두 꺼짐

이 기록은 macOS 한 대에서 각 명령을 한 번씩 실행한 결과입니다. 방화벽, 공유기, 원격 서버처럼 이번에 재현하지 않은 원인은 다루지 않았습니다. 이런 환경이 의심된다면 위 순서로 내 컴퓨터 쪽을 먼저 걸러 낸 뒤 따로 확인해야 합니다.

참고 자료