본문으로 건너뛰기

HTTP 통신 구조: 요청과 응답에는 무엇이 담길까?

상품 조회 코드는 짧다. 하지만 이 코드가 서버로 보내는 정보는 URL 하나뿐일까?

const response = await fetch('/api/products/1');
const product = await response.json();

브라우저는 요청 대상과 메서드, 헤더 등을 HTTP 요청으로 전달한다. 서버는 상태 코드와 헤더, 필요한 본문을 담아 응답한다. response.json()은 받은 응답의 본문을 읽고 JSON으로 해석하는 단계다.

이 글에서는 HTTP의 역할 → 요청·응답 구조 → 메서드 → 상태 코드 → 헤더 순서로 살펴본다. 각 개념의 예시를 먼저 이해한 뒤, 절 끝의 면접 답변 · 꼬리 질문으로 말로 설명하는 연습을 할 수 있다. 세부 내용은 더 알아보기에 담았다.

스터디 질문은 Seongwon97의 Network Q&A를 참고하고, 답변은 HTTP 표준과 공식 문서를 바탕으로 보완했다.


1. HTTP: 어떤 계층에서 무엇을 약속할까?​

HTTP(Hypertext Transfer Protocol)는 클라이언트와 서버가 요청과 응답을 주고받는 애플리케이션 계층의 프로토콜이다. HTML뿐 아니라 JSON, 이미지, 파일 등 다양한 데이터를 전달할 수 있다.

HTTP의 기본 특성​

특성의미
요청·응답클라이언트가 요청을 보내고 서버가 그 요청에 응답한다.
무상태HTTP 자체는 이전 요청의 사용자 상태를 다음 요청에 자동으로 이어 주지 않는다.
전송 연결과 구분요청을 처리하는 방식과 TCP·QUIC 연결을 유지하거나 재사용하는 방식은 별개다.

예를 들어 상품 상세 페이지에서는 다음과 같은 교환이 일어난다.

구분주체HTTP에서 전달하는 내용
요청클라이언트 → 서버어떤 대상에 어떤 작업을 요청하는가?
응답서버 → 클라이언트요청이 어떻게 처리됐고 어떤 데이터를 보내는가?

HTTP 메서드와 상태 코드는 이 교환의 의미를 설명한다. TCP·QUIC은 그 메시지를 운반하는 역할을 하며, HTTPS는 통신의 암호화·무결성·서버 인증 등을 제공한다.

HTTP는 이전 요청을 기억하지 않는데, 로그인은 어떻게 유지할까?​

위에서 본 무상태(stateless) 때문에 HTTP 자체는 앞선 로그인 요청을 기억해 다음 요청에 자동으로 적용하지 않는다.

로그인 이후 요청에도 사용자를 식별할 정보가 필요하다. 예를 들어 브라우저가 세션 ID를 쿠키로 보내면, 서버는 세션 저장소에서 해당 ID를 찾아 사용자를 확인할 수 있다.

GET /me HTTP/1.1
Host: api.example.com
Cookie: sessionId=example-session-id

쿠키는 전송 조건이 맞으면 브라우저가 자동으로 요청에 포함한다. 이것이 HTTP 자체가 이전 로그인을 기억한다는 뜻은 아니다. 애플리케이션이 쿠키와 세션 등의 장치로 상태를 관리하는 것이다.

무상태이면 서버에 어떤 데이터도 저장할 수 없을까?

아니다. 무상태는 HTTP가 이전 요청의 사용자 정보를 다음 요청에 자동으로 붙여 주지 않는다는 뜻이다. 서버에 데이터를 저장하면 안 된다는 뜻은 아니다.

예를 들어 서버는 상품 정보를 데이터베이스에 저장해 두고, 상품 조회 요청이 올 때마다 읽을 수 있다. 로그인 정보도 서버의 세션 저장소에 보관할 수 있다. 이 경우 브라우저는 매 요청마다 세션 ID를 쿠키로 보내고, 서버는 그 ID로 저장된 로그인 정보를 찾는다.

서버가 여러 대라면 어느 서버가 요청을 받아도 로그인 정보를 찾을 수 있도록 세션 저장소를 공유하는 등의 설계가 필요하다.

더 알아보기 · 연결 유지와 HTTP 버전

무상태와 비연결성은 같은 말일까?

두 개념의 판단 대상이 다르다.

개념관심사예시
무상태이전 요청의 사용자 상태를 프로토콜이 기억하는가?다음 요청에도 인증 정보가 필요함
연결 재사용요청마다 전송 연결을 새로 만드는가?하나의 TCP 연결로 여러 요청을 주고받음

HTTP/1.1은 기본적으로 지속 연결(persistent connection)을 사용한다. 연결을 유지하면서도 HTTP는 무상태일 수 있다. 따라서 “HTTP는 매 응답 직후 TCP 연결을 반드시 끊는다”라고 답하면 부정확하다.

HTTP Keep-Alive와 TCP Keepalive는 어떻게 다를까?

  • HTTP의 지속 연결: 여러 요청·응답에 전송 연결을 재사용해 연결 수립 비용을 줄인다. HTTP/1.1에서는 기본 동작이며, 종료 의사를 Connection: close로 표현할 수 있다.
  • TCP Keepalive: 설정된 유휴 시간이 지난 연결에 탐색 패킷을 보내 상대가 여전히 응답 가능한지 확인하는 TCP 기능이다.

HTTP 연결 재사용과 TCP 연결 생존 확인은 관련이 있지만 역할이 다르다. TCP Keepalive가 있어야만 HTTP 연결을 재사용할 수 있는 것도 아니다.

버전이 바뀌면 메서드와 상태 코드도 달라질까?

버전연결·전송 방식의 핵심이해할 점
HTTP/1.0기본적으로 요청·응답 후 연결 종료지속 연결 확장이 사용되기도 했음
HTTP/1.1TCP 지속 연결, 텍스트 기반 메시지연결을 재사용하지만 일반적인 한 연결의 응답은 순서대로 처리
HTTP/2TCP 위 바이너리 프레임과 다중 스트림한 연결에서 여러 요청·응답을 다중화
HTTP/3UDP 위 QUIC의 스트림 사용TCP 대신 QUIC이 신뢰성과 스트림 관리를 제공

버전이 달라져도 GET·POST, 상태 코드, 헤더 등의 핵심 의미는 유지된다. HTTP/2가 무조건 HTTPS의 동의어인 것은 아니다. 웹 브라우저의 HTTP/2 사용은 일반적으로 TLS와 함께 이루어진다.

HTTP/2는 HTTP/1.1의 응답 순서에 의한 대기 문제를 줄이지만, TCP 패킷 손실로 여러 스트림이 함께 기다리는 문제는 남는다. QUIC은 스트림별 전달을 관리해 이 문제를 줄인다. “HTTP/3는 UDP라서 신뢰성을 포기한다”는 설명도 잘못이다.

면접 답변 · 꼬리 질문

HTTP는 요청과 응답의 의미를 정의하는 애플리케이션 계층 프로토콜입니다. 각 요청을 독립적으로 다루는 무상태성을 가지며, 로그인 상태는 쿠키나 토큰 등을 이용해 애플리케이션에서 관리합니다. 무상태와 연결 재사용은 별개이므로, HTTP/1.1에서는 연결을 유지하면서 여러 요청을 처리할 수 있습니다.

이어지는 질문

  • 로그인 상태는 어떻게 유지하나요? 쿠키나 토큰 등의 정보를 각 요청에 전달하고 서버가 확인합니다.
  • 무상태인데 TCP 연결을 유지해도 되나요? 사용자 상태와 연결 재사용은 서로 다른 문제입니다.

2. Request와 Response: 메시지를 어떻게 읽을까?​

HTTP/1.1 메시지는 시작 줄 → 헤더 → 빈 줄 → 선택적인 본문으로 이해하면 된다. 줄 구분은 실제로 CRLF를 사용하지만, 아래에서는 일반 코드 블록으로 표현한다.

요청 줄에는 무엇이 들어갈까?​

GET /products?category=keyboard&page=1 HTTP/1.1
Host: api.example.com
Accept: application/json

요소예시역할
메서드GET요청 동작의 의미
요청 대상/products?category=keyboard&page=1경로와 쿼리 등
버전HTTP/1.1메시지가 사용하는 HTTP 버전
헤더Host, Accept호스트와 원하는 응답 형식 등
본문이 예시에는 없음전달할 데이터

경로는 대상 리소스를 나타내고, 쿼리는 검색·필터·페이지 같은 조건을 전달하는 데 자주 사용한다.

HTTP/1.1 요청 줄에는 URL 전체가 들어갈까?

위 예시의 요청 URL이 https://api.example.com/products?category=keyboard&page=1이라면, HTTP/1.1 요청 줄에는 보통 /products?category=keyboard&page=1이 들어간다. 호스트 api.example.com은 Host 헤더에 적는다.

헤더와 본문은 어떤 관계일까?​

상품 등록 요청의 예시다.

POST /products HTTP/1.1
Host: api.example.com
Accept: application/json
Content-Type: application/json
Content-Length: 34

{"name":"키보드","price":50000}
  • 헤더는 본문의 형식, 인증 정보 등 메시지를 처리하기 위한 정보를 전달한다.
  • 빈 줄은 헤더 영역의 끝을 나타낸다.
  • 본문은 실제 등록할 데이터를 담는다.

Content-Length의 34는 위 JSON을 UTF-8로 인코딩한 바이트 수다. 문자 개수와 바이트 수는 다를 수 있다. 브라우저의 fetch를 사용할 때는 보통 Content-Length를 직접 설정하지 않는다.

응답은 요청과 무엇이 다를까?​

앞의 상품 등록에 성공해 새 리소스가 생성되었다고 가정하자.

HTTP/1.1 201 Created
Content-Type: application/json
Content-Length: 41
Location: /products/1

{"id":1,"name":"키보드","price":50000}
요소예시역할
상태 줄HTTP/1.1 201 CreatedHTTP 버전, 상태 코드, 설명 문구
상태 코드201리소스 생성 성공
헤더Content-Type, Location본문 형식과 생성된 리소스 위치 등
본문상품 정보 JSON처리 결과에 관한 데이터

Created 같은 설명 문구를 문자열로 검사하는 대신 상태 코드를 기준으로 처리한다. HTTP/2와 HTTP/3에는 HTTP/1.1 형태의 상태 줄이 없다.

요청이나 응답에 본문이 꼭 있어야 할까?​

상품 조회는 요청 본문 없이 상품 정보를 응답 본문으로 받고, 상품 등록은 요청과 응답에 본문이 있으며, 상품 삭제의 204 응답에는 본문이 없는 비교 그림

본문은 빈 줄 뒤에 오는 실제 데이터다. 그림에서 GET 요청은 조회할 대상을 URL로 지정하므로 보통 본문이 없지만, 200 OK 응답에는 조회한 상품 정보를 담을 수 있다. 상품 등록 POST 요청에는 등록할 데이터가 들어간다. 삭제 후 204 No Content 응답은 성공을 알리지만 본문은 없다.

따라서 204 응답에 response.json()을 호출하면 읽을 본문이 없어 파싱 오류가 난다. HEAD 요청의 응답, 304, 1xx 응답에도 본문이 없다.

더 알아보기 · 본문 길이와 메시지 경계
본문의 끝은 어떻게 구분할까?

HTTP/1.1에서는 빈 줄 뒤에 본문이 시작된다. 어디서 끝나는지는 다음처럼 알 수 있다. 아래 예시의 각 줄 끝은 실제 메시지에서 CRLF(\r\n)로 구분된다.

길이를 미리 아는 경우: Content-Length: 5이므로 빈 줄 뒤부터 정확히 5바이트를 읽는다. Hello가 본문 전체다.

HTTP/1.1 200 OK
Content-Type: text/plain
Content-Length: 5

Hello

길이를 미리 정하지 않은 경우: Transfer-Encoding: chunked는 본문을 조각으로 보낸다. 5는 다음 조각이 5바이트라는 뜻이고, 0은 더 이상 조각이 없다는 뜻이다. 마지막 빈 줄까지 읽으면 메시지가 끝난다.

HTTP/1.1 200 OK
Content-Type: text/plain
Transfer-Encoding: chunked

5
Hello
0

일부 응답은 연결 종료로 본문 끝을 구분할 수도 있지만, 요청은 그렇게 할 수 없다. HTTP/2·3는 이와 다른 스트림·프레임 구조를 사용한다.

면접 답변 · 꼬리 질문

HTTP/1.1 요청은 메서드·요청 대상·버전이 있는 요청 줄, 헤더, 빈 줄, 선택적인 본문으로 구성됩니다. 응답은 요청 줄 대신 버전과 상태 코드가 있는 상태 줄로 시작합니다. 헤더는 형식이나 인증 등의 정보를, 본문은 실제 데이터를 전달하며 모든 메시지에 본문이 있는 것은 아닙니다.

이어지는 질문

  • 모든 응답에 본문이 있나요? HEAD 응답, 204, 304 등에는 본문이 없습니다.
  • 헤더와 본문의 차이는 무엇인가요? 헤더는 처리에 필요한 정보, 본문은 실제 전달 데이터입니다.

3. Method: 어떤 동작을 요청하는가?​

HTTP 메서드는 대상 리소스에 수행하려는 동작의 의미를 나타낸다.

메서드의미예시
GET리소스의 표현 조회상품 목록·상세 조회
POST대상에서 요청 데이터를 처리하도록 요청상품 생성, 주문 처리
PUT전달한 표현으로 대상 상태 생성·대체지정한 상품의 전체 상태 교체
PATCH변경 지시를 적용상품 가격 부분 변경
DELETE대상 리소스 삭제 요청상품 삭제
HEADGET과 같은 조회 의미이나 응답 본문은 받지 않음파일의 메타데이터 확인
OPTIONS대상의 통신 옵션 확인지원 메서드 확인, CORS 사전 요청

GET과 POST는 어디에 데이터를 넣는지가 핵심일까?​

GET은 조회이고 주로 경로·쿼리·헤더로 정보를 전달하며 안전하고 멱등하다. POST는 요청 데이터를 처리하며 본문도 자주 사용하고 안전성·멱등성이 보장되지 않는다. 캐시와 URL 공유의 차이도 보여주는 그림

먼저 요청의 의미가 다르다. GET은 조회이고, POST는 데이터를 처리하도록 요청한다. POST의 흔한 사례가 생성이지만, POST를 반드시 생성으로만 제한하지 않는다. GET은 조회 조건을 URL에 담아 공유하기 쉽고, POST 본문은 URL만으로 공유되지 않는다.

여기서 안전은 보안이라는 뜻이 아니다. 안전성과 멱등성은 아래에서 구분한다. POST 본문이 주소창에 보이지 않아도 자동으로 암호화되는 것은 아니며, 전송 구간 보호에는 HTTPS가 필요하다.

PUT과 PATCH는 어떻게 다를까?​

상품의 수정 가능한 정보가 name, price 두 필드라고 가정하자. 아래 두 코드는 같은 작업을 위한 대안이며, 순서대로 실행하라는 뜻은 아니다.

// PUT: 대체할 전체 상태를 전달한다.
fetch('/api/products/1', {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name: '키보드', price: 45000 }),
});
// PATCH: 서버가 지원하는 형식으로 변경 내용을 전달한다.
fetch('/api/products/1', {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ price: 45000 }),
});

PUT은 대상이 없을 때 생성할 수도 있다. 다만 실제 허용 여부는 API 계약에 따른다. 전체 대체는 수정 가능한 표현의 범위로 이해해야 하며, 데이터베이스의 모든 컬럼을 클라이언트가 전달한다는 뜻은 아니다.

PATCH 본문은 단순히 필드 몇 개를 생략한 객체로만 한정되지 않는다. JSON Patch처럼 구체적인 변경 연산을 담는 형식도 있으며, 이 예시는 서버가 해당 JSON 객체를 부분 변경으로 해석한다고 가정한다.

안전성·멱등성·캐시는 어떻게 다를까?​

세 개념은 서로 다른 질문에 답한다.

속성판단 기준
안전성(Safe)클라이언트가 서버 상태 변경을 요청하지 않는가?
멱등성(Idempotent)동일 요청을 반복해도 의도한 효과가 한 번과 같은가?
캐시 가능성(Cacheable)응답을 저장해 이후 요청에 재사용할 수 있는가?

메서드의 기본 의미를 기준으로 분류하면 다음과 같다.

GET·HEAD·OPTIONS·TRACE·PUT·DELETE는 메서드 의미상 멱등성이 보장되고, POST·PATCH·CONNECT는 메서드 자체로 멱등성이 보장되지 않는다는 분류 그림

보장되지 않음은 모든 요청이 반드시 중복 변경을 일으킨다는 뜻이 아니다. 메서드 이름만 보고 같은 요청을 안전하게 재시도할 수 있다고 판단할 수 없다는 뜻이다.

GET·HEAD·OPTIONS·TRACE는 서버 상태 변경을 요청하지 않으므로 멱등하다. PUT은 같은 표현으로 대체하고 DELETE는 삭제를 요청하므로, 같은 요청을 반복해도 의도한 효과가 같다. 반면 POST·PATCH·CONNECT는 메서드의 의미만으로 반복 효과를 보장하지 않는다. 예를 들어 주문 생성 POST를 두 번 보내면 주문이 두 개 생길 수 있다.

멱등이면 응답도 항상 같아야 할까?​

아니다. 반복 요청이 서버에 의도한 효과가 같다는 뜻이다. 위 그림에서 멱등성이 보장되는 GET·PUT·DELETE를 예로 보자.

PUT은 같은 상태로 다시 대체하고, DELETE는 응답이 204와 404로 달라도 삭제 상태가 같으며, GET은 다른 사용자의 수정으로 응답이 달라져도 조회 자체는 상태 변경을 요청하지 않는 세 가지 멱등성 예시

PUT은 같은 값으로 다시 대체해도 결과 상태가 같다. DELETE는 204 다음 404처럼 응답이 달라도 상품이 삭제된 상태는 같다. GET은 두 요청 사이에 다른 사용자가 상품을 수정하면 응답이 달라질 수 있지만, GET 자체는 상태 변경을 요청하지 않는다.

더 알아보기 · GET/POST 주의점 · 안전성 · 재시도 · 캐시

그 밖의 메서드

  • CONNECT: 브라우저가 목적지 서버에 직접 연결하지 않고 중간 서버(프록시)를 거쳐야 할 때 사용한다. 예를 들어 프록시에 api.example.com:443으로 가는 통로를 열어 달라고 요청한다. 통로가 만들어지면 그 안에서 TLS 연결을 수립하고 HTTPS 데이터를 주고받는다. 일반적인 fetch 코드에서 직접 호출하는 메서드는 아니다.
  • TRACE: 요청이 전달된 끝에서 어떤 HTTP 요청을 받았는지 되돌려 받는 진단용 메서드다. 예를 들어 중간 서버를 거친 뒤 요청이 어떻게 도착했는지 확인할 때 쓸 수 있다. 상품 조회·수정 같은 일반 API 기능에는 쓰지 않는다.

GET 본문은 절대 존재할 수 없을까?

HTTP에서 GET 본문의 일반적인 의미는 정의되어 있지 않다. 일부 서버가 별도 규칙으로 지원할 수 있지만, 중간 프록시나 캐시까지 같은 방식으로 처리한다고 보장할 수 없다. 브라우저 fetch는 GET과 HEAD 요청의 본문을 허용하지 않는다.

조회 조건은 보통 쿼리에 넣는다.

const params = new URLSearchParams({
category: 'keyboard',
page: '1',
});

const response = await fetch(`/api/products?${params}`);

검색 조건이 매우 복잡하거나 URI에 노출하기 부적절한 데이터를 전달해야 한다면, API 계약에 따라 POST로 검색을 제공할 수도 있다. 이때도 POST가 조회라는 별도의 의미로 표준을 바꾸는 것은 아니며, GET이 제공하는 일반적인 캐시·링크 활용과는 차이가 있다.

POST는 GET보다 보안상 안전할까?

POST 본문이 주소창에 보이지 않는다고 암호화되는 것은 아니다. 평문 HTTP라면 본문도 전송 중 노출될 수 있다. 전송 구간 보호는 HTTPS가 담당한다.

한편 URL은 방문 기록이나 서버·프록시 로그 등에 남기 쉬우므로 비밀번호·토큰 같은 민감한 값을 쿼리에 넣지 않아야 한다. HTTPS를 사용해도 서버나 브라우저 측 기록 문제까지 사라지는 것은 아니다. 본문도 애플리케이션 설정에 따라 로그에 남을 수 있다.

GET은 2,048자, POST는 무제한일까?

모든 환경에 동일하게 적용되는 고정 제한으로 외우면 안 된다. URI 길이와 요청 본문 크기에는 브라우저·프록시·서버 등의 구현과 설정에 따른 제한이 있다. HTTP 전체를 하나의 숫자로 단정하지 않는다.


안전한 GET도 서버 로그를 남기지 않을까?

안전한 메서드도 서버 로그, 통계 기록 같은 부수 효과를 만들 수 있다. 핵심은 클라이언트가 요청한 동작의 의미가 조회인지다.

GET으로 상품 삭제를 수행하도록 설계하면 이 의미를 어긴다. 검색 엔진이나 브라우저의 사전 조회가 GET을 호출했을 때 의도하지 않은 삭제가 발생할 수 있다.

메서드안전성멱등성
GET, HEAD, OPTIONS있음있음
TRACE있음있음
PUT, DELETE없음있음
POST, PATCH보장하지 않음보장하지 않음
CONNECT보장하지 않음보장하지 않음

이 표는 HTTP가 정의한 의미를 기준으로 한다. 구현을 잘못해서 GET이 데이터를 삭제한다고 GET의 표준상 분류가 바뀌는 것은 아니다.

DELETE는 데이터베이스의 행을 지우는 명령일까?

HTTP는 내부 저장 방식까지 정하지 않는다. 서버는 논리 삭제나 외부 연결 해제 등으로 삭제를 구현할 수 있다. 클라이언트는 API가 정의한 리소스 삭제의 의미를 기준으로 요청한다.


응답을 못 받았다면 재시도해도 될까?

주문 생성 POST가 타임아웃됐다고 가정하자. 서버가 처리에 실패했을 수도 있지만, 주문 생성은 끝났고 응답만 유실됐을 수도 있다. 무조건 재시도하면 주문이 중복 생성될 수 있다.

서버와 합의한 멱등성 키를 사용하면 중복 실행을 제어하는 설계를 할 수 있다.

// 개념 예시: 같은 주문 작업의 재시도에서는 같은 키를 재사용한다.
const operationKey = crypto.randomUUID();

async function submitOrder() {
return fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': operationKey,
},
body: JSON.stringify({ productId: 1, quantity: 1 }),
});
}

이 코드는 하나의 주문 작업을 가정한다. 별개의 새 주문은 새 키를 발급해야 한다. 헤더를 넣는 것만으로 중복 방지가 생기지는 않는다. 서버가 키·요청 내용·처리 상태·결과를 관리하고 중복 요청을 판별해야 한다. 새 키를 매 재시도마다 발급하면 같은 작업을 식별할 수 없다.

프론트엔드에서 버튼을 비활성화하는 것은 중복 클릭을 줄이지만, 네트워크 재시도나 여러 탭의 요청까지 서버 측 중복 실행을 보장해 막는 방식은 아니다.


POST 응답은 캐시할 수 없을까?

표준상 조건을 만족하면 POST 응답도 캐시할 수 있다. 다만 일반적인 HTTP 캐시는 GET·HEAD를 중심으로 지원한다.

RFC 9110은 POST 응답에 명시적인 신선도 정보와 POST 대상 URI와 같은 Content-Location이 있는 조건을 설명한다. 그 캐시 응답을 이후 GET·HEAD에 사용할 수 있지만, 새 POST 실행을 캐시 응답으로 대신하는 것은 아니다. 따라서 “POST는 원천적으로 캐시 불가능”과 “POST도 GET처럼 보통 캐시된다” 모두 피해야 한다.

면접 답변 · 꼬리 질문

GET은 조회를, POST는 대상 리소스에서의 데이터 처리를 요청합니다. 단순히 쿼리와 본문의 차이로만 구분하지 않습니다. PUT은 전달한 표현으로 대상 상태를 대체하거나 생성하고, PATCH는 변경 지시를 적용합니다. PUT은 멱등하지만 PATCH는 메서드 자체로 멱등성을 보장하지 않습니다.

이어지는 질문

  • 멱등이면 응답도 같아야 하나요? 응답이 아니라 서버에 의도한 효과를 기준으로 판단합니다.
  • POST를 다시 보내도 될까요? 응답만 유실됐을 수 있으므로 서버의 중복 방지 계약과 요청의 의미를 확인해야 합니다.

4. Status Code: 결과에 따라 무엇을 판단할까?​

상태 코드는 서버가 요청을 어떻게 처리했는지 알려 주는 세 자리 숫자다.

범위의미대표 예시
1xx중간 처리 정보100 Continue
2xx성공200 OK, 201 Created
3xx리다이렉션 등 추가 동작301, 304, 307
4xx클라이언트 요청 관련 오류400, 401, 403, 404
5xx서버의 처리 실패500, 502, 503, 504

200·201·202·204는 어떻게 다를까?​

코드의미예시
200 OK요청 성공상품 조회 결과 반환
201 Created하나 이상의 리소스 생성 성공새 상품 생성, Location으로 위치 안내 가능
202 Accepted처리를 접수했으나 아직 완료되지 않음대용량 보고서 생성 작업 접수
204 No Content요청 성공, 본문 없음삭제 성공 후 전달할 데이터 없음
206 Partial Content범위 요청에 대한 일부 내용 반환Range를 사용한 파일 일부 다운로드

202를 받았다고 작업의 최종 성공까지 확정된 것은 아니다. 완료 여부를 확인할 작업 ID나 조회 API가 추가로 필요할 수 있다.

401과 403은 인증과 인가로 설명하면 될까?​

  • 401 Unauthorized: 유효한 인증 정보가 없는 경우다. 로그인하지 않았거나 토큰이 만료된 상황이 대표적이다. 서버는 WWW-Authenticate 헤더로 인증 요구를 안내해야 한다.
  • 403 Forbidden: 서버가 요청을 이해했지만 수행을 거부한다. 인증은 됐지만 관리자 권한이 없는 상황이 대표적이다. 다만 원인이 인증 후 권한 부족으로만 한정되지는 않는다.

인증은 “누구인지 확인”, 인가는 “해당 작업을 허용할지 판단”으로 구분할 수 있다. 403을 받았다고 무조건 재로그인을 반복하는 처리는 적절하지 않을 수 있다.

fetch는 404를 받으면 바로 catch로 갈까?​

const response = await fetch('/api/products/999');

if (!response.ok) {
throw new Error(`상품 조회 실패: ${response.status}`);
}

아니다. fetch는 404·500 같은 HTTP 오류 응답도 Response로 전달한다. response.ok는 200~299일 때 true이며, 위 예제에서는 이를 검사해 직접 예외를 던진다.

상황실패가 나타나는 단계
서버가 404 반환fetch는 Response 전달, 코드에서 상태 확인
네트워크 실패·CORS 제한 등fetch Promise가 거부될 수 있음
응답이 JSON 형식을 어김response.json() Promise가 거부될 수 있음
204에 JSON 파싱 시도빈 본문 때문에 파싱 실패
더 알아보기 · 리다이렉션 · 오류 코드 · 응답 처리

리다이렉션은 어떤 요청으로 이어질까?

코드의미메서드 관련 주의점
301영구 이동POST가 GET으로 변경될 수 있음
302임시 이동POST가 GET으로 변경될 수 있음
303다른 위치에서 결과 조회보통 GET으로 결과 조회, HEAD도 가능
307임시 이동자동 리다이렉션 시 메서드 변경 금지
308영구 이동자동 리다이렉션 시 메서드 변경 금지

새 위치는 일반적으로 Location 헤더로 안내한다. 메서드와 본문을 유지해야 한다면 307·308의 차이를 설명할 수 있어야 한다. 단, 본문을 재전송할 수 있는지와 클라이언트 정책 등에 따라 리다이렉션이 실패할 수도 있다.

304 Not Modified는 다른 URL로 이동하라는 뜻이 아니다. 조건부 조회에서 기존에 저장된 표현이 여전히 유효하다는 의미이며, 본문 없이 캐시를 재사용하게 한다.


실무에서 구분할 만한 다른 오류는?

코드의미확인할 점
400 Bad Request잘못된 요청구문·형식·요청 조건
404 Not Found현재 표현을 찾지 못했거나 존재를 공개하지 않음주소·대상 존재·접근 정책
405 Method Not Allowed해당 리소스에서 메서드 미지원Allow 헤더와 API 명세
409 Conflict현재 리소스 상태와 충돌중복 데이터·상태 전이 충돌 등
412 Precondition Failed요청의 사전 조건 실패If-Match 등 조건과 현재 버전
415 Unsupported Media Type본문 형식·코딩 등을 지원하지 않음Content-Type 등
422 Unprocessable Content형식과 구문은 이해했으나 내용 지시를 처리하지 못함도메인 검증 규칙 등
429 Too Many Requests요청 빈도 제한재시도 정책, Retry-After가 있으면 참고
500 Internal Server Error예기치 못한 서버 내부 오류서버 로그·오류 식별자
502 Bad Gateway게이트웨이가 상위 서버에서 잘못된 응답을 받음프록시·상위 서비스
503 Service Unavailable일시적으로 처리 불가과부하·점검 등, Retry-After가 있으면 참고
504 Gateway Timeout게이트웨이가 상위 서버 응답을 제때 받지 못함상위 서비스 지연

클라이언트 자체의 타임아웃이 반드시 HTTP 504로 전달되는 것은 아니다. 응답을 받지 못하고 네트워크 오류로 끝날 수도 있다.

모든 응답을 200으로 보내고 body의 code만 보면 안 될까?

애플리케이션 내부 오류 코드를 추가할 수는 있다. 하지만 실패 응답을 모두 200으로 반환하면 HTTP 상태에 기반한 모니터링, 캐시, 클라이언트 오류 처리의 의미가 흐려진다.

일반적으로 HTTP 상태로 큰 처리 결과를 표현하고, 본문에는 상세 오류 코드와 설명을 함께 제공한다. 다만 실제 연동에서는 해당 API 계약을 따라야 한다.


아래는 JSON을 반환하거나 204로 본문 없이 성공하는 API를 위한 예시다. 인증 갱신·재시도·상세 오류 본문 처리는 별도의 API 계약이 필요하므로 이 코드에 포함하지 않았다.

async function requestJson(url, options) {
const response = await fetch(url, options);

// ① HTTP 처리 결과 확인
if (!response.ok) {
throw new Error(`HTTP 오류: ${response.status}`);
}

// ② 명세상 본문이 없는 성공 응답 처리
if (response.status === 204) {
return undefined;
}

// ③ 이 API가 JSON을 반환한다는 계약 확인
const contentType = response.headers.get('content-type') ?? '';
const mediaType = contentType.split(';', 1)[0].trim().toLowerCase();

if (mediaType !== 'application/json' && !mediaType.endsWith('+json')) {
throw new Error('JSON 응답이 아닙니다.');
}

// ④ 본문 읽기와 JSON 파싱
return response.json();
}
try {
const product = await requestJson('/api/products/1');
console.log(product);
} catch (error) {
console.error(error);
}

상태 확인, 형식 확인, 본문 파싱은 각각 별개의 단계다. JSON Content-Type이 있어도 실제 본문이 잘못되면 파싱은 실패한다. 이 함수는 JSON/204 전용이며 HEAD, 텍스트, 파일 응답 전체를 처리하는 범용 함수는 아니다.

AbortController로 취소하면 서버 작업도 취소될까?

클라이언트가 응답 대기를 중단했다고 서버 작업이 반드시 취소되는 것은 아니다. 이미 서버에 전달되어 처리된 주문이 있을 수 있다.

const controller = new AbortController();

const request = fetch('/api/products/1', {
signal: controller.signal,
});

controller.abort();

try {
await request;
} catch (error) {
console.log(error.name); // 일반적인 기본 취소 사유는 AbortError
}

취소 후 서버 상태를 확인할 필요가 있는지는 작업의 성격에 따라 판단한다. 주문이나 결제처럼 중복 실행이 문제가 되는 작업은 타임아웃·취소·재시도를 함께 설계해야 한다.

면접 답변 · 꼬리 질문

상태 코드는 요청의 처리 결과를 나타냅니다. 2xx는 성공, 3xx는 리다이렉션 등 추가 동작, 4xx는 요청 관련 오류, 5xx는 서버 처리 실패입니다. 프론트엔드에서는 특히 인증 문제인 401과 거부를 나타내는 403, 본문이 없는 204를 구분해 처리해야 합니다.

이어지는 질문

  • 202는 작업 완료인가요? 접수됐다는 뜻이며 최종 성공을 보장하지 않습니다.
  • 204는 성공인데 왜 JSON 파싱이 실패하나요? 응답 본문이 없어 JSON으로 해석할 수 없기 때문입니다.

5. Header: 메시지를 어떻게 해석하고 처리할까?​

헤더는 본문 형식, 인증, 캐시, 리다이렉션 등 메시지 처리에 필요한 추가 정보를 전달한다. 헤더 이름은 대소문자를 구분하지 않지만, 값까지 항상 대소문자를 무시하는 것은 아니다.

헤더주로 쓰는 방향역할
Accept요청원하는 응답 미디어 타입
Content-Type요청·응답해당 본문의 미디어 타입
Authorization요청인증 자격 정보
Cookie / Set-Cookie요청 / 응답쿠키 전달 / 설정
Cache-Control요청·응답캐시 지침

Accept와 Content-Type은 어떻게 구분할까?​

fetch('/api/products', {
method: 'POST',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ name: '키보드', price: 50000 }),
});
  • Accept: “응답으로 JSON을 받고 싶다.”
  • Content-Type: “지금 보내는 본문은 JSON이다.”
  • JSON.stringify: JavaScript 객체를 실제 JSON 문자열로 변환한다.

헤더 선언은 데이터 변환을 대신하지 않는다. 또한 Accept를 설정했다고 서버가 반드시 그 형식을 제공하는 것도 아니다. 콘텐츠 협상 결과에 따라 406을 반환하거나 서버 정책에 따라 다른 방식으로 응답할 수 있다.

Content-Type과 Content-Encoding도 다르다. 예를 들어 JSON을 gzip으로 압축했다면 Content-Type은 application/json, Content-Encoding은 gzip이다.

FormData의 Content-Type은 직접 설정할까?

fetch에 FormData를 넣으면 브라우저가 본문의 구분자인 boundary를 포함해 Content-Type을 생성한다. 일반적으로 직접 multipart/form-data만 지정하지 않는다. 서버가 경계를 해석하지 못할 수 있기 때문이다.

더 알아보기 · 캐시 · CORS · TanStack Query

함께 알아둘 헤더

헤더주로 쓰는 방향역할
Host요청, HTTP/1.1대상 호스트·포트
Content-Length요청·응답해당 조건에서의 본문 바이트 길이
Content-Encoding요청·응답gzip 등 표현에 적용한 코딩
WWW-Authenticate응답인증 요구·방식 안내
Location응답리다이렉션 또는 생성 리소스 위치 등
Allow응답대상이 지원하는 메서드
ETag / If-None-Match응답 / 요청표현의 버전 검증
Retry-After응답재시도 전 대기 안내

no-cache와 no-store는 어떻게 다를까?

응답의 Cache-Control을 기준으로 보자.

지시자의미
max-age=60응답의 나이 등을 기준으로 60초 동안 신선한 것으로 취급
no-cache저장은 가능하나 재사용 전에 성공적인 검증 필요
no-store해당 응답을 저장하지 않도록 지시
private브라우저 같은 개인 캐시에는 저장 가능, 공유 캐시에는 저장 금지
public다른 제한 조건도 따르면서 공유 캐시 저장을 허용

no-cache는 저장 금지가 아니다. 저장 금지가 목적이면 no-store를 구분해야 한다. 개인화 데이터에 무조건 public을 지정해서도 안 된다.

ETag와 304는 어떻게 연결될까?

서버가 상품 데이터를 처음 반환하며 ETag를 보낸다고 가정하자.

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 41
Cache-Control: private, no-cache
ETag: "product-1-v1"

{"id":1,"name":"키보드","price":50000}

클라이언트의 캐시는 본문과 검증자를 저장할 수 있다. 다음 조회에서 저장된 표현이 여전히 유효한지 확인한다.

GET /products/1 HTTP/1.1
Host: api.example.com
If-None-Match: "product-1-v1"

서버에서 표현이 바뀌지 않았다면 다음과 같이 응답할 수 있다.

HTTP/1.1 304 Not Modified
Cache-Control: private, no-cache
ETag: "product-1-v1"

304는 새 본문을 보내지 않는다. 캐시는 필요한 메타데이터를 갱신하고 저장된 본문을 재사용한다. 바뀌었다면 보통 새 본문과 새 ETag를 200으로 반환한다.

이런 HTTP 캐시 검증은 보통 브라우저가 처리한다. 따라서 Network 수준에서 304가 발생했다고 fetch 코드가 반드시 status 304를 그대로 받는 것은 아니다. 브라우저가 저장된 응답과 합쳐 전달할 수 있다.


OPTIONS가 보이면 CORS 때문일까?

OPTIONS는 원래 대상의 통신 옵션을 확인하는 메서드다. 브라우저의 CORS 사전 요청(preflight)에도 쓰이지만 모든 OPTIONS 요청이 CORS인 것은 아니다.

예를 들어 서로 다른 출처 간에 JSON POST를 보내거나 Authorization 헤더를 사용하면 보통 사전 요청이 필요하다. 사전 요청 결과가 캐시되어 생략되는 경우도 있다.

OPTIONS /products HTTP/1.1
Host: api.example.com
Origin: https://app.example.com
Access-Control-Request-Method: POST
Access-Control-Request-Headers: content-type

서버는 다음과 같은 헤더로 실제 요청을 허용할지 안내할 수 있다.

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: POST
Access-Control-Allow-Headers: content-type

실제 요청에 대한 응답에도 필요한 CORS 헤더가 있어야 한다. 자격 증명을 포함하는 경우에는 Access-Control-Allow-Credentials 등 추가 조건도 확인해야 한다.

CORS는 브라우저가 다른 출처의 응답을 스크립트에 공개하는 것을 제어하는 체계다. 서버 인증·인가를 대체하지 않는다. 사전 요청 없이 전송되는 요청은 CORS 확인이 실패해도 이미 서버에 도달했을 수 있으므로, “CORS 오류면 서버에 요청이 아예 안 간다”라고 단정하지 않는다.


HTTP 캐시와 TanStack Query 캐시는 같은 것일까?

구분HTTP 캐시TanStack Query의 캐시
주체브라우저·중간 캐시 등애플리케이션 라이브러리
저장 기준HTTP 메서드·URI·헤더·캐시 정책 등queryKey와 QueryClient 설정 등
대표 설정Cache-Control, ETag, VarystaleTime, gcTime, 무효화 등
재사용 대상HTTP 응답queryFn이 반환한 데이터

invalidateQueries 등으로 다시 요청하더라도 그 아래의 HTTP 계층이 캐시를 활용할 수 있다. 반대로 HTTP 응답에 no-store가 있어도 queryFn의 반환값을 라이브러리가 메모리에 보관하는 정책까지 자동으로 꺼지는 것은 아니다.

이 비교는 라이브러리 캐시의 세부 API보다 애플리케이션 데이터 캐시와 HTTP 캐시가 다른 계층이라는 점을 설명하기 위한 것이다.

면접 답변 · 꼬리 질문

헤더는 메시지를 해석하고 처리하는 데 필요한 정보를 전달합니다. Accept는 원하는 응답 형식, Content-Type은 해당 본문의 형식입니다. Cache-Control과 ETag는 캐시 정책과 검증에 사용하며, no-cache는 저장 금지가 아니라 재사용 전 검증을 요구한다는 점을 구분해야 합니다.

이어지는 질문

  • Content-Type을 지정하면 객체가 JSON이 되나요? 형식을 알릴 뿐이며 JSON.stringify 같은 실제 변환이 필요합니다.
  • no-cache는 저장 금지인가요? 저장은 가능하며 재사용 전 검증이 필요합니다. 저장 금지는 no-store와 구분합니다.

복습 문제​

1. 같은 요청인데 응답이 다르면 멱등하지 않을까?​

퀴즈. 아래 결과만으로 DELETE가 멱등하지 않다고 판단할 수 있을까? 두 호출 사이에 상품이 다시 생성되는 등 다른 상태 변경은 없다고 가정한다.

await fetch('/api/products/1', { method: 'DELETE' }); // 204
await fetch('/api/products/1', { method: 'DELETE' }); // 404
정답 보기

판단할 수 없다. 첫 요청으로 상품이 삭제되고 두 번째 요청에서도 삭제된 상태가 유지된다면 의도한 효과는 같다. 응답 코드가 같아야 멱등한 것은 아니다.

2. 404 응답이면 어느 코드가 실행될까?​

퀴즈. fetch가 실제 404 Response를 받았다고 가정한다. 출력 순서는 무엇일까?

try {
const response = await fetch('/api/products/999');
console.log('A');

if (!response.ok) {
throw new Error('상품이 없습니다.');
}

console.log('B');
} catch (error) {
console.log('C');
}
정답 보기

A → C다. 404라는 이유만으로 fetch가 예외를 던지지는 않는다. 이후 코드가 response.ok를 검사해 직접 예외를 던지므로 B는 실행되지 않는다.

3. 삭제에 성공했는데 왜 Promise가 거부될까?​

퀴즈. 서버가 204를 반환할 때 아래 함수에서 문제가 되는 부분을 찾아 수정해 보자.

async function deleteProduct(id) {
const response = await fetch(`/api/products/${id}`, {
method: 'DELETE',
});

if (!response.ok) throw new Error('삭제 실패');

return response.json();
}
정답 보기

204는 성공 상태지만 본문이 없다. response.json()이 빈 본문을 JSON으로 해석하려 하므로 실패한다.

다른 성공 응답은 JSON을 반환한다는 계약이라면 다음처럼 처리한다.

if (response.status === 204) return;
return response.json();

삭제 API가 항상 본문 없이 성공한다면 JSON 파싱 자체를 생략한다.

4. 이 요청은 무엇을 보내고 무엇을 받고 싶을까?​

퀴즈. 서버가 해당 JSON 객체를 부분 변경으로 지원한다고 가정한다. 메서드, 본문, Accept, Content-Type을 각각 설명해 보자.

fetch('/api/products/1', {
method: 'PATCH',
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({ price: 45000 }),
});
정답 보기
  • PATCH는 부분 변경을 요청한다.
  • 본문은 가격을 45,000원으로 바꾸라는 데이터다.
  • Accept는 응답을 JSON으로 받고 싶다는 뜻이다.
  • Content-Type은 보내는 본문이 JSON이라는 뜻이다.

JSON 문자열을 만드는 것은 헤더가 아니라 JSON.stringify다.

5. 새 키로 재시도하면 중복 주문을 막을 수 있을까?​

퀴즈. 서버가 Idempotency-Key로 중복 작업을 식별한다고 가정한다. 첫 주문이 서버에서 생성됐으나 응답을 받지 못한 경우 아래 코드의 문제는 무엇일까?

async function submitOrder() {
return fetch('/api/orders', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Idempotency-Key': crypto.randomUUID(),
},
body: JSON.stringify({ productId: 1, quantity: 1 }),
});
}

// 사용자 코드가 실패 후 submitOrder()를 다시 호출한다고 가정한다.
정답 보기

호출할 때마다 새 키를 만들므로 서버가 같은 주문 작업의 재시도라고 식별하지 못할 수 있다. 이미 생성한 주문과 별개의 새 주문으로 처리될 수 있다.

한 작업을 시작할 때 키를 만들고 그 작업의 재시도에서는 같은 키를 재사용해야 한다. 별개의 새 주문에는 새 키를 사용한다. 서버의 중복 판별 구현도 필요하다.


핵심 정리​

  • HTTP 메시지의 의미와 전송 연결의 동작을 구분한다. 무상태여도 연결을 재사용할 수 있다.
  • 요청은 대상과 동작을, 응답은 처리 결과를 전달한다. 헤더와 본문이 맡는 역할도 다르다.
  • GET/POST는 요청의 의미로 구분한다. POST가 자동으로 암호화되거나 GET이 무조건 캐시되는 것은 아니다.
  • 안전성·멱등성·캐시는 서로 다르다. DELETE는 안전하지 않지만 멱등하다.
  • 멱등성은 동일 응답이 아니라 동일한 의도된 효과다. 재시도와 중복 실행을 설명할 때 연결한다.
  • 상태 코드별로 후속 동작이 다르다. 202는 접수, 204는 본문 없음, 304는 캐시 검증, 401은 인증 요구다.
  • Accept와 Content-Type, no-cache와 no-store를 구분한다.
  • HTTP 성공, 본문 파싱 성공, 실제 비즈니스 작업 완료는 각각 확인해야 한다.

공통 질문인 “주소창에 URL을 입력한 뒤 화면이 표시될 때까지 무슨 일이 일어나는가?”에서 이 파트는 HTTP 요청을 구성하고, 서버의 응답을 해석하며, 캐시·리다이렉션·인증 요구 등에 대응하는 구간이다. DNS·연결 수립은 앞 파트와, 응답 이후의 화면 생성은 다음 렌더링 파트와 연결한다.


참고​

스터디 질문의 출발점​

개념 확인용 표준·공식 문서​