301, 302, 307, 308 리다이렉트의 차이: 영구와 임시, 그리고 POST가 GET이 되는 이유

HTTP 리다이렉트 301·302·303·307·308의 차이를 RFC 9110과 WHATWG Fetch 표준으로 정리했습니다. POST가 GET으로 바뀌는 조건, 캐시와 검색 엔진에서의 차이도 다룹니다.

개발웹네트워크

폼을 POST로 보냈는데 서버 로그에는 새 주소로 들어온 GET 요청만 있고 본문은 비어 있다면, 중간에 받은 리다이렉트의 상태 코드부터 확인해 볼 만합니다. 301이나 302를 받은 클라이언트는 POST를 GET으로 바꿔 다시 요청해도 됩니다. 브라우저가 따르는 Fetch 표준과 curl의 기본 -L 동작은 POST 요청에서 실제로 그렇게 처리합니다. 307과 308에서는 메서드가 바뀌지 않습니다.

두 가지 기준으로 네 가지 코드를 나눈다

HTTP 명세인 RFC 9110은 301, 302, 307, 308을 “이 리소스는 Location 헤더에 적힌 다른 URI에 있을 수 있다”는 종류의 리다이렉트로 묶습니다. 네 코드는 두 가지 질문으로 구분됩니다.

  • 옮겨 간 것이 영구적인가, 임시인가
  • 리다이렉트를 자동으로 따라갈 때 요청 메서드를 바꿔도 되는가
POST→GET 변경 허용 메서드 유지
영구 301 Moved Permanently 308 Permanent Redirect
임시 302 Found 307 Temporary Redirect

HTTP 리다이렉트 상태 코드 선택표. 가로축은 POST를 GET으로 바꿀 수 있는 코드와 메서드를 유지하는 코드, 세로축은 영구 이동과 임시 이동입니다. 영구 이동에서는 301 Moved Permanently가 POST를 GET으로 바꿀 수 있고 308 Permanent Redirect가 메서드를 유지합니다. 임시 이동에서는 302 Found가 POST를 GET으로 바꿀 수 있고 307 Temporary Redirect가 메서드를 유지합니다. 아래에 따로 303 See Other가 있습니다. 303은 이동이 아니라 다른 URI에 있는 간접 응답을 가리키며, GET이나 HEAD로 가져옵니다. 폼 POST 뒤의 결과 페이지가 대표적인 쓰임입니다.

그림: 코드를 고르기 위한 단순화한 선택표입니다. 캐시, 검색 엔진, 클라이언트 구현에 따른 차이와 예외는 본문에서 다룹니다.

301과 308은 리소스에 새 영구 URI가 할당됐으니 앞으로는 그 URI를 쓰라는 뜻입니다. 302와 307은 리소스가 잠시 다른 URI에 있을 뿐이므로 클라이언트는 앞으로도 원래 URI로 요청하라는 뜻입니다.

메서드에 관해서는 307의 규정이 가장 분명합니다. 사용자 에이전트가 리다이렉트를 자동으로 따라갈 때 요청 메서드를 바꾸면 안 됩니다(MUST NOT). RFC 9110의 308 절에는 같은 문장이 없습니다. 대신 명세는 307과 308을 “메서드를 유지하는 리다이렉트임을 모호하지 않게 나타내기 위해” 추가된 코드라고 설명하고, 301 절에서도 메서드가 바뀌는 것을 원하지 않으면 308을 쓰라고 안내합니다.

301과 302에서 POST가 GET으로 바뀌는 이유

RFC 9110의 301과 302 절에는 “역사적인 이유로” 사용자 에이전트가 다음 요청의 메서드를 POST에서 GET으로 바꿔도 된다(MAY)는 주석이 있습니다.

명세가 밝히는 경위는 이렇습니다. HTTP/1.0에서 301과 302는 원래 메서드를 유지하는 코드로 정의됐습니다. 그런데 초기 사용자 에이전트들은 POST를 그대로 POST로 보낼지, 다른 사이트로 넘어갈 때 더 안전한 GET으로 바꿀지에서 갈렸고, 널리 퍼진 관행은 GET으로 바꾸는 쪽으로 모였습니다. 그래서 메서드를 확실히 유지하는 307과 308이 나중에 추가됐고, 301과 302의 정의는 POST를 GET으로 바꾸는 것을 허용하도록 조정됐습니다.

그러니 301이나 302에서 메서드가 바뀌는 것은 명세 위반이 아니라 명세가 허용한 선택입니다. 바꾸지 않는 것도 허용되며, 어느 쪽인지는 클라이언트 구현이 정합니다.

303은 “결과는 다른 곳에서 가져가라”

303 See Other는 위 네 코드와 성격이 다릅니다. 원래 리소스가 옮겨 갔다는 뜻이 아니라, 요청에 대한 간접적인 응답을 다른 리소스에서 가져가라는 뜻입니다. 사용자 에이전트는 그 URI에 GET이나 HEAD로 요청합니다. RFC 9110은 303이 주로 POST 처리 결과를 별도 리소스로 보내, 그 결과를 따로 식별하고 북마크하고 캐시할 수 있게 하는 데 쓰인다고 설명합니다. 폼을 처리한 뒤 결과 페이지를 GET으로 열게 하려는 의도라면 302보다 303이 그 의도를 정확히 나타냅니다.

브라우저의 규칙: Fetch 표준

브라우저가 리다이렉트를 따라가는 절차는 WHATWG Fetch 표준의 “HTTP-redirect fetch”에 정해져 있습니다. 이 표준은 301, 302, 303, 307, 308을 리다이렉트 상태로 정의하고, 다음 경우에 요청 메서드를 GET으로 바꾸고 본문을 비웁니다.

  • 상태가 301 또는 302이고 요청 메서드가 POST일 때
  • 상태가 303이고 요청 메서드가 GET이나 HEAD가 아닐 때

307과 308은 이 메서드 변경 조건에 포함되지 않습니다. 따라서 리다이렉트를 따라갈 수 있는 요청에서는 메서드와 본문을 GET과 빈 본문으로 바꾸지 않습니다. 조건을 보면 301이나 302라도 PUT이나 DELETE 요청이면 이 변환이 일어나지 않는다는 것도 알 수 있습니다.

다만 이것이 모든 요청 본문을 항상 다시 보낼 수 있다는 뜻은 아닙니다. 같은 Fetch 절차는 303이 아닌 리다이렉트에서 요청 본문을 재생할 수 없으면 네트워크 오류로 끝냅니다.

같은 절차에는 다른 규칙도 있습니다. 리다이렉트 횟수가 20에 이르면 네트워크 오류로 끝나고, 요청이 다른 출처(origin)로 넘어가면 Authorization 헤더가 제거됩니다. RFC 9110도 클라이언트가 순환 리다이렉트를 감지해 개입해야 한다고(SHOULD) 적고, 예전 명세(RFC 2068)가 최대 5회를 권장했으므로 그런 고정 한도를 둔 클라이언트가 있을 수 있다고 덧붙입니다.

curl로 확인하기

curl의 -L(--location)도 같은 방향으로 동작합니다. curl 매뉴얼에 따르면 POST 요청이 301, 302, 303 응답을 받으면 다음 요청을 GET으로 보내고, 그 밖의 3xx에서는 메서드를 바꾸지 않습니다. 301과 302에서도 POST를 유지하려면 --post301, --post302 옵션을 씁니다.

로컬에서 확인해 봤습니다. /from/<코드>로 요청하면 그 상태 코드로 /to에 리다이렉트하고, /to는 받은 메서드와 본문을 그대로 돌려주는 작은 Python 서버를 띄운 뒤 macOS의 curl 8.7.1로 요청했습니다.

for c in 301 302 303 307 308; do
  printf "$c -> "; curl -s -L -d 'x=1' http://127.0.0.1:8765/from/$c
done
printf "301 --post301 -> "; curl -s -L --post301 -d 'x=1' http://127.0.0.1:8765/from/301

실제 출력입니다.

301 -> GET /to body=b''
302 -> GET /to body=b''
303 -> GET /to body=b''
307 -> POST /to body=b'x=1'
308 -> POST /to body=b'x=1'
301 --post301 -> POST /to body=b'x=1'

이 출력은 위 환경(macOS, curl 8.7.1)에서 한 번 실행한 결과이고, 테스트 서버의 코드는 싣지 않았습니다. 다른 버전의 curl이나 다른 HTTP 클라이언트에서는 결과가 다를 수 있으니, 쓰는 환경에서 직접 확인해 보세요.

캐시와 검색 엔진에서의 차이

영구와 임시의 차이는 캐시에서도 나타납니다. RFC 9110은 301과 308을 “휴리스틱으로 캐시할 수 있는” 응답으로 분류합니다. 명시적인 만료 정보가 없어도 캐시가 유효 기간을 추정해 응답을 재사용할 수 있다는 뜻입니다. 302와 307은 이 분류에 들지 않습니다. 어느 쪽이든 Cache-Control 같은 명시적 캐시 지시가 있으면 그것을 따릅니다. 따라서 되돌릴 수도 있는 이동에 301을 쓰면, 이미 응답을 캐시한 클라이언트가 되돌린 뒤에도 옛 리다이렉트를 재사용할 수 있습니다. 되돌릴 가능성이 있다면 임시 코드를 쓰거나 캐시 지시를 명시해 두는 편이 낫습니다.

검색 엔진은 HTTP 명세가 아니라 각자의 정책을 따릅니다. Google Search Central 문서(2026년 4월 14일 갱신)에 따르면 Google은 301과 308을 영구 리다이렉트로 보고 대상 URL을 표준(canonical)으로 삼으라는 신호로 씁니다. 302, 303, 307은 임시 리다이렉트로 보고 그런 신호로 쓰지 않습니다. 다만 다른 표준화 신호가 있으면 대상 페이지가 색인될 수도 있다고 합니다.

어떤 코드를 쓸까

  • 도메인을 옮겼거나 여러 주소를 한 주소로 통일할 때처럼 이동이 영구적이면 301이나 308을 씁니다. POST 요청을 받는 API 주소를 옮겼다면 메서드가 유지되는 308이 맞습니다.
  • 서비스가 잠시 중단돼 안내 페이지로 보내는 것처럼 일시적이면 302나 307을 씁니다. 메서드와 본문을 유지해야 한다면 307입니다.
  • 폼을 처리한 뒤 결과 페이지를 GET으로 열게 하려면 303을 씁니다.

한 가지 더 있습니다. 308은 2014년 6월 실험적 RFC에서 처음 정의돼 다른 코드보다 훨씬 새로운 코드이고, RFC 9110은 그래서 308이 모든 곳에서 인식되지는 않을 수 있다고 주석에 적고 있습니다. 오래된 클라이언트까지 지원해야 한다면 그 클라이언트에서 직접 확인해 보세요.

참고한 명세와 문서

링크와 내용은 2026년 9월 29일에 확인했습니다. Fetch는 계속 갱신되는 표준이고 Google 문서도 바뀔 수 있습니다.

블로그로