PHP로 페이지를 구성하다 보면 “어떤 조건에서는 다른 페이지로 보내야 한다”는 상황이 자주 생깁니다.
보통은 아래처럼 간단히 처리하죠.
header('Location: /somewhere', true, 302);
exit;
그런데 실제 운영 환경에서는 이 방식이 종종 실패합니다. 대표적인 케이스가 이미 출력이 시작된 상태입니다.
- include로 여러 파일을 조립하는 구조
- BOM(UTF-8 시그니처) / 공백 / 디버그 echo가 앞에서 한 번이라도 출력됨
- 템플릿 시스템에서 헤더보다 body를 먼저 출력하는 구조
이 경우 PHP는 더 이상 HTTP 헤더를 수정할 수 없어서
Warning: Cannot modify header information - headers already sent
같은 문제가 터지고, 리다이렉트가 되지 않습니다.
그래서 실무에서는 “어떤 상황에서도 안정적으로 이동시키는” redirect() 유틸을 만들어 씁니다.
function redirect($url){
$url = (string)$url;
if(!headers_sent()){
header('Location: '.$url, true, 302);
exit;
}
$safe = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
echo '<!doctype html><html><head><meta charset="utf-8">';
echo '<meta http-equiv="refresh" content="0;url='.$safe.'">';
echo '</head><body>';
echo '<script>location.replace("'.$safe.'");</script>';
echo '</body></html>';
exit;
}
① (string)$url로 캐스팅하는 이유
redirect()는 다양한 값이 들어올 수 있습니다.
- redirect('/login')처럼 문자열
- redirect($row['id'])처럼 숫자
- 조건에 따라 `null`이 들어오는 실수(운영에서 가끔 나옴)
여기서 $url을 문자열로 통일해두면 타입으로 인한 경고를 줄이고, 출력/조합 시 동작을 예측 가능하게 만듭니다.
② headers_sent() 체크를 하는 이유
리다이렉트의 정석은 “HTTP 헤더 Location”입니다.
하지만 헤더는 **아직 아무 것도 출력되지 않았을 때만** 설정 가능합니다.
if(!headers_sent()){
header('Location: '.$url, true, 302);
exit;
}- headers_sent()가 `false`면: 가장 깔끔하고 표준적인 302 이동
- headers_sent()가 `true`면: 이미 출력이 시작되어 header()가 실패할 가능성이 높음
이 조건 분기가 `redirect()`의 존재 이유 그 자체입니다.
③ 왜 fallback이 “meta refresh + JS” 2개나 있나?
헤더 리다이렉트가 막힌 상황에서는 브라우저에서 할 수 있는 방법이 제한적입니다. 그래서 흔히 이중 안전장치를 둡니다.
- 자바스크립트가 꺼진 환경에서도 동작 가능
- 오래된 브라우저/특수 브라우저에서도 비교적 호환성이 높음
- 대부분의 브라우저에서 가장 즉각적으로 이동
- replace()는 “현재 페이지를 히스토리에 남기지 않음”
- 즉 사용자가 “뒤로가기” 했을 때 다시 리다이렉트 페이지로 돌아와 무한루프에 빠지는 문제를 줄임
meta refresh만 쓰면 일부 환경에서 딜레이나 무시되는 경우가 있고, JS만 쓰면 JS 차단 환경에서 실패할 수 있으니 둘 다 두는 편이 안정적입니다.
④ 왜 여기서는 `htmlspecialchars()`를 직접 쓰나?
fallback은 HTML을 직접 출력하는 구간이고, `$url`이 아래처럼 HTML 속성/JS 문자열에 들어갑니다.
<meta ... content="0;url=...">
<script>location.replace("...")</script>
그래서 최소한
- * `"` (큰따옴표)
- * `'` (작은따옴표)
가 깨지지 않도록 `ENT_QUOTES`를 적용합니다.
$safe = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
여기서 “왜 `h()`를 안 쓰고 직접 썼냐”는 질문이 나올 수 있는데, 핵심은 컨텍스트입니다.
- `h()`는 보통 “본문 텍스트 출력용” 전역 헬퍼로 많이 씁니다.
- 하지만 여기서는 **HTML 속성 + JS 문자열**로 들어가므로, “이 함수 안에서만 필요한 로우레벨 escape”를 의도적으로 명시해 둔 겁니다.
⑤ `exit;`를 꼭 하는 이유
리다이렉트는 “이동”이 목적이므로, 이후 로직이 실행되면 문제가 생깁니다.
- DB 업데이트/삭제가 계속 실행될 수 있음
- 템플릿이 더 렌더링되며 화면이 섞임
- 리다이렉트가 늦거나 꼬임
그래서 헤더 이동이든 fallback이든 무조건 exit로 흐름을 끝내는 게 정석입니다.
짧은 함수지만, 운영 환경에서 발생하는 애매한 리다이렉트 실패를 상당히 줄여줍니다.