오래된 PHP 사이트의 쿼리스트링 URL을 짧은 URL 구조로 바꿨습니다. 주소를 바꾸는 것 자체는 .htaccess와 PHP 코드 몇 줄이면 가능했습니다. 하지만 실제로 어려웠던 부분은 새 주소를 만드는 일이 아니라, 기존 주소를 안전하게 새 주소로 보내는 일이었습니다.
처음에는 “주소만 예쁘게 바꾸면 되겠지”라고 생각했습니다. 그런데 막상 작업해보니 리다이렉트 체인, 302 처리, 검색 파라미터 누락, 내부 링크 방치 같은 문제가 계속 나왔습니다. URL 구조 변경은 단순한 디자인 수정이 아니라 사이트 전체 이사에 가까웠습니다.
이 글은 짧은 URL을 만드는 문법만 설명하는 글이 아닙니다. 쿼리스트링 기반 주소를 짧은 URL로 바꾸면서 기존 색인을 잃지 않기 위해 확인했던 301 리다이렉트 원칙을 정리한 글입니다.
이전 주소와 새 주소 기준 정리
먼저 바꾸기 전 주소는 이런 형태였습니다.
/list.php?id=site&table=board_856&category=&delivery=
/detail.php?post=1226&table=board_856&id=site
/search.php?id=site&mode=ok&word=재테크&x=0&y=0새 주소는 아래처럼 정리했습니다.
/site/board/856
/site/board/856/1226
/site/search?q=재테크주소만 보면 훨씬 깔끔합니다. 사용자는 어떤 페이지인지 대략 이해할 수 있고, 공유할 때도 불필요한 파라미터가 줄어듭니다.
하지만 기존 주소가 이미 검색엔진에 색인되어 있거나 외부에 공유되어 있다면, 단순히 새 주소만 만들면 안 됩니다. 예전 주소로 들어온 사용자와 검색엔진을 새 주소로 보내야 합니다.
정규 URL 표부터 먼저 작성
가장 먼저 한 일은 정규 URL 표를 만드는 것이었습니다. 이 표가 없으면 리다이렉트 규칙을 쓰다가 계속 흔들립니다.
| 페이지 종류 | 이전 주소 예 | 정규 주소 |
|---|---|---|
| 미니홈 메인 | page.php?id=site | /site |
| 일반 페이지 | page.php?id=site&menu=1250 | /site/1250 |
| 게시판 목록 | list.php?id=site&table=board_856 | /site/board/856 |
| 게시글 상세 | detail.php?post=1226&table=board_856&id=site | /site/board/856/1226 |
| 검색 결과 | search.php?id=site&word=재테크 | /site/search?q=재테크 |
이 표는 개발용 메모에 가깝지만 실제로 중요했습니다. 목적지가 하나로 정해져 있어야 301도 정확해집니다.
기준이 없으면 같은 게시글이 /site/board/856/1226으로도 가고, /site/856/1226으로도 가는 식으로 다시 중복 URL이 생깁니다.
302가 아니라 301로 보내기
PHP에서 header('Location: ...')만 쓰면 기본적으로 임시 이동으로 처리될 수 있습니다. URL 구조를 영구적으로 바꾸는 상황이라면 301을 명시해야 합니다.
// 임시 이동처럼 처리될 수 있는 방식
header('Location: ' . $newUrl);
exit;아래처럼 상태 코드를 함께 넣었습니다.
// 영구 이동
header('Location: ' . $newUrl, true, 301);
exit;exit도 중요합니다. 헤더를 보낸 뒤 아래 코드가 계속 실행되면 불필요한 DB 조회가 이어질 수 있고, 상황에 따라 본문 일부가 같이 출력될 수도 있습니다.
리다이렉트는 “보내고 끝”이어야 합니다.
예전 목록 URL 처리
게시판 목록 주소는 테이블명에서 게시판 번호를 뽑아 새 주소로 보냈습니다.
<?php
if (isset($_GET['id'], $_GET['table'])
&& preg_match('/^board_(\d+)$/', $_GET['table'], $m)) {
$siteId = $_GET['id'];
$boardNo = (int)$m[1];
$target = sprintf(
'https://example.com/%s/board/%d',
rawurlencode($siteId),
$boardNo
);
header('Location: ' . $target, true, 301);
exit;
}
?>이전 URL에 category, delivery처럼 빈 값이나 필요 없는 값이 붙어 있어도 최종 목적지는 하나로 맞췄습니다.
/list.php?id=site&table=board_856&category=&delivery=
→ /site/board/856목록에서 페이징 값이 있다면 무조건 버리면 안 됩니다. 실제로 쓰는 값은 새 주소에 맞게 살려야 합니다.
/list.php?id=site&table=board_856&start=20
→ /site/board/856?start=20예전 상세 URL 처리
게시글 상세 주소도 같은 방식으로 정리했습니다.
<?php
if (isset($_GET['post'], $_GET['table'], $_GET['id'])
&& preg_match('/^board_(\d+)$/', $_GET['table'], $m)) {
$siteId = $_GET['id'];
$boardNo = (int)$m[1];
$postId = (int)$_GET['post'];
$target = sprintf(
'https://example.com/%s/board/%d/%d',
rawurlencode($siteId),
$boardNo,
$postId
);
header('Location: ' . $target, true, 301);
exit;
}
?>이렇게 하면 예전 상세 주소로 들어와도 새 상세 주소로 바로 이동합니다.
/detail.php?post=1226&table=board_856&id=site
→ /site/board/856/1226기존 주소를 404로 버리지 않은 이유는 외부 링크가 남아 있을 수 있기 때문입니다. 글이 실제로 존재한다면 새 주소로 합치는 편이 사용자와 검색엔진 모두에게 안전했습니다.
검색 URL은 검색어 보존
검색 URL을 바꿀 때는 더 조심해야 했습니다. 불필요한 파라미터를 지우다가 검색어까지 날리면 검색 기능이 망가집니다.
/search.php?id=site&mode=ok&word=재테크&x=0&y=0여기서 x, y는 이미지 버튼 때문에 붙은 좌표값이라 제거해도 됩니다. 하지만 word는 검색어이므로 반드시 새 주소의 q로 옮겨야 합니다.
<?php
if (isset($_GET['id'], $_GET['word'])) {
$siteId = $_GET['id'];
$keyword = trim((string)$_GET['word']);
$target = sprintf(
'https://example.com/%s/search?q=%s',
rawurlencode($siteId),
rawurlencode($keyword)
);
header('Location: ' . $target, true, 301);
exit;
}
?>정리 후 흐름은 이렇게 만들었습니다.
/search.php?id=site&mode=ok&word=재테크&x=0&y=0
→ /site/search?q=재테크검색어, 페이징, 필터처럼 사용자 기능에 필요한 값은 “쓰레기 파라미터”로 보면 안 됩니다. 짧은 URL 작업을 할 때 가장 쉽게 놓치는 부분이었습니다.
리다이렉트 체인 줄이기
규칙을 하나씩 추가하다 보면 이런 흐름이 생길 수 있습니다.
A → B → C → D동작은 합니다. 하지만 검색엔진과 사용자 모두에게 좋지 않습니다. 중간 이동이 많으면 속도가 느려지고, 크롤러가 중간에서 추적을 멈출 수도 있습니다.
그래서 예전 주소는 가능하면 최종 주소로 바로 보내도록 했습니다.
/list.php?id=site&table=board_856
→ /site/board/856
/detail.php?post=1226&table=board_856&id=site
→ /site/board/856/1226확인은 curl로 했습니다.
curl -sIL "https://example.com/detail.php?post=1226&table=board_856&id=site" | grep -i "^HTTP\|^location"결과에서 301이 여러 번 나오면 중간 단계를 줄여야 합니다.
HTTP/2 301
location: https://example.com/site/board/856/1226
HTTP/2 200이 정도가 원하는 흐름입니다.
리다이렉트 루프 방지
가장 무서운 사고는 무한 리다이렉트였습니다. 목적지가 현재 주소와 같은데도 계속 301을 보내면 브라우저에서 “리다이렉션이 너무 많음” 오류가 납니다.
그래서 공통 리다이렉트 함수에 루프 방지 조건을 넣었습니다.
<?php
function redirect301(string $target): void
{
$current = (($_SERVER['HTTPS'] ?? '') === 'on' ? 'https://' : 'http://')
. $_SERVER['HTTP_HOST']
. $_SERVER['REQUEST_URI'];
if (rtrim($current, '/') === rtrim($target, '/')) {
return;
}
header('Location: ' . $target, true, 301);
exit;
}
?>이 함수는 목적지가 현재 주소와 같으면 리다이렉트를 실행하지 않습니다. 특히 끝 슬래시 제거, www 정리, http → https 이동이 같이 있을 때 필요했습니다.
내부 링크도 새 주소로 변경
301을 깔아두었다고 끝이 아니었습니다. 사이트 내부 링크가 계속 예전 주소를 가리키면 사용자가 클릭할 때마다 301을 한 번 더 타게 됩니다.
그래서 링크 생성 함수를 따로 만들었습니다.
<?php
function boardUrl(string $siteId, int $boardNo): string
{
return sprintf('/%s/board/%d', rawurlencode($siteId), $boardNo);
}
function postUrl(string $siteId, int $boardNo, int $postId): string
{
return sprintf('/%s/board/%d/%d', rawurlencode($siteId), $boardNo, $postId);
}
function searchUrl(string $siteId, string $keyword): string
{
return sprintf('/%s/search?q=%s', rawurlencode($siteId), rawurlencode($keyword));
}
?>그리고 파일 전체에서 예전 주소 패턴을 검색했습니다.
grep -rn "list.php\|detail.php\|search.php" ./ --include="*.php" --include="*.html" --include="*.tpl"리다이렉트는 외부에서 들어오는 예전 주소를 살리는 안전망입니다. 내부 링크까지 계속 예전 주소를 쓰는 것은 좋은 상태가 아니었습니다.
사이트맵과 canonical도 함께 수정
새 주소를 만들었으면 사이트맵도 새 주소만 담아 다시 만들어야 합니다.
<url>
<loc>https://example.com/site/board/856</loc>
</url>
<url>
<loc>https://example.com/site/board/856/1226</loc>
</url>canonical도 새 주소를 가리키도록 맞췄습니다.
<?php
$canonical = 'https://example.com' . postUrl($siteId, $boardNo, $postId);
echo '<link rel="canonical" href="' . htmlspecialchars($canonical, ENT_QUOTES, 'UTF-8') . '">' . "\n";
?>리다이렉트, 내부 링크, 사이트맵, canonical이 서로 다른 주소를 가리키면 검색엔진은 다시 헷갈립니다. 네 가지가 모두 같은 정규 URL을 바라보게 맞추는 것이 중요했습니다.
배포 후 확인한 항목
- 예전 목록 URL이 새 목록 URL로 301 이동하는지
- 예전 상세 URL이 새 상세 URL로 301 이동하는지
- 검색어가 새 검색 URL의
q값으로 보존되는지 - 리다이렉트 체인이 1회로 끝나는지
- 새 주소가
200 OK로 열리는지 - canonical이 자기 자신의 새 주소를 가리키는지
- 사이트맵에 예전 주소가 남아 있지 않은지
- 내부 링크에
list.php,detail.php가 남아 있지 않은지
확인용으로는 이런 명령을 자주 썼습니다.
curl -sIL "https://example.com/list.php?id=site&table=board_856" | grep -i "^HTTP\|^location"
curl -sIL "https://example.com/detail.php?post=1226&table=board_856&id=site" | grep -i "^HTTP\|^location"
curl -sIL "https://example.com/search.php?id=site&word=재테크&x=0&y=0" | grep -i "^HTTP\|^location"서치콘솔에서는 새 주소 몇 개를 직접 URL 검사로 확인하고, 사이트맵을 다시 제출했습니다. 기존 색인이 새 주소로 완전히 넘어가는 데는 시간이 걸렸습니다.
301 리다이렉트 점검 체크리스트
새 주소보다 이전 주소 처리
URL 구조 변경은 새 주소를 만드는 작업보다 이전 주소를 정리하는 작업이 더 중요했습니다. 새 주소가 아무리 깔끔해도, 예전 주소가 404가 되거나 여러 단계로 이동하거나 검색어를 잃어버리면 실제 운영에서는 문제가 됩니다.
제가 얻은 기준은 단순합니다. 정규 URL 표를 먼저 만들고, 예전 주소는 최종 주소로 바로 301 이동시킵니다. 검색어와 페이징처럼 필요한 값은 보존하고, 내부 링크와 사이트맵도 새 주소로 함께 바꿉니다.
주소 변경은 사이트를 이사하는 작업입니다. 간판만 새로 다는 것이 아니라, 예전 길로 찾아오는 사람도 새 입구로 안내해야 합니다. 301 리다이렉트는 그 안내판이고, 제대로 만들려면 새 주소보다 이전 주소 목록을 더 꼼꼼하게 봐야 했습니다.