HTTP 302 에러(302 Found) 뜻과 해결 방법: 원인과 301 차이
핵심 요약
HTTP 302 Found는 에러가 아니라 임시 리다이렉트입니다. 서버가 '지금은 다른 URL에 있다'고 알리며 그 URL을 Location 헤더에 담아 보내면, 브라우저는 곧바로 그 URL을 불러오고 링크와 검색엔진은 원래 URL을 그대로 유지합니다. 영구 이동에는 301이나 308을 쓰세요. 저희가 확인한 대형 사이트 20곳 중 영구 이동에 302를 쓴 곳은 없었습니다. 302는 주로 로그인이 필요한 페이지에서 만납니다. 브라우저는 302를 따라갈 때 POST를 GET으로 바꾸므로, 메서드를 유지하려면 307을, 폼 제출 뒤에는 303을 쓰세요.
지금 확인하기

읽기 시작
HTTP 302 상태 코드란?
HTTP 302 Found는 임시 리다이렉트입니다. 서버가 '요청한 리소스는 지금은 다른 곳에 있다'고 응답하면서 그 URL을 Location 헤더에 담아 보냅니다. 브라우저는 그 URL을 곧바로 불러오지만, 북마크와 링크, 검색엔진은 원래 URL을 계속 사용해야 합니다(RFC 9110 §15.4.3).
'지금은'이라는 점이 301 Moved Permanently와의 차이의 전부입니다. 301은 이전 URL이 완전히 사라졌다는 뜻입니다. HTTP/1.0에서는 302를 'Moved Temporarily'라고 불렀고(RFC 1945), 1999년 RFC 2616에서 이름이 'Found'로 바뀌었지만 의미는 그대로입니다.
방문자는 302를 볼 일이 거의 없습니다. 화면에 무언가 표시되기 전에 브라우저가 먼저 따라가기 때문입니다. 302는 주로 DevTools, curl, API 클라이언트, SEO 점검 도구에서 만나게 됩니다.
모든 리다이렉트와 상태 코드 확인하기
302 응답의 구조
302 응답은 간단합니다. 구성 요소는 다음과 같습니다.
- 상태 라인 —
HTTP/1.1 302 Found또는 간단히HTTP/2 302(HTTP/2에는 'Found' 같은 사유 문구가 없습니다) - Location — 이동할 URL입니다. RFC 9110은 서버가 이 헤더를 보내야 한다(SHOULD)고 규정합니다. 없으면 브라우저는 갈 곳이 없어 302 응답 자체의 본문을 표시하는데, 보통 빈 화면입니다
- 본문 — RFC 9110에 따르면 본문에는 '보통 링크가 담긴 짧은 하이퍼텍스트 메모'가 들어갑니다. 브라우저는 본문을 무시합니다. nginx, Apache, Express는 기본으로 한 줄짜리 메모를 보내고, 대형 사이트 상당수는 빈 본문을 보냅니다
- 캐시 헤더 — 필요 없습니다.
Cache-Control: max-age나Expires를 추가하지 않는 한 브라우저는 302를 캐시하지 않습니다. 반면 301과 308은 기본적으로 캐시됩니다. 다만 CDN은 302를 캐시할 수 있습니다(아래 '사라지지 않는 302' 참고)
HTTP/1.1 302 Found
Location: https://www.example.com/login?next=%2Fdashboard
Content-Length: 0
Date: Mon, 26 Oct 2026 14:00:00 GMTLocation에는 전체 URL이나 /login 같은 상대 경로를 쓸 수 있습니다. RFC 9110은 모든 URI 참조를 허용하며, 브라우저는 요청한 URL을 기준으로 이를 해석합니다. 애플리케이션 코드(예: Express)에서는 상대 경로를 쓰면 프록시 뒤에서 내부 호스트 이름이나 http://가 새어 나가는 일을 막을 수 있습니다. nginx와 Apache는 absolute_redirect off나 RedirectRelative On을 설정하지 않는 한 어차피 절대 URL로 바꿔 보냅니다.
200 대신 302 응답이 오는 이유
페이지가 요청에 없는 무언가를 요구하거나, 사용한 URL이 사이트가 실제로 제공하는 URL과 다르기 때문입니다. 흔한 원인은 다음과 같습니다.
- 로그인하지 않았습니다 — 로그인이 필요한 페이지는 로그인 페이지로 302를 보내고, 원래 보려던 페이지는 돌아올 주소를 담는 파라미터에 넣습니다. 로그아웃 상태에서
github.com/settings에 접속하면/login?return_to=…로 이동합니다. curl이나 API 클라이언트에서는 세션 쿠키나 토큰을 함께 보내세요. - 클라이언트가 리다이렉트를 따라가지 않습니다 — 브라우저,
fetch(), Python의 requests는 자동으로 따라갑니다. curl은-L을 붙이지 않으면 따라가지 않고 302에서 멈춘 채 그 본문만 출력하는데, 대개 아무것도 나오지 않습니다(상태와 Location을 보려면-i를 추가하세요). - 사이트가 제공하는 URL이 아닙니다 — 끝의 슬래시가 빠졌거나 더 붙은 경우, 언어 경로가 붙은 경우, 추적 링크가 다른 곳으로 넘겨주는 경우입니다. Express의
res.redirect(), URL만 지정한 nginx의return, Apache의Redirect는 따로 지정하지 않으면 모두 302를 보냅니다. - 서버 앞단의 무언가가 추가합니다 — 오리진 서버를 직접 테스트하면 200인데 공개 URL에서는 302가 온다면, CDN·로드 밸런서·프록시 규칙이 리다이렉트하고 있는 것입니다.
API는 로그인이 없을 때 보통 리다이렉트 대신 401 Unauthorized로 응답합니다. 모든 홉과 각 상태 코드를 보려면 URL을 리다이렉트 확인에 넣어 보세요.
302 vs 301 vs 307 vs 308: 어떤 리다이렉트를 사용해야 할까?
리다이렉트 코드 다섯 개면 거의 모든 경우를 처리할 수 있습니다. 차이는 두 가지, 이동이 영구적인지와 POST가 POST로 유지되는지입니다.
| 코드 | 영구? | 브라우저에서 POST는 | 기본 캐시? | 적합한 용도 |
|---|---|---|---|---|
| 301 Moved Permanently | 예 | GET으로 바뀜 | 예 | 영구적인 URL 변경, 도메인 이전 |
| 302 Found | 아니요 | GET으로 바뀜 | 아니요 | 로그인, 단기 이동, A/B 테스트 |
| 303 See Other | 아니요 | GET으로 바뀜 | 아니요 | 폼 제출 후 보여 줄 페이지 |
| 307 Temporary Redirect | 아니요 | POST 유지 | 아니요 | 메서드를 유지해야 하는 임시 이동 |
| 308 Permanent Redirect | 예 | POST 유지 | 예 | 메서드를 유지해야 하는 영구 이동 |
303과 307은 302 때문에 생겼습니다. 1997년 HTTP/1.1에 303이 추가되었고, 1999년 RFC 2616은 '기존 사용자 에이전트 구현 대부분이 302를 303 응답처럼 처리한다'고 인정하면서 307을 추가하고 302의 이름을 'Found'로 바꿨습니다(RFC 2616 §10.3.3). 임시 리다이렉트가 POST를 받을 수 있다면, POST를 유지하려면 307을, 다음 페이지를 GET으로 불러오려면 303을 쓰세요.
302 리다이렉트를 사용해야 할 때
리다이렉트가 언젠가 없어질 것으로 예상될 때 302를 쓰세요. 대표적인 경우는 다음과 같습니다.
- 로그인 리다이렉트 — 로그인하지 않은 사용자를
/dashboard에서/login으로 보낸 뒤, 로그인하면 다시 돌려보냄 - A/B 테스트 — 방문자 일부를 변형 페이지로 보냄. Google의 테스트 가이드도 이때는 301이 아닌 302를 쓰라고 안내합니다(Google 검색 센터)
- 수리 중인 페이지 — 페이지를 다시 만드는 며칠 동안 방문자를 안내 페이지로 보냄
- 단기 캠페인 URL —
/black-friday가 세일 기간에만 올해 세일 페이지를 가리킴 - 점검 — 방문자에게만 해당합니다. 계획된 점검 중 검색엔진에는 Retry-After를 붙인 503이 더 나은 응답입니다
이 중 하나라도 영구적인 것이 되면 301로 바꾸세요.
302 상태 코드 보내는 방법
대부분의 서버와 프레임워크는 한 줄로 302를 보낼 수 있습니다. 아래 예시는 모두 302 Found와 Location 헤더를 보내며, 302에 필요한 것은 이것이 전부입니다.
Nginx
nginx에서는 return에 코드를 함께 씁니다. URL만 지정한 return도 302를 보냅니다(nginx 문서):
server {
listen 80;
server_name example.com;
# Temporary redirect (302 Found)
location /old-page {
return 302 https://example.com/new-page;
}
}Apache (.htaccess)
Apache에서는 Redirect(상태를 지정하지 않으면 기본값이 302)나 [R=302,L] 플래그를 붙인 RewriteRule을 사용합니다:
# Simple temporary redirect
Redirect 302 /old-page https://example.com/new-page
# Or with mod_rewrite for pattern matching
RewriteEngine On
RewriteRule ^maintenance$ /maintenance.html [R=302,L]Node.js (Express)
Express의 res.redirect()는 상태 코드를 지정하지 않으면 기본으로 302를 보냅니다:
// Temporary redirect (302 Found by default)
app.get('/dashboard', (req, res) => {
if (!req.user) {
return res.redirect('/login') // sends 302
}
// ...render dashboard
})
// Or be explicit:
res.redirect(302, '/login')302 상태 코드 테스트 방법
가장 빠른 확인 방법은 터미널의 curl입니다. 브라우저 캐시의 방해 없이 상태 코드와 Location 헤더를 보여 줍니다.
# Status and headers, without following the redirect
curl -sI https://example.com/old-page
# -I sends HEAD, which some servers answer differently; this sends a real GET
curl -s -o /dev/null -D - https://example.com/old-page
# Expected:
# HTTP/2 302
# location: https://example.com/new-page
# Follow the whole chain and print each hop's status and Location
curl -sL -o /dev/null -D - https://example.com/old-page | grep -iE '^(HTTP/|location:)'터미널이 없다면 DNS Robot의 리다이렉트 확인를 쓰세요. 저희 서버에서 모든 홉(301, 302, 303, 307, 308, meta refresh)을 따라가며 각 상태 코드와 최종 URL을 보여 줍니다. HTTP 헤더 검사 도구는 각 홉의 원본 헤더를 '리다이렉트' 탭에서 보여 줍니다.
302 리다이렉트와 SEO
Google은 두 종류의 리다이렉트를 서로 다른 지시로 받아들입니다. 임시 리다이렉트(302, 303, 307)는 Googlebot이 따라가지만, Google은 이를 리다이렉트 대상이 표준 URL이어야 한다는 신호로 사용하지 않습니다. 그래서 보통 소스 페이지가 검색결과에 남습니다. 다른 신호가 대상을 가리키면 대상 페이지도 색인될 수 있습니다(Google 검색 센터).
영구 리다이렉트(301, 308)는 반대로 '새 URL을 표시하라'는 뜻입니다. 그러니 진짜 질문은 '302가 PageRank를 전달하는가?'가 아니라 '검색결과에 어떤 URL이 나오기를 원하는가?'입니다. rel=canonical 태그도 그 다른 신호 중 하나입니다. 페이지가 어떤 URL을 선언하는지는 메타 태그 검사기로 확인할 수 있습니다.
- 영구 이동에는 301이나 308 — 새 도메인, 새 URL 구조, 페이지 통합
- 임시 이동에는 302 — 로그인, A/B 테스트, 캠페인
- 영구 이동을 302로 방치하지 않기 — Google이 이전 URL을 계속 표시할 수 있습니다
- 체인은 짧게 — 홉마다 왕복이 한 번씩 늘어나고, Googlebot은 기본적으로 최대 10개의 리다이렉트 홉까지 따라갑니다(Google)
왜 302는 POST 요청을 GET으로 바꾸는가
302에서 가장 의외인 부분입니다. 초기 사양은 클라이언트가 메서드를 바꾸는 것을 한 번도 허용하지 않았지만, 브라우저들은 302를 받으면 POST를 GET으로 바꿔 버렸습니다. 지금은 브라우저가 구현하는 Fetch Standard가 이 관행을 명문화했습니다. 301이나 302를 받으면 POST는 GET이 되고 본문은 버려집니다(Fetch Standard).
그래서 브라우저가 POST /submit-form을 보내고 서버가 302 Found로 응답하면, 브라우저는 이어서 GET /thank-you를 요청하고 폼 데이터는 사라집니다. 브라우저는 PUT과 DELETE는 그대로 유지합니다. 다른 클라이언트는 제각각입니다. Python의 requests는 302를 받으면 HEAD를 제외한 모든 메서드를 GET으로 바꾸고, curl -L은 301, 302, 303에서 POST를 GET으로 바꿉니다(--post302를 쓰면 302에서 POST를 유지합니다).
# What you send:
POST /submit-form HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
name=Alice&email=alice@example.com
# Server responds with 302:
HTTP/1.1 302 Found
Location: /thank-you
# Browser follows with GET (form data dropped!):
GET /thank-you HTTP/1.1
Host: example.com리다이렉트에서 원래 메서드를 유지해야 한다면(POST는 POST로, PUT은 PUT으로) 307 Temporary Redirect를 쓰세요. 일부러 본문을 버리고 GET으로 바꾸려면(전형적인 POST/Redirect/GET 패턴) 303 See Other를 쓰세요. 두 코드는 의미가 분명하지만 302는 그렇지 않습니다.
일반적인 302 문제와 해결
302가 문제를 일으킨다면 대개 다음 중 하나입니다.
- 200 대신 302 — 로그인이 필요한 페이지, 리다이렉트를 따라가지 않는 클라이언트, 또는 URL 변형 때문입니다(위의 '200 대신 302 응답이 오는 이유' 참고)
- Location 헤더 없는 302 — 따라갈 곳이 없어 브라우저는 302 자체의 본문을 표시하며, 보통 빈 화면입니다. 상태를 보내기 전에
Location을 설정하세요 - 자기 자신을 가리키는 302 — 리다이렉트 루프입니다. 규칙이 자신의 대상 URL에서도 다시 실행됩니다. 빠진 조건을 추가하세요
- 데이터가 사라지는 폼 — POST → 302 → GET 과정에서 본문이 버려집니다. 결과 페이지를 의도적으로 보여 주려면 303을, POST를 유지하려면 307을 쓰세요
- 사라지지 않는 302 — 서버가
Cache-Control: max-age나Expires를 함께 보내 브라우저가 캐시했거나, CDN이 캐시한 경우입니다(Cloudflare는 자신이 캐시하는 콘텐츠에서는 헤더에 다른 지시가 없으면 302를 20분 동안 보관합니다. Cloudflare 문서). 해당 헤더를 없애고 CDN 캐시를 비운 뒤 브라우저 캐시를 지우세요 - 프로덕션에서만 나는 302 — 대개 CDN, 로드 밸런서, 프록시 규칙이 리다이렉트를 추가한 것입니다. 오리진을 직접 테스트해 확인하세요
302 리다이렉트 루프: 진단 방법
리다이렉트 루프는 URL A가 B로 보내고, B가 다시 A로 보내는 상태입니다. Fetch Standard가 정한 한도인 리다이렉트 20번이 지나면 Chrome은 ERR_TOO_MANY_REDIRECTS를, Firefox는 '페이지가 제대로 리디렉션되지 않음'을 표시하고 포기합니다.
대표적인 원인은 Cloudflare의 Flexible 암호화 모드입니다. Cloudflare는 일반 HTTP로 서버에 접속하고, 서버는 HTTP를 HTTPS로 리다이렉트하며, Cloudflare는 다시 HTTP로 요청합니다(Cloudflare 문서).
# Trace the full redirect chain (limit to 10 hops to avoid infinite loops)
curl -ILs --max-redirs 10 https://example.com 2>&1 | grep -i 'HTTP/\|location:'
# Example of a loop:
# HTTP/2 302
# location: http://example.com/
# HTTP/1.1 302 Found
# Location: https://example.com/
# HTTP/2 302
# location: http://example.com/ <-- loop confirmedLocation에 같은 URL이 계속 나온다면, 즉 URL이 자기 자신을 가리키거나 두 URL이 번갈아 나온다면 루프입니다. Cloudflare Flexible 모드에서는 https://example.com/ 이 자기 자신으로 리다이렉트합니다. 전체 해결 방법은 ERR_TOO_MANY_REDIRECTS 가이드를 참고하세요. DNS Robot의 리다이렉트 확인는 저희 서버에서 체인을 추적하고 루프가 생기는 지점에서 멈춥니다.
302 상태 코드 모범 사례
몇 가지 습관만 지키면 302 문제 대부분을 피할 수 있습니다.
- Location 헤더를 항상 보내기 — 없으면 아무것도 리다이렉트를 따라갈 수 없습니다
- 캐시 헤더를 붙이지 않기 —
max-age나Expires가 없는 302는 캐시되지 않으며, 그것이 원하는 동작입니다 - 프록시 뒤의 절대 URL에 주의하기 — 애플리케이션 코드에서는 상대 경로
/login을 쓰면 내부 호스트 이름이나http://가 Location에 새어 나가지 않습니다. nginx와 Apache는 자체 설정으로 절대 URL을 만듭니다 - 한 홉으로 끝내기 —
A → B는 괜찮지만,A → B → C는 모든 방문자에게 왕복을 한 번 더 추가합니다 - POST를 302로 다른 페이지에 보내지 않기 — 결과 페이지를 보여 주려면 303을, POST를 유지하려면 307을 쓰세요
- 오래된 임시 리다이렉트 점검하기 — 이유가 사라진 뒤에도 남아 있는 경우가 많습니다. 리다이렉트 확인로 확인하세요
- 이동이 영구적인 것이 되면 바로 301로 바꾸기
브라우저와 도구는 302를 어떻게 처리할까
클라이언트는 모두 302를 따라가지만, 한도와 메서드 처리 방식은 다릅니다.
| 클라이언트 | 302를 받으면 | 리다이렉트 한도 |
|---|---|---|
| Chrome, Edge, Firefox, Safari | 즉시 따라감. POST는 GET이 됨 | 20(Fetch Standard) |
| JavaScript의 fetch() | 기본으로 따라감(redirect: "follow"). POST 규칙은 같음 | 20 |
| curl | 따라가지 않음. 302의 본문만 출력하며 대개 비어 있음(상태와 Location을 보려면 -i나 -I 추가) | 없음(따라가지 않음) |
| curl -L | 따라감. 301, 302, 303에서 POST는 GET이 됨(--post301, --post302, --post303으로 유지) | 50(--max-redirs) |
| wget | 따라감 | 20(--max-redirect) |
| Python requests | 따라감. HEAD를 제외한 모든 메서드가 GET이 됨 | 30 |
| Googlebot | 따라감. 소스 URL을 검색결과로 유지 | 기본 10홉 |
정확한 정의는 RFC 9110의 302 정의와 MDN의 302 문서를 참고하세요.
리다이렉트 체인을 몇 초 만에 추적
DNS Robot의 무료 리다이렉트 확인 도구로 리다이렉트 체인의 모든 홉을 점검하세요. 상태 코드, Location 헤더, 최종 목적지를 중립 서버에서 확인합니다(브라우저 캐시 없음).
출처
- RFC 9110: HTTP Semantics, 302 Found, IETF
- Fetch Standard: HTTP-redirect fetch, WHATWG
- Redirects and Google Search, Google Search Central
- Configure cache by status code (default 20 minutes for 302), Cloudflare



