Windows · Node.js v24.18.0. 본문의 첫 예제를 실행하여 표준 출력과 예상 결과가 일치함을 확인했습니다. 연습문제의 모든 입력을 검증한 것은 아닙니다.
이 글의 순서 5개 단락
브라우저에서 데이터가 보이지 않을 때는 서버가 오류 응답을 보냈는지, 브라우저가 응답 자체를 사용할 수 없었는지 먼저 구분해야 합니다. fetch()는 HTTP 상태 코드가 404라는 이유만으로 자동으로 catch에 들어가지 않습니다.
이 글의 첫 예제는 Node.js 24에서 응답 객체와 거부된 Promise를 만들어 처리 흐름을 살펴봅니다. 이어서 Node.js 24 로컬 서버와 Chromium 브라우저로 실제 요청을 직접 재현합니다.
먼저 응답 상태와 예외를 구분하기
다음 코드는 별도 파일이나 서버 없이 실행할 수 있습니다. Response는 Node.js 24에 포함된 웹 API입니다. 입력은 코드에 만든 200 응답, 404 응답, 거부된 Promise 세 가지입니다.
async function inspect(label, request) {
try {
const response = await request;
console.log(`${label}: status=${response.status}, ok=${response.ok}`);
} catch (error) {
console.log(`${label}: catch=${error.message}`);
}
}
async function main() {
await inspect('성공', Promise.resolve(new Response('OK', { status: 200 })));
await inspect('404', Promise.resolve(new Response('Not Found', { status: 404 })));
await inspect('거부', Promise.reject(new Error('요청 실패')));
}
main();

예상 출력은 다음과 같습니다.
성공: status=200, ok=true
404: status=404, ok=false
거부: catch=요청 실패
response.ok는 상태 코드가 200~299일 때 true입니다. 404 응답도 받은 응답이므로 await 다음 줄에서 상태를 읽습니다. 반면 거부된 Promise는 await에서 예외를 발생시켜 catch로 이동합니다. 이 예제의 마지막 줄은 처리 흐름을 보여 주는 모의 실패이며, 실제 네트워크나 CORS 검사는 아닙니다.
로컬 서버 준비하기
Node.js 24를 설치한 환경에서 아래 코드를 fetch-lab.cjs로 저장하고 터미널에서 node fetch-lab.cjs를 실행합니다. 이 서버는 127.0.0.1:8123에서 실습 페이지와 같은 출처 API를, 127.0.0.1:8124에서 CORS 헤더가 없는 API를 제공합니다.
const http = require('node:http');
const page = '<!doctype html><html lang="ko"><meta charset="utf-8"><title>fetch 실습</title><body><h1>fetch 실습</h1><p>개발자 도구의 Console에서 요청 코드를 실행하세요.</p></body></html>';
http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end(page);
} else if (req.url === '/api/ok') {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ message: '안녕하세요' }));
} else {
res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });
res.end('Not Found');
}
}).listen(8123, '127.0.0.1');
http.createServer((req, res) => {
res.writeHead(200, { 'Content-Type': 'application/json; charset=utf-8' });
res.end(JSON.stringify({ message: '다른 포트의 응답' }));
}).listen(8124, '127.0.0.1');
console.log('서버 시작: http://127.0.0.1:8123/');
Chromium에서 http://127.0.0.1:8123/을 열고 개발자 도구의 Console과 Network 탭을 엽니다. 주소를 localhost로 바꾸지 마세요. 호스트 이름이 달라져 출처가 바뀔 수 있습니다. 포트가 이미 사용 중이면 서버가 시작되지 않으므로 해당 포트를 비우거나 코드와 아래 주소를 함께 바꿉니다. 실습을 마치면 서버를 실행한 터미널에서 Ctrl+C로 종료합니다.
네 가지 요청을 관찰하기
페이지의 Console에 아래 코드를 붙여 넣고 실행합니다. 개발자 도구가 처음 붙여넣기를 막으면 브라우저에 표시된 안내를 따릅니다. 네 요청은 순서대로 실행됩니다.
async function inspectFetch(label, url) {
try {
const response = await fetch(url);
console.log(`${label}: status=${response.status}, ok=${response.ok}`);
} catch (error) {
console.log(`${label}: catch=${error.name}`);
}
}
async function runFetchLab() {
await inspectFetch('정상', '/api/ok');
await inspectFetch('404', '/api/missing');
await inspectFetch('연결 실패', 'http://127.0.0.1:8125/');
await inspectFetch('CORS 차단', 'http://127.0.0.1:8124/');
}
runFetchLab();
예상되는 코드의 출력은 다음과 같습니다. 연결 실패를 재현하려면 8125 포트에서 수신 중인 프로그램이 없어야 합니다. 다른 프로그램이 사용 중이라면 사용하지 않는 포트로 주소를 바꾸세요.
정상: status=200, ok=true
404: status=404, ok=false
연결 실패: catch=TypeError
CORS 차단: catch=TypeError
| 경우 | Console에서 살펴볼 내용 | Network에서 살펴볼 내용 |
|---|---|---|
| 정상 | 코드가 200, true를 출력합니다. | /api/ok의 상태 200과 응답 본문을 확인합니다. |
| 404 | 코드가 404, false를 출력합니다. 브라우저가 별도로 404 메시지를 표시할 수 있습니다. | /api/missing의 상태 404와 응답을 확인합니다. |
| 연결 실패 | 코드가 catch=TypeError를 출력합니다. 연결 오류 메시지도 표시될 수 있습니다. | 해당 포트의 요청이 실패한 항목으로 표시됩니다. |
| CORS 차단 | 코드가 catch=TypeError를 출력합니다. CORS 정책 관련 메시지를 살펴봅니다. | 8124 요청과 오류 표시, 사용 가능한 응답 헤더를 확인합니다. |
8123과 8124는 포트가 다르므로 서로 다른 출처입니다. 8124 서버는 응답을 보내지만 Access-Control-Allow-Origin 헤더를 보내지 않아 브라우저가 페이지의 JavaScript에 응답을 제공하지 않습니다. 따라서 이 경우 코드에서 상태 200을 읽을 수 없습니다. Network의 표시와 오류 문구는 브라우저 버전이나 환경에 따라 달라질 수 있으므로, 각 요청의 주소와 Console 메시지를 함께 확인하세요.
흔한 실수와 경계 조건
catch만 작성하면 404나 500 응답을 놓칠 수 있습니다. HTTP 오류를 예외로 처리하고 싶다면 응답을 받은 뒤 직접 검사합니다.
async function loadJson(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json();
}
loadJson('/api/missing')
.then(data => console.log(data))
.catch(error => console.log(error.message));
위 코드를 실습 페이지의 Console에서 실행하면 예상 출력은 HTTP 404입니다. 이때 catch에 들어가는 이유는 fetch가 404를 자동으로 거부해서가 아니라 코드에서 직접 throw했기 때문입니다. 정상 상태라도 본문이 유효한 JSON이 아니면 response.json()에서 오류가 날 수 있습니다.
CORS 오류를 피하려고 mode: 'no-cors'를 지정해도 데이터를 읽을 수 있는 해결책은 아닙니다. 다른 출처의 불투명 응답은 JavaScript에서 본문을 읽을 수 없고 상태도 0으로 보입니다. 서버를 제어할 수 있다면 허용할 출처에 맞는 CORS 응답 헤더를 서버에서 설정해야 합니다.
작은 연습
8124 서버의 res.writeHead에 'Access-Control-Allow-Origin': 'http://127.0.0.1:8123'을 추가하고 서버를 재시작하세요. 이후 페이지를 새로고침하고 네 가지 요청 코드를 다시 실행하면 마지막 줄은 어떻게 바뀔까요?
풀이: 서버가 해당 페이지의 출처를 허용하므로 마지막 줄은 CORS 차단: status=200, ok=true가 됩니다. 출력의 CORS 차단은 코드에 적어 둔 라벨이므로 결과가 성공으로 바뀌어도 이름은 그대로입니다. 이 실습은 인증 정보를 보내지 않는 단순 GET 요청이며, 인증 정보나 별도 헤더가 필요한 요청의 CORS 설정은 추가로 살펴봐야 합니다.
CHECK THE SOURCE
참고한 공식 문서
기능 설명은 위 문서를 참고했으며, 예제의 입력·결과와 한계는 본문에 따로 기록했습니다.