이 글에서 정리하는 내용
페이지를 열 때 HTML·CSS가 각각 요청되는 과정을 로컬 서버로 관찰합니다. URL, 요청 메서드, 상태 코드, Content-Type과 응답 본문을 구분하여 fetch와 웹 보안을 배우기 전 필요한 기준을 만듭니다.
목표·선수 지식: HTML의 링크와 CSS 연결을 알고 터미널에서 파일을 실행할 수 있으면 시작할 수 있습니다. 읽고 나면 같은 요청의 상태 코드·헤더·본문을 서로 다른 근거로 기록할 수 있어야 합니다.
실습 경로: 현재 ZIP → 전체 코드. 먼저 설명에서 지정한 파일만 읽고, 설정·테스트 파일은 필요한 때 확인하세요.
HTML 파일을 열었는데 요청은 왜 여러 개일까요?
퍼블리싱할 때는 HTML과 CSS를 함께 편집하지만 브라우저가 받는 응답은 파일마다 나뉩니다. 먼저 문서를 받고, 그 안의 stylesheet나 이미지 주소를 읽으면 필요한 자원을 추가로 요청합니다. JavaScript 실행 중에도 API 요청이 더 생길 수 있습니다. 화면 한 장과 HTTP 응답 한 개는 같은 단위가 아닙니다. MDN: Overview of HTTP

주소창에 도메인 URL을 넣으면 브라우저는 필요한 주소 조회와 연결 과정을 거쳐 서버에 요청합니다. HTTPS는 전송을 보호하는 보안 계층을 사용합니다. 이 글에서는 DNS·TLS 내부 대신 “어떤 URL에 무엇을 요청했고 어떤 응답이 왔는가”를 Network 패널에서 읽는 데 집중합니다. 로컬 실습은 IP 주소와 HTTP를 사용하여 외부 DNS·인증서 없이 실행합니다.
서버 프로그램은 요청 URL에 따라 HTML 파일을 읽어 보내거나 JSON을 만들어 보낼 수 있습니다. 응답은 HTML로 한정되지 않습니다. 같은 서버 주소에서 문서와 API를 모두 제공할 수 있고, 브라우저는 응답 형식과 사용 맥락에 맞게 처리합니다.
URL·메서드·헤더·본문을 나눠 읽기
예를 들어 http://127.0.0.1:43123/api/message?from=link#result에는 통신 방식, 호스트, 포트, 경로, 쿼리, fragment가 들어 있습니다. 실제 실습 포트는 실행 때마다 달라집니다. from=link는 서버로 전달하는 쿼리이고, #result는 HTTP 요청 대상에 포함되지 않는 브라우저 쪽 fragment입니다. MDN: URI fragment
| 부분 | 예시 | 확인할 의미 |
|---|---|---|
| scheme / host / port | http / 127.0.0.1 / 43123 | 어느 서버의 연결 지점인가 |
| path | /api/message | 어떤 자원을 요청하나 |
| query | ?from=link | 이번 요청의 추가 입력 |
| method | GET | 읽기를 요청하는 동작 |
| status | 200 / 404 | 서버가 처리한 결과 |
| response header | Content-Type: application/json | 응답 본문의 형식 |
| body | message와 from이 있는 JSON | 실제 전달된 데이터 |
GET은 자원 조회, POST는 제출·처리 요청에 흔히 쓰입니다. GET으로 서버 상태를 변경하는 기능을 만들면 링크·캐시·미리 가져오기 등과 어긋납니다. 2xx는 성공 계열, 3xx는 리다이렉션·캐시 검증 등 추가 처리 계열, 4xx는 요청·접근 조건 관련 오류, 5xx는 서버 처리 오류 계열입니다. 상태 코드는 개발자가 정한 JSON 안의 숫자와 별도로 HTTP 응답에 존재합니다.
헤더는 형식·캐시 같은 메타데이터이고 본문은 실제 콘텐츠입니다. Network에 보이는 헤더는 브라우저가 정리한 표현이며, HTTP/2·HTTP/3의 전송 형식이 HTTP/1.1 텍스트와 같다는 뜻은 아닙니다. 메시지 구성은 MDN: HTTP messages를 참고하세요.
문서·CSS·JSON을 돌려주는 로컬 서버
완성 예제 관찰형(TPL-02). 이 실습은 세 파일을 함께 실행한 뒤 응답을 읽는 수업입니다. 뷰어에서 파일명을 선택해 전체 코드를 복사하거나 아래 ZIP을 받으세요. index.html의 viewport 설정은 모바일 화면 너비를 사용하게 합니다. index.html 전체 코드
Node.js가 없다면 Node.js 공식 다운로드에서 운영체제에 맞는 LTS 설치 방법을 선택하세요. 설치 후 터미널을 새로 열고 node --version이 버전 번호를 출력하는지 확인합니다. 빈 http-lab 폴더에 아래 세 파일을 저장하세요. 외부 패키지나 npm 설치는 필요 없습니다. 이 예제는 Node.js 24.19.0에서 실행해 확인했습니다. 서버의 고정 포트 충돌을 피하려고 운영체제가 빈 포트를 고르게 합니다.
server.mjs는 요청 경로를 읽고 상태 코드·Content-Type·본문을 명시적으로 만듭니다. JSON.stringify는 객체를 HTTP 본문으로 보낼 문자열로 바꿉니다. API 경로가 아니라면 HTML·CSS 또는 404를 반환합니다. Node의 request/response API 구조는 Node.js: HTTP에서 확인할 수 있습니다. server.mjs 전체 코드
현재 실습 ZIP
server.mjs
import http from "node:http";
import { readFile } from "node:fs/promises";
const page = await readFile(new URL("./index.html", import.meta.url));
const style = await readFile(new URL("./style.css", import.meta.url));
const server = http.createServer((request, response) => {
const url = new URL(request.url, "http://127.0.0.1");
console.log(request.method, url.pathname, url.search);
response.setHeader("Cache-Control", "no-store");
if (request.method !== "GET") {
response.writeHead(405, { "Allow": "GET", "Content-Type": "text/plain; charset=utf-8" });
response.end("GET only");
} else if (url.pathname === "/") {
response.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
response.end(page);
} else if (url.pathname === "/style.css") {
response.writeHead(200, { "Content-Type": "text/css; charset=utf-8" });
response.end(style);
} else if (url.pathname === "/api/message") {
response.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
response.end(JSON.stringify({ message: "Hello, HTTP", from: url.searchParams.get("from") }));
} else {
response.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
response.end("Not found");
}
});
server.listen(0, "127.0.0.1", () => {
console.log(`READY http://127.0.0.1:${server.address().port}`);
});
index.html
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>HTTP 요청 관찰</title>
<link rel="stylesheet" href="/style.css">
</head>
<body>
<h1>HTTP 요청 관찰</h1>
<p>문서와 스타일은 서로 다른 요청입니다.</p>
<a href="/api/message?from=link#result">JSON 응답 열기</a>
<a href="/missing">404 응답 열기</a>
</body>
</html>
style.css
body {
max-width: 40rem;
margin: 3rem auto;
padding: 0 1rem;
font-family: sans-serif;
}
a {
display: block;
margin-block: 1rem;
}
index.html의 stylesheet는 별도 요청을 만들고, 첫 링크는 JSON 문서로 이동합니다. 이 단계에서는 fetch 코드를 사용하지 않아 HTTP 자체에 집중할 수 있습니다.
style.css는 적용 여부를 눈으로 구분할 작은 스타일입니다. CSS도 서버가 올바른 형식으로 전달해야 브라우저가 사용할 수 있습니다.
세 파일을 저장한 폴더의 터미널에서 다음 명령을 실행합니다. READY 뒤에 나온 실제 주소를 브라우저에 붙여 넣으세요. 파일을 더블클릭해 file://로 열면 이 서버 실습과 다른 환경이 됩니다.
node server.mjs
터미널은 서버가 요청을 받을 동안 계속 실행됩니다. 종료는 Ctrl+C입니다. 포트가 바뀔 수 있으므로 다시 실행했다면 이전 북마크 대신 새 READY 주소를 사용합니다.
Network 패널에서 네 가지 응답 비교하기
먼저 예측하기: /missing의 본문에 글자가 보이면 성공일까요? /api/message를 쿼리 없이 열면 from은 무엇일까요? 각 요청의 상태 코드, Content-Type, 본문을 세 칸에 따로 적고 아래 결과표와 비교하세요.
판별 답안
없는 경로는 본문이 있어도 404입니다. 쿼리가 없는 JSON은 200이고 from: null입니다. message는 응답 본문 필드이며 상태 코드가 아닙니다. POST 결과는 405, Allow는 GET, 본문은 GET only로 각각 구분합니다.
개발자 도구의 Network를 열고 All 필터에서 기록이 켜져 있는지 확인하세요. 링크 이동 기록을 남기려면 Preserve log를 켭니다. 페이지를 새로 고친 뒤 문서 요청과 style.css 요청을 각각 클릭합니다. 브라우저에 따라 favicon 요청이 추가되어 404가 보일 수도 있는데 본문·CSS 요청과 구분하세요. style.css 전체 코드

| 조작 | 요청 경로 | 예상 상태와 형식 | 직접 볼 곳 |
|---|---|---|---|
| 첫 페이지 열기 | / | 200 text/html | Response의 HTML |
| 스타일 로드 | /style.css | 200 text/css | Headers의 Content-Type |
| JSON 응답 링크 클릭 | /api/message?from=link | 200 application/json | Response의 message·from |
| 뒤로 온 뒤 404 링크 클릭 | /missing | 404 text/plain | Status와 Not found 본문 |
JSON 링크를 클릭하면 주소창에는 #result가 남을 수 있지만 서버 로그는 GET /api/message ?from=link를 기록합니다. Network의 요청 주소와 서버 로그에서 fragment가 서버에 전달되지 않는 것을 비교하세요. HTML에 JSON이 자동으로 끼워지는 것은 아닙니다. 여기서는 링크 이동이므로 브라우저가 JSON 응답 문서 자체를 엽니다.
같은 URL의 응답 본문을 보아도 Headers를 생략하면 상태를 오해할 수 있습니다. 이 서버의 /missing에는 읽을 수 있는 본문이 있어도 HTTP 상태는 404입니다. 반대로 CSS 경로가 200이어도 Content-Type이나 본문이 HTML이라면 스타일 요청으로는 잘못된 응답입니다.
추가로 현재 페이지의 Console에서 아래를 실행하면 POST 요청을 보내 서버가 허용하지 않는 메서드를 어떻게 응답하는지 볼 수 있습니다. 서버 소스의 405 분기와 Network의 Allow 헤더를 연결해 보세요.
개발자 도구 Console
const response = await fetch("/", { method: "POST" });
console.log(response.status, response.headers.get("Allow"));
console.log(await response.text());
예상 출력은 405 GET과 GET only입니다. 메서드 제한은 이 서버 예제의 설계입니다. 모든 웹 서버가 GET만 허용한다는 뜻은 아닙니다.
응답 실패와 연결 실패를 구분하며 마무리하기
서버가 실행 중일 때 /missing은 404 응답을 보냅니다. 서버를 Ctrl+C로 종료한 뒤 같은 주소를 열면 서버에 연결하지 못하여 HTTP 응답 자체를 받지 못할 수 있습니다. “본문이 없다”만 보고 두 상황을 같은 오류로 처리하면 확인할 위치를 놓칩니다.
| 관찰 | 의미 | 다음 확인 |
|---|---|---|
| 404 응답이 보임 | 서버까지 도달했지만 요청 자원을 찾지 못함 | 경로·라우팅 |
| 연결 거부·네트워크 실패 | 유효한 HTTP 응답을 받지 못함 | 서버 실행·주소·포트 |
| 200 JSON이 도착함 | HTTP 요청은 성공 | 데이터 내용·화면 반영 |
| 3xx 또는 캐시 관련 응답 | 항상 실패는 아님 | Location·캐시 정책·후속 요청 |
HTTP는 요청마다 필요한 정보를 보내는 방식입니다. 이전에 로그인했음을 이어가려면 쿠키나 토큰 등 추가 약속이 필요합니다. 그 전에 지금 실습에서 요청 URL, 메서드, 상태 코드, Content-Type, 본문을 각각 찾을 수 있는지 확인하세요.
이 글의 서버 검증은 문서·CSS·JSON 200, 없는 경로 404, POST 405와 Allow 헤더를 실제 HTTP 요청으로 확인했습니다. 브라우저 Network 화면 관찰은 독자가 위 순서로 수행하는 단계입니다. 이후 JavaScript fetch 오류 처리에서 같은 응답을 JavaScript가 받아 로딩·성공·실패로 바꾸는 흐름을 배우고, 쿠키·출처 기초 다음에 CSRF·XSS 구분으로 넘어가세요.
실습 파일 안내
ZIP의 complete/ 폴더에 뷰어와 같은 전체 실행 파일이 들어 있습니다. 압축을 풀고 이 폴더에서 node server.mjs를 실행하세요. 외부 패키지는 필요하지 않습니다. server.mjs 전체 코드
공식 자료 확인·실행 검증: 2026년 9월 12일. Node v24.19.0 실제 HTTP 요청: HTML/CSS/JSON 200, 쿼리와 null, /missing 404 본문, POST 405와 Allow 확인. 실제 브라우저 Network·모바일 화면은 미실행. 공식 API·정책 근거
이 글이 도움이 되었나요?
CS 기초 학습 순서
필수 3개 · 전체 3개
읽음 기록 관리
새 글 받아보기
RSS 리더에서 BlogFlow의 새 글을 확인할 수 있습니다.