이 오류가 뜻하는 것
리다이렉트 루프는 페이지가 어디에 있는지를 두고 둘 이상의 규칙이 서로 다른 답을 내는 상황입니다. 규칙 A는 브라우저를 URL B로 보내고, B의 무언가는 다시 A로, 혹은 A로 돌아가는 URL로 보냅니다. 브라우저는 한도(Chrome과 Firefox 모두 약 20번)에 닿을 때까지 따라가다 포기합니다.
Chrome은 ERR_TOO_MANY_REDIRECTS(“이 페이지가 작동하지 않습니다 … 리디렉션한 횟수가 너무 많습니다”)로, Firefox는 “The page isn’t redirecting properly”(영문판 기준)로, Safari는 리다이렉트가 너무 많다는 메시지로 보여 줍니다.
규칙들은 거의 항상 서로를 모르는 다른 층에 있습니다. CDN, 로드밸런서, 웹 서버 설정, .htaccess, 애플리케이션, CMS 플러그인입니다. 각각은 따로 보면 멀쩡합니다. 합쳐 놓으면 빙빙 돕니다.
사이트 방문자라면
순서대로 두 가지를 해 보세요.
- 시크릿(InPrivate) 창에서 그 페이지를 엽니다. 거기서 열린다면 루프는 내 브라우저에 저장된 무언가에 의존하고 있습니다.
- 그 사이트의 쿠키만 지웁니다(Chrome: 주소창 왼쪽 아이콘 → 쿠키 및 사이트 데이터). 그리고 새로고침합니다.
시크릿 창에서도 루프가 난다면 모두에게 고장 난 것이고, 운영자만 고칠 수 있습니다. 브라우저 기록을 통째로 지워도 달라지지 않습니다.
체인을 직접 추적하기
설정 파일부터 들여다보지 마세요. 실제 hop 순서부터 확보합니다.
curl -sIL --max-redirs 10 http://example.com/ | grep -iE '^(HTTP|location)'
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
HTTP/2 301
location: http://example.com/
HTTP/1.1 301 Moved Permanently
Location: https://example.com/
...
curl: (47) Maximum (10) redirects followed
-I는 HEAD 요청을 보냅니다. HEAD와 GET에 다르게 리다이렉트하는 앱도 가끔 있으니, 결과가 이상하면 GET으로 보내고 헤더만 출력해 보세요.
curl -sL --max-redirs 10 -o /dev/null -D - http://example.com/ | grep -iE '^(HTTP|location)'
누군가 입력할 만한 모든 형태로 돌려 보세요. http://와 https://, www가 있는 것과 없는 것입니다. 루프는 진입점 하나에서만 나는 경우가 많습니다.
HTTP 점검은 바깥에서 최대 10 hop까지 따라가며 hop마다 상태 코드와 Location을 보여 주고, 체인이 끝나지 않으면 루프로 표시합니다. curl처럼 쿠키를 들고 가지 않으므로 서버 쪽 루프는 잡지만, 로그인 상태에 따라 생기는 루프는 잡지 못합니다.
브라우저에서는 개발자 도구 → Network에서 “Preserve log”를 켜면 curl이 볼 수 없는 hop까지 모두 보입니다.
반복 패턴 읽기
Location 값을 나란히 놓고 보세요. 반복되는 쌍이 루프의 종류를 알려 줍니다.
| 반복되는 것 | 루프 종류 | 볼 곳 |
|---|---|---|
같은 호스트에서 http:// ↔ https:// | 어딘가에서 HTTPS 판별이 틀림 | 원인 1, 2 |
example.com ↔ www.example.com | 두 층이 대표 호스트를 다르게 봄 | 원인 3 |
| 같은 URL이 자기 자신으로 리다이렉트 | 절대 충족되지 않는 조건, 대개 프록시 헤더 | 원인 2 |
/path ↔ /path/ | 끝 슬래시 규칙끼리 충돌 | 원인 5 |
/login → /account → /login | 세션이나 쿠키가 유지되지 않음 | 원인 6 |
/ → /ko/ → / | 언어·지역 리다이렉트의 판단이 고정되지 않음 | 원인 6 |
흔한 원인
1. Cloudflare Flexible SSL과 원 서버의 HTTPS 리다이렉트
Cloudflare 프록시를 쓰는 사이트에서 가장 흔한 루프이고, 이유도 단순합니다. Flexible 모드에서 방문자는 Cloudflare와 HTTPS로 통신하지만, Cloudflare는 내 서버와 평문 HTTP로 통신합니다. HTTP를 HTTPS로 리다이렉트하도록 설정된 내 서버는 301 Location: https://example.com/으로 답합니다. 브라우저는 HTTPS로 다시 Cloudflare에 오고, Cloudflare는 또 HTTP로 내 서버를 가져오고, 이게 계속 반복됩니다.
알아보는 법: 체인에서 같은 https:// URL이 자기 자신으로 리다이렉트되고, 모든 hop의 응답 헤더에 server: cloudflare가 있습니다.
해결: SSL/TLS 모드를 Full (strict)로 바꾸고 원 서버에 인증서를 두세요. 인증서가 없다면 무료 Cloudflare Origin CA 인증서로 충분합니다. 원 서버의 리다이렉트를 없애도 루프는 멈추지만, Cloudflare와 원 서버 사이 구간이 암호화되지 않은 채로 남습니다. 호스트명 하나에만 Flexible을 거는 Configuration Rule이나 예전 Page Rule이 없는지도 확인하세요. 모드별 차이는 Cloudflare SSL 모드에 있고, Full로 바꿨더니 525가 난다면 Cloudflare 525를 보세요.
2. TLS가 프록시에서 끝나는데 앱이 그걸 모름
Cloudflare가 없어도 같은 구조로 생깁니다. 로드밸런서(AWS ALB, Kubernetes ingress, HAProxy, 앞단 nginx)가 HTTPS를 종료하고 백엔드에는 평문 HTTP로 넘깁니다. 백엔드는 “이 요청이 HTTPS인가?”를 확인하고, 아니라고 보고, HTTPS로 리다이렉트합니다. 끝없이.
프록시는 원래 스킴을 X-Forwarded-Proto: https 헤더로 백엔드에 알려 줍니다. 백엔드가 그걸 읽지 않을 뿐입니다. 흔한 위치는 이렇습니다.
-
Apache
.htaccess: TLS를 종료하는 프록시 뒤에서는RewriteCond %{HTTPS} off가 항상 참입니다. 헤더를 보도록 바꾸세요.RewriteCond %{HTTP:X-Forwarded-Proto} !https RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301] -
프록시 뒤의 nginx: 백엔드에서
$scheme은http입니다. 헤더를 기준으로 리다이렉트하거나, 더 좋게는 HTTPS 리다이렉트를 엣지에서만 하세요.if ($http_x_forwarded_proto = "http") { return 301 https://$host$request_uri; } -
프레임워크: 프록시를 신뢰하도록 설정합니다. Express는
app.set('trust proxy', 1)을 해야req.secure가 헤더를 반영합니다. Django는SECURE_SSL_REDIRECT와 함께SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https'). Laravel은 신뢰할 프록시를 설정하고, Spring Boot는server.forward-headers-strategy를 씁니다.
이 헤더는 내 프록시에서 온 것만 믿어야 합니다. 클라이언트가 백엔드에 직접 닿을 수 있다면 X-Forwarded-Proto를 마음대로 보낼 수 있습니다. 프레임워크들이 이 설정을 기본으로 꺼 두는 이유입니다.
평범한 nginx에서 비슷한 실수도 있습니다. 80과 443을 함께 listen하는 server 블록 안에 조건 없는 return 301 https://…를 두면, 모든 HTTPS 요청이 자기 자신으로 리다이렉트됩니다. 리다이렉트는 80만 listen하는 server 블록에 두세요.
3. www와 루트 도메인이 서로 다름
한 층은 www.example.com을 example.com으로 보내고, 다른 층은 다시 돌려보냅니다. Cloudflare 리다이렉트 규칙은 루트 도메인을, 원 서버 nginx 설정은 www를 대표로 삼는 식입니다. 도메인 등록업체의 URL 포워딩과 CMS가 서로 다른 말을 하는 경우도 있습니다.
해결: 대표 호스트를 정하고, 그쪽으로 보내는 리다이렉트는 정확히 한 층만 맡게 하고, 다른 층이 반대로 보내지 않는지 확인하세요. 찾아볼 곳은 정해져 있습니다. CDN 리다이렉트 규칙, nginx -T | grep -n 'return 30', .htaccess, CMS의 사이트 주소 설정, SEO·리다이렉트 플러그인입니다.
4. 워드프레스 siteurl과 home
워드프레스는 모든 요청을 home과 siteurl 옵션에 저장된 호스트와 스킴으로 리다이렉트합니다(redirect_canonical). 이 값이 https://www.example.com인데 서버가 example.com을 강제하거나, 값은 https://인데 워드프레스가 요청이 HTTP로 왔다고 판단하면(프록시나 Cloudflare Flexible 뒤에서) 루프가 생깁니다. wp-admin도 같이 루프에 빠지므로 관리자 화면에서 고치기 어려운 경우가 많습니다.
셸에서 확인하고 고치세요.
wp option get home
wp option get siteurl
wp option update home 'https://example.com'
wp option update siteurl 'https://example.com'
WP-CLI가 없다면 DB를 조회합니다(테이블 접두사가 wp_가 아닐 수 있습니다).
SELECT option_name, option_value FROM wp_options WHERE option_name IN ('home','siteurl');
wp-config.php의 WP_HOME, WP_SITEURL 상수는 DB 값보다 우선하므로 거기도 보세요. TLS가 프록시에서 끝난다면 워드프레스에 요청이 HTTPS였다고 알려 줘야 합니다. wp-config.php의 “stop editing” 줄 위에 다음을 넣으세요.
if (isset($_SERVER['HTTP_X_FORWARDED_PROTO']) && $_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https') {
$_SERVER['HTTPS'] = 'on';
}
SSL·리다이렉트 플러그인을 설치한 뒤부터 루프가 생겼다면, wp-content/plugins 아래 그 플러그인 폴더 이름을 바꿔 비활성화하고 다시 시험해 보세요.
5. 끝 슬래시
URL이 실제 디렉터리에 해당하면 nginx와 Apache(mod_dir)는 /docs를 /docs/로 리다이렉트합니다. 애플리케이션이나 rewrite 규칙이 끝 슬래시를 떼면 둘이 번갈아 가며 보냅니다. 자체 끝 슬래시 설정이 있는 프레임워크나 정적 사이트 생성기도 같은 방식으로 웹 서버와 싸울 수 있습니다.
해결: 한 가지 형태로 정하고, 애플리케이션을 그에 맞추고, 웹 서버가 그 위에 슬래시를 덧붙이지 않는지 확인하세요. 정적 사이트라면 생성기의 출력 구조(docs/index.html이냐 docs.html이냐)가 서버 동작을 결정하므로, 그 설정을 리다이렉트 규칙과 맞추세요.
6. 쿠키, 세션, 언어 리다이렉트
이 루프는 상태에 의존하기 때문에 어떤 사용자에게만 나고 다른 사용자에게는 안 납니다.
- 로그인이 유지되지 않음. 로그인 처리기가 세션 쿠키를 설정하고
/account로 보내는데,/account는 쿠키를 보지 못하고/login으로 돌려보냅니다. 원인은 HTTP로 서비스되는 페이지에서Secure쿠키를 설정함(브라우저가 저장하지 않음), 호스트와 맞지 않는Domain속성(www.example.com용으로 설정하고example.com에서 사용),Secure없는SameSite=None(Chromium 계열 브라우저가 거부), 브라우저의 쿠키 하나당 약 4 KB 한도를 넘어 조용히 버려진 쿠키 등입니다. - 언어나 동의 쿠키.
/는Accept-Language를 보고/ko/로 보내고,/ko/는 언어 쿠키가 영어라며/로 보냅니다. 쿠키에 따라 리다이렉트한다면 쿠키가 없을 때의 답이 고정돼 있어야 하고, 쿠키가 말하는 것을 다른 규칙이 뒤집으면 안 됩니다.
쿠키까지 포함해 재현하려면 curl에 쿠키 저장 파일을 줘서 브라우저처럼 동작하게 하세요.
: > /tmp/jar
curl -sIL --max-redirs 10 -b /tmp/jar -c /tmp/jar https://example.com/account \
| grep -iE '^(HTTP|location|set-cookie)'
Set-Cookie 줄을 보세요. 속성(Domain, Path, Secure, SameSite)이 쿠키가 왜 돌아오지 않는지 대개 알려 줍니다.
7. 브라우저의 기억: HSTS와 캐시된 301
curl로는 재현되지 않는 루프를 만드는 브라우저의 기억이 두 가지 있습니다.
HSTS. 사이트가 한 번 Strict-Transport-Security를 보내면, 브라우저는 그 사이트로 가는 모든 http:// 요청을 밖으로 내보내기 전에 https://로 바꿉니다. 개발자 도구에는 307 Internal Redirect로 보입니다. 그런데 서버가 어떤 HTTPS URL을 HTTP로 리다이렉트하면 브라우저가 곧바로 다시 업그레이드하면서 루프가 됩니다. curl은 기본적으로 HSTS 상태를 기억하지 않으므로 https → http 리다이렉트 한 번만 보이고 루프는 없습니다. 버그는 서버가 HTTP로 보내는 리다이렉트입니다. HSTS가 무엇을 약속하게 만드는지는 HSTS preload에 정리돼 있습니다.
캐시된 301. 브라우저는 영구 리다이렉트를 오래 캐시할 수 있습니다. 서버를 고친 뒤에도 잘못된 301을 캐시한 브라우저는 캐시를 지울 때까지 루프를 돕니다. 수정 사항은 시크릿 창에서 시험하고, 아직 실험 중이라면 302를 쓰세요. CDN에 전체 캐시 규칙이 있다면 리다이렉트 응답 자체가 캐시됐을 수 있으니 퍼지하세요. 두 코드의 캐시 차이는 301과 302의 차이에서 설명합니다.
고친 다음에
- 네 가지 진입점(
http/https,www유무)으로 curl 추적을 다시 돌리세요. 모두 한두 hop 안에 같은 대표 URL의200으로 끝나야 합니다. - 시크릿 창에서, 그리고 계정이 있는 사이트라면 로그인한 상태로도 시험하세요.
- HTTP 점검으로 바깥에서 본 체인을 확인하고, 체인에 나오는 모든 호스트명에 SSL 점검을 돌리세요. 브라우저는 TLS를 끝내야 리다이렉트를 볼 수 있으므로 호스트명마다 유효한 인증서가 필요합니다.
- HTTPS를 담당하는 층을 바꿨다면 페이지에
http://자원 링크가 하드코딩돼 있지 않은지 확인하세요. 혼합 콘텐츠를 참고하세요.
내 도메인에서 바로 확인
무료, 가입 불필요. 이 가이드가 다루는 항목을 바로 검사하고 조치 방법을 알려드립니다.