존재하지 않는 게시판 번호로 접근하면 보기 싫은 에러 화면이 떴습니다. 사용자에게 흉한 화면을 보여주기 싫어서 처음에는 없는 주소를 전부 사이트 메인으로 301 리다이렉트했습니다.
화면만 보면 깔끔해진 것처럼 보였습니다. 하지만 몇 주 뒤 서치콘솔에서 소프트 404 경고가 쌓이기 시작했습니다. 없는 페이지를 메인으로 보내는 처리가 검색엔진에게는 잘못된 신호가 되고 있었습니다.
이 글은 404, 410, 301의 사전적 의미를 설명하는 글이 아닙니다. 실제 오래된 게시판에서 없는 번호, 삭제된 글, 옮겨진 주소를 어떻게 나눠 처리했는지 정리한 글입니다.
없는 주소를 메인으로 보낸 첫 처리
처음에 넣은 코드는 단순했습니다. 메뉴 번호가 DB에 없으면 사이트 메인으로 보내는 방식이었습니다.
<?php
$stmt = $pdo->prepare(
'SELECT number FROM site_menu
WHERE number = ? AND site_id = ? LIMIT 1'
);
$stmt->execute([$menuNo, $siteId]);
if (!$stmt->fetch()) {
header('Location: https://example.com/' . rawurlencode($siteId), true, 301);
exit;
}
?>
사용자 입장에서는 흉한 에러 화면 대신 메인으로 이동하니 나쁘지 않아 보였습니다. 하지만 이 코드는 검색엔진에게 이렇게 말하는 것과 같습니다.
/mysite/999 는 영구히 /mysite 로 옮겨졌습니다.
문제는 이 말이 사실이 아니라는 점입니다. /mysite/999는 메인으로 옮겨진 페이지가 아니라 애초에 존재하지 않는 주소였습니다.
소프트 404가 생긴 이유
없는 페이지를 메인으로 301 이동시키면 검색엔진은 혼란스러워질 수 있습니다. 사용자는 메인 화면을 보지만, 검색엔진 입장에서는 없는 URL이 정상 URL처럼 처리됩니다.
이런 처리가 쌓이면 소프트 404로 잡힐 수 있습니다.
소프트 404는 화면은 정상처럼 보이거나 다른 페이지로 이동하지만, 실제로는 콘텐츠가 없는 상태를 검색엔진이 감지한 경우에 가깝습니다.
문제가 되는 이유는 아래와 같습니다.
- 존재하지 않는 URL을 계속 유효한 주소처럼 다시 확인함
- 없는 번호가 모두 메인으로 연결되어 메인이 이상한 목적지가 됨
- 진짜 이전된 페이지의 301 신호와 섞임
- 크롤링 예산이 없는 주소 확인에 낭비됨
- 서치콘솔에서 소프트 404 보고가 계속 늘어남
특히 게시판처럼 번호만 바꿔 접근할 수 있는 구조는 더 위험했습니다.
/mysite/1
/mysite/2
/mysite/999
/mysite/10000
봇이 숫자를 바꿔가며 접근하면 없는 주소가 끝없이 만들어질 수 있습니다. 이런 주소를 전부 메인으로 보내면 문제가 더 커집니다.
301, 404, 410 기준 분리
결국 모든 없는 주소를 하나로 처리하지 않고 상황을 나눴습니다.
| 상황 | 상태 코드 | 처리 기준 |
|---|---|---|
| 실제로 새 주소로 옮긴 페이지 | 301 | 같은 콘텐츠가 새 주소에 있음 |
| 예전에는 있었지만 삭제된 콘텐츠 | 410 | 다시 살릴 계획이 없음 |
| 애초에 없는 메뉴나 글 번호 | 404 | 대응할 콘텐츠가 없음 |
| 임시로 접근 불가 | 404 또는 403 | 상황에 따라 분리 |
가장 중요한 기준은 이것입니다.
대체할 같은 콘텐츠가 있으면 301
영구 삭제가 확실하면 410
존재한 적 없거나 대응할 콘텐츠가 없으면 404
화면을 예쁘게 보이게 하려고 상태 코드를 바꾸면 안 됐습니다. 화면은 친절하게 만들고, 상태 코드는 사실대로 보내는 방식으로 정리했습니다.
실제로 옮긴 주소는 301
301은 페이지가 영구적으로 다른 주소로 이동했을 때 사용했습니다. 예를 들어 게시판 메뉴가 예전에는 /mysite/856으로 열렸지만, 지금은 /mysite/board/856이 표준 주소인 경우입니다.
<?php
if (strpos($menu['menu_type'], 'board_') === 0) {
$target = 'https://example.com/'
. rawurlencode($siteId)
. '/board/'
. (int)$menuNo;
header('Location: ' . $target, true, 301);
exit;
}
?>
이 경우에는 실제로 같은 게시판이 새 주소에 있습니다. 그러므로 301이 맞습니다.
/mysite/856 → 301 → /mysite/board/856
이런 301은 사용자에게도 도움이 되고, 검색엔진에게도 대표 주소를 명확하게 알려줍니다.
삭제된 글은 410
예전에는 있었지만 지금은 삭제된 글이라면 410을 검토했습니다.
<?php
header('HTTP/1.1 410 Gone');
include __DIR__ . '/error/410.php';
exit;
?>
410은 “이 콘텐츠는 영구적으로 사라졌다”는 신호입니다. 단순히 찾을 수 없다는 404보다 삭제 의도가 분명합니다.
다만 410은 신중하게 써야 합니다. 나중에 다시 살릴 가능성이 있거나 일시적으로 숨긴 글이라면 404가 더 안전할 수 있습니다.
저는 아래 기준으로 나눴습니다.
삭제 기록이 있고 복구 계획 없음 → 410
존재 여부가 불확실하거나 임시 누락 → 404
다른 주소에 같은 글이 있음 → 301
삭제된 글을 무조건 메인으로 보내지 않은 이유는 간단합니다. 삭제된 글은 메인으로 옮겨진 것이 아니기 때문입니다.
애초에 없는 번호는 404
존재한 적 없는 메뉴 번호나 게시판 번호는 404로 처리했습니다.
<?php
if (!$menu) {
http_response_code(404);
include __DIR__ . '/error/404.php';
exit;
}
?>
예를 들어 /mysite/999라는 메뉴가 DB에 없다면, 이 주소는 메인으로 이동할 이유가 없습니다.
/mysite/999 → 404 Not Found
이렇게 보내야 검색엔진도 해당 URL을 없는 주소로 이해합니다. 없는 주소를 301로 메인에 붙이면 사용자에게는 순간적으로 편해 보여도, 검색엔진에는 잘못된 이동 신호가 됩니다.
404 화면은 친절하게 구성
404를 보낸다고 해서 사용자 경험을 포기한 것은 아닙니다. 처음에 메인으로 보냈던 이유도 결국 흉한 에러 화면을 피하고 싶어서였습니다.
그래서 상태 코드는 404로 유지하되, 화면은 사용자가 다음 행동을 할 수 있게 바꿨습니다.
<?php
http_response_code(404);
$pageTitle = '페이지를 찾을 수 없습니다';
include __DIR__ . '/header.php';
?>
<main class="error-page">
<h1>페이지를 찾을 수 없습니다</h1>
<p>주소가 잘못되었거나 삭제된 페이지일 수 있습니다.</p>
<form method="get" action="/search">
<label for="q" class="sr-only">검색어</label>
<input type="text" id="q" name="q" placeholder="검색어를 입력하세요">
<button type="submit">검색</button>
</form>
<ul>
<li><a href="/">사이트 메인</a></li>
<li><a href="/notice">공지사항</a></li>
<li><a href="/board">게시판 목록</a></li>
</ul>
</main>
<?php include __DIR__ . '/footer.php'; ?>
핵심은 화면과 상태 코드를 분리하는 것입니다.
화면: 친절한 안내, 검색창, 주요 링크
상태 코드: 404 Not Found
화면에 “404”라고 써 있어도 서버가 200을 보내면 검색엔진은 정상 페이지로 볼 수 있습니다.
상태 코드 직접 확인
수정 후에는 브라우저 화면만 보지 않고 상태 코드를 확인했습니다.
curl -I https://example.com/mysite/999
curl -I https://example.com/mysite/deleted-post
curl -I https://example.com/mysite/856
기대 결과는 아래와 같았습니다.
/mysite/999 → 404 Not Found
/mysite/deleted-post → 410 Gone
/mysite/856 → 301 → /mysite/board/856
브라우저는 리다이렉트를 자동으로 따라가고, 예쁜 에러 페이지를 보여주기 때문에 상태 코드를 놓치기 쉽습니다. curl -I로 확인하는 습관이 필요했습니다.
소프트 404를 줄이기 위해 확인한 것
서치콘솔에서 소프트 404가 줄어드는지 확인하기 전에 서버 응답부터 점검했습니다.
- 없는 메뉴 번호가 404를 반환하는지
- 삭제된 글이 410을 반환하는지
- 실제 이전된 주소만 301을 반환하는지
- 404 페이지가 200으로 출력되지 않는지
- 메인으로 보내는 일괄 301이 남아 있지 않은지
- robots.txt로 404, 410 확인을 막고 있지 않은지
특히 마지막도 중요했습니다. 검색엔진이 404나 410 응답을 확인해야 색인에서 정리할 수 있습니다. robots.txt로 먼저 막아버리면 삭제 신호를 제대로 전달하지 못할 수 있습니다.
적용 후 달라진 점
상태 코드를 나눠 처리한 뒤 서치콘솔 보고가 조금씩 정리됐습니다.
- 소프트 404 항목 감소
- 없는 번호가 메인으로 몰리던 리다이렉트 제거
- 실제 이전된 URL만 301로 남음
- 삭제된 글은 410으로 더 명확하게 처리
- 404 페이지에서 검색이나 주요 링크로 이동 가능
- 리다이렉트 맵이 훨씬 읽기 쉬워짐
가장 큰 변화는 “무조건 메인으로 보내는” 규칙을 없앤 것입니다. 메인은 모든 오류의 목적지가 아니었습니다.
404, 410, 301 점검 체크리스트
상태 코드는 정직하게, 화면은 친절하게
301은 강한 신호입니다. 이 주소가 영구적으로 다른 주소로 옮겨졌다는 선언이기 때문입니다. 그래서 없는 페이지를 조용히 치우는 용도로 쓰면 안 됐습니다.
제가 처음에 한 실수는 화면 문제를 리다이렉트로 해결하려 한 것입니다. 에러 화면이 보기 싫으면 에러 화면을 고치면 됩니다. 없는 주소를 메인으로 보내면 사용자에게도 검색엔진에게도 정확한 설명이 되지 않습니다.
기준은 단순했습니다. 실제로 옮겨진 페이지는 301, 영구 삭제된 콘텐츠는 410, 존재하지 않는 주소는 404입니다. 상태 코드는 사실대로 보내고, 화면은 검색창과 안내 링크로 친절하게 만드는 것이 가장 안전했습니다.