01. 웹과 HTTP
서버가 무엇을 받고 무엇을 돌려주는지, 그 형식을 정리합니다.
인터넷 기초
요청이 서버에 닿기까지 거치는 것들입니다.
| 요소 | 역할 |
|---|---|
| IP | 네트워크 상의 주소 |
| DNS | 도메인 이름을 IP로 변환 |
| 포트 | 한 장비 안에서 어떤 프로그램인지 구분 |
| TCP | 끊김 없이 순서대로 전달 |
서버는 특정 포트를 듣고 있다가 연결이 오면 처리합니다.
lsof -i :3000 # 3000번 포트를 쓰는 프로세스
netstat -an | grep 3000localhost는 자기 자신(127.0.0.1)입니다. 0.0.0.0으로 바인딩하면 외부에서도 접근할 수 있습니다. 컨테이너 안에서 127.0.0.1로 띄우면 밖에서 닿지 않는 것이 이 때문입니다.
HTTP 요청과 응답
POST /api/todos HTTP/1.1
Host: api.example.com
Content-Type: application/json
Authorization: Bearer eyJ...
{"title":"공부"}HTTP/1.1 201 Created
Content-Type: application/json
Location: /api/todos/1
{"id":1,"title":"공부","done":false}요청은 메서드·경로·헤더·본문, 응답은 상태 코드·헤더·본문으로 이루어집니다.
메서드
| 메서드 | 용도 | 안전 | 멱등 |
|---|---|---|---|
| GET | 조회 | 예 | 예 |
| POST | 생성 | 아니오 | 아니오 |
| PUT | 전체 교체 | 아니오 | 예 |
| PATCH | 부분 수정 | 아니오 | 아니오 |
| DELETE | 삭제 | 아니오 | 예 |
안전은 서버 상태를 바꾸지 않는다는 뜻이고, 멱등은 여러 번 호출해도 결과가 같다는 뜻입니다.
이 성질이 중요한 이유는 재시도 때문입니다. 네트워크가 끊겨 응답을 못 받았을 때, 멱등한 요청은 그냥 다시 보내면 됩니다. POST는 그럴 수 없어서 중복 생성이 생깁니다.
GET으로 데이터를 바꾸면 안 됩니다. 브라우저와 프록시가 마음대로 미리 호출하거나 캐시합니다.
상태 코드
| 코드 | 의미 |
|---|---|
| 200 | 성공 |
| 201 | 생성됨 — Location 헤더로 위치를 알려 줍니다 |
| 204 | 성공했지만 본문 없음 (삭제 등) |
| 400 | 요청 형식이 잘못됨 |
| 401 | 인증되지 않음 (누구인지 모름) |
| 403 | 인가되지 않음 (누구인지는 알지만 권한 없음) |
| 404 | 없음 |
| 409 | 현재 상태와 충돌 (중복 등) |
| 422 | 형식은 맞지만 값이 유효하지 않음 |
| 429 | 요청이 너무 많음 |
| 500 | 서버 오류 |
| 503 | 일시적으로 처리 불가 |
401과 403을 섞어 쓰는 경우가 많습니다. 토큰이 없거나 만료면 401, 토큰은 유효한데 권한이 없으면 403입니다.
500은 서버가 잘못한 것입니다. 사용자가 이상한 값을 보내서 난 오류를 500으로 돌려주고 있다면 400대로 바꿔야 합니다. 모니터링에서 진짜 장애와 구분되지 않습니다.
헤더
Content-Type: application/json 요청·응답 본문의 형식
Accept: application/json 원하는 응답 형식
Authorization: Bearer <token> 인증 정보
User-Agent: ... 클라이언트 정보
X-Request-Id: abc-123 요청 추적용프록시를 거치면 원래 클라이언트 정보가 바뀝니다. 실제 IP는 별도 헤더로 전달됩니다.
X-Forwarded-For: 203.0.113.1
X-Forwarded-Proto: https이 헤더는 클라이언트가 위조할 수 있습니다. 신뢰할 수 있는 프록시가 덧붙인 값만 믿어야 합니다.
연결 재사용과 HTTP 버전
HTTP/1.1은 한 연결에서 요청을 하나씩 처리합니다. 앞이 느리면 뒤가 막힙니다.
HTTP/2는 하나의 연결에서 여러 요청을 동시에 주고받고, 헤더를 압축합니다. 보통 앞단의 리버스 프록시나 로드밸런서가 처리하고, 뒤쪽 애플리케이션 서버와는 HTTP/1.1로 통신하는 구성이 흔합니다.
서버 쪽에서 신경 쓸 것은 연결 재사용입니다. 외부 API를 부를 때 매번 새 연결을 맺으면 TLS 핸드셰이크 비용이 반복됩니다. HTTP 클라이언트를 한 번 만들어 재사용합니다.
// 매 요청마다 새로 만들지 않습니다
const agent = new https.Agent({ keepAlive: true });캐시 헤더와 콘텐츠 협상
서버가 "이 응답을 얼마나 써도 되는지" 알려 줍니다.
Cache-Control: public, max-age=3600
Cache-Control: no-cache
Cache-Control: no-store
Cache-Control: private, max-age=0| 값 | 의미 |
|---|---|
no-store | 저장하지 말 것 |
no-cache | 저장하되 매번 서버에 확인할 것 |
private | 브라우저만 저장, 공용 프록시는 금지 |
max-age | 초 단위 유효 기간 |
개인 정보가 담긴 응답에 public을 주면 중간 프록시에 캐시되어 다른 사용자에게 노출될 수 있습니다.
조건부 요청으로 변경 여부만 확인할 수 있습니다.
# 첫 응답
ETag: "abc123"
# 다음 요청
If-None-Match: "abc123"
# 바뀌지 않았으면
HTTP/1.1 304 Not Modified본문이 비어 있어 전송량이 거의 없습니다.
콘텐츠 협상은 클라이언트가 원하는 형식을 요청하는 방식입니다.
Accept: application/json
Accept-Language: ko-KR
Accept-Encoding: gzip, br압축은 대부분 앞단에서 처리하지만, 직접 설정해야 하는 경우도 있습니다. JSON 응답은 압축 효과가 큽니다.
HTTPS와 TLS, JSON
HTTPS는 TLS로 감싼 HTTP입니다. 세 가지를 보장합니다.
- 내용을 가운데서 볼 수 없음
- 내용을 가운데서 바꿀 수 없음
- 상대가 그 도메인의 주인이 맞음
인증서는 보통 Let's Encrypt로 무료 발급받고 자동 갱신합니다. 실무에서는 로드밸런서나 리버스 프록시에서 TLS를 끝내고(termination) 내부는 평문으로 통신하는 구성이 많습니다.
JSON은 형식이 단순한 대신 몇 가지 주의가 필요합니다.
{
"id": 9007199254740993,
"amount": 0.1,
"createdAt": "2026-10-26T09:00:00Z"
}- 큰 정수는 JavaScript에서 정밀도를 잃습니다. ID가 크면 문자열로 보냅니다
- 소수는 금액에 쓰지 않습니다. 최소 단위 정수로 보내거나 문자열을 씁니다
- 날짜 타입이 없습니다. ISO 8601 문자열에 시간대를 포함해 보냅니다
요청 직접 확인하기
curl로 서버를 직접 두드려 보는 것이 가장 빠릅니다.
curl -i http://localhost:3000/api/todos
curl -X POST http://localhost:3000/api/todos \
-H "Content-Type: application/json" \
-d '{"title":"공부"}'
curl -v https://api.example.com/todos # 핸드셰이크와 헤더까지| 옵션 | 역할 |
|---|---|
-i | 응답 헤더 포함 |
-v | 요청·응답 전부 |
-X | 메서드 지정 |
-H | 헤더 추가 |
-d | 본문 |
브라우저 개발자 도구의 Network 탭에서 요청을 우클릭하면 Copy as cURL로 그대로 복사할 수 있습니다. 재현할 때 유용합니다.