게시글 상세 페이지 하단에 스크랩, 소스보기, 인쇄, 신고 버튼이 붙어 있었습니다. 오래된 게시판 프로그램에서 기본으로 제공하던 기능들이라 별생각 없이 두고 있었습니다.
문제는 사용자는 거의 누르지 않는 버튼을 검색엔진은 성실하게 따라간다는 점이었습니다. 글 하나마다 여러 개의 부가 URL이 생겼고, 서치콘솔에는 실제 게시글보다 부가 기능 주소가 더 많이 잡히고 있었습니다.
이 글은 버튼 몇 개를 지운 이야기만은 아닙니다. 글 하나에 붙은 작은 링크들이 사이트 전체에서는 얼마나 많은 URL을 만들고, 크롤링 예산과 중복 콘텐츠에 어떤 영향을 주는지 정리한 글입니다.
글 하나에서 만들어진 부가 URL
게시글 하단에는 이런 링크들이 있었습니다.
<a href="/board/scrap.php?num=1226">스크랩</a>
<a href="/board/source_view.php?num=1226">소스보기</a>
<a href="/board/print.php?num=1226">인쇄</a>
<a href="/board/report.php?num=1226">신고</a>글 하나만 보면 네 개의 링크입니다. 하지만 게시글이 3,000개라면 이야기가 달라집니다.
부가 URL 4개 × 게시글 3,000개 = 12,000개 URL실제 콘텐츠는 3,000개인데, 부가 기능 URL이 12,000개나 생기는 구조였습니다. 특히 소스보기와 인쇄 페이지는 원본 글 내용을 거의 그대로 보여줘 중복 콘텐츠처럼 보일 수 있었습니다.
서치콘솔에 잡힌 이상한 주소들
색인 현황을 보다가 이런 URL들이 보였습니다.
/board/scrap.php?num=1226
/board/source_view.php?num=1226
/board/print.php?num=1226
/board/report.php?num=1226처음에는 몇 개만 잡힌 줄 알았습니다. 하지만 패턴을 세어보니 글 번호마다 비슷한 주소가 계속 만들어지고 있었습니다.
문제는 세 가지였습니다.
- 크롤러가 실제 게시글 대신 부가 URL을 방문
- 소스보기와 인쇄 페이지가 원본과 중복됨
- 스크랩과 신고 페이지는 로그인 안내만 보여주는 얇은 페이지가 됨
사용자는 거의 쓰지 않는 기능이었지만, 검색엔진 입장에서는 전부 따라갈 수 있는 링크였습니다.
링크 수를 글 개수와 곱해서 보기
이 문제를 판단할 때는 버튼 하나만 보면 안 됐습니다. 링크 하나가 만들어내는 URL 수에 글 개수를 곱해야 했습니다.
| 기능 | 글 하나당 URL | 글 3,000개 기준 | 처리 |
|---|---|---|---|
| 스크랩 | 1개 | 3,000개 | 삭제 |
| 소스보기 | 1개 | 3,000개 | 삭제 |
| 인쇄 | 1개 | 3,000개 | CSS 인쇄로 대체 |
| 신고 | 1개 | 3,000개 | POST 버튼으로 변경 |
이렇게 보니 작은 버튼 하나가 아니었습니다. 사이트 전체에서 수천 개의 URL을 만드는 장치였습니다.
실제 사용 흔적부터 확인
무조건 삭제하지는 않았습니다. 먼저 실제로 쓰이는 기능인지 확인했습니다.
1. 최근 1년간 사용 기록이 있는가
2. 없앴을 때 사용자가 다른 방법으로 해결할 수 있는가
3. 유지한다면 검색엔진이 따라가지 않게 만들 수 있는가결과는 이렇게 나눴습니다.
| 버튼 | 사용 흔적 | 결정 |
|---|---|---|
| 스크랩 | 거의 없음 | 삭제 |
| 소스보기 | 없음 | 삭제 |
| 인쇄 | 소수 있음 | CSS 인쇄 스타일로 전환 |
| 신고 | 소수 있음 | POST 방식으로 유지 |
기준은 “있으면 좋아 보이는가”가 아니라 실제 사용자 행동이었습니다.
삭제한 기능은 410으로 정리
스크랩과 소스보기는 템플릿에서 링크를 제거했습니다. 그리고 해당 PHP 파일도 그냥 남겨두지 않았습니다. 파일이 남아 있으면 외부 링크나 크롤러가 계속 접근할 수 있기 때문입니다.
삭제한 URL에는 410 응답을 주도록 했습니다.
<?php
header('HTTP/1.1 410 Gone');
include __DIR__ . '/error/410.php';
exit;
?>410은 영구적으로 없어졌다는 의미입니다. 이미 색인된 부가 URL이 많은 상황에서는 404보다 의도가 명확했습니다.
중요한 것은 robots.txt로 먼저 막지 않는 것입니다.
Disallow: /board/source_view.php이렇게 막아버리면 검색엔진이 410 응답을 볼 수 없습니다. 색인에서 빼려면 크롤러가 한 번은 들어와서 삭제 신호를 확인할 수 있어야 합니다.
인쇄 페이지는 CSS로 대체
인쇄 기능은 일부 사용 흔적이 있어서 완전히 없애지는 않았습니다. 대신 별도 인쇄 URL을 만들지 않고 CSS 인쇄 스타일로 대체했습니다.
@media print {
header,
nav,
footer,
.sidebar,
.comment-form,
.btn-group {
display: none;
}
body {
font-size: 11pt;
color: #000;
background: #fff;
}
a[href]::after {
content: " (" attr(href) ")";
font-size: 9pt;
}
}이렇게 하면 별도 /print.php?num=1226 주소가 필요 없습니다. 사용자는 브라우저의 인쇄 기능을 쓰면 되고, 사이트는 인쇄 전용 중복 URL을 만들지 않아도 됩니다.
인쇄물에서는 링크를 클릭할 수 없으므로 a[href]::after로 주소를 같이 출력하게 했습니다.
신고 기능은 GET 링크에서 POST 버튼으로 변경
신고 기능은 남겼습니다. 다만 <a href> 링크로 두면 크롤러가 따라갈 수 있습니다.
<a href="/board/report.php?num=1226">신고</a>신고는 상태를 바꾸는 동작입니다. 원래 GET 링크보다 POST 폼이 맞습니다.
<form action="/board/report" method="post">
<input type="hidden" name="post_no" value="1226">
<button type="submit">신고</button>
</form>서버에서도 GET 요청을 막았습니다.
<?php
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
header('HTTP/1.1 405 Method Not Allowed');
exit;
}
$postNo = filter_input(INPUT_POST, 'post_no', FILTER_VALIDATE_INT);
if (!$postNo) {
header('HTTP/1.1 400 Bad Request');
exit;
}
$stmt = $pdo->prepare('SELECT number FROM posts WHERE number = ? LIMIT 1');
$stmt->execute([$postNo]);
if (!$stmt->fetch()) {
header('HTTP/1.1 404 Not Found');
exit;
}
?>이렇게 바꾸면 크롤러가 단순 링크처럼 따라갈 URL이 줄어듭니다. 동시에 신고처럼 상태를 바꾸는 동작도 더 올바른 방식으로 처리됩니다.
상세 페이지 링크 수 확인
부가 기능 버튼을 정리하면서 상세 페이지의 전체 링크 수도 같이 세어봤습니다. 한 글 페이지에서 나가는 링크가 90개가 넘는 경우도 있었습니다.
구성은 대략 이랬습니다.
- 상단 메뉴 링크
- 서브메뉴 링크
- 이전글, 다음글
- 댓글 작성자 프로필 링크
- 태그 링크
- 하단 기능 버튼
- 관련 글 링크
특히 댓글 작성자 프로필 링크가 많았습니다. 댓글이 30개면 프로필 링크도 30개가 생겼습니다. 대부분 비어 있거나 의미 없는 페이지라면 크롤러에게 중요한 링크라고 보기 어렵습니다.
그래서 비어 있는 프로필 링크나 사용하지 않는 작성자 페이지 링크도 함께 줄였습니다.
크롤링 대상 URL 확인
수정 후에는 서버 응답과 링크 수를 같이 확인했습니다.
curl -I https://example.com/board/source_view.php?num=1226
curl -I https://example.com/board/scrap.php?num=1226
curl -I https://example.com/board/print.php?num=1226삭제한 URL은 410, 유지하지 않는 URL은 404 또는 410으로 명확히 응답해야 합니다.
페이지 안에 남은 링크는 간단한 스크립트로 세어봤습니다.
document.querySelectorAll('a[href]').length그리고 부가 기능 URL이 남아 있는지도 확인했습니다.
[...document.querySelectorAll('a[href]')]
.map(a => a.getAttribute('href'))
.filter(href => /scrap|source_view|print|report/.test(href));브라우저에서 직접 확인하면 템플릿에 남은 링크를 빠르게 찾을 수 있었습니다.
적용 후 달라진 점
정리 후에는 색인 대상이 훨씬 단순해졌습니다.
- 스크랩, 소스보기 같은 부가 URL 제거
- 인쇄 페이지를 CSS 인쇄 스타일로 대체
- 신고 기능을 GET 링크에서 POST 폼으로 변경
- 로그인 안내만 담긴 얇은 페이지 색인 감소
- 원본 글과 중복되던 소스보기·인쇄 페이지 제거
- 상세 페이지의 링크 수 감소
- 새 글 색인 속도 개선 체감
무엇보다 크롤러가 실제 콘텐츠에 더 집중하게 됐습니다. 사용하지 않는 기능을 줄인 것이 단순한 UI 정리가 아니라 색인 품질 관리로 이어졌습니다.
부가 URL 점검 체크리스트
링크 하나가 사이트 전체 URL을 늘린다
오래된 게시판에는 “있으면 좋을 것 같아서” 남긴 기능이 많았습니다. 하지만 사용자는 거의 쓰지 않아도 크롤러는 링크가 보이면 따라갑니다.
제가 얻은 기준은 단순합니다. 링크 하나가 글 개수만큼 URL을 만든다면, 그 기능이 실제로 필요한지 다시 봐야 합니다. 글이 3,000개라면 버튼 하나가 3,000개의 URL을 만들 수 있습니다.
안 쓰는 기능을 지우는 것은 단순한 디자인 정리가 아니었습니다. 크롤링 예산을 실제 콘텐츠에 쓰게 하고, 중복 URL을 줄이고, 검색엔진이 봐야 할 주소를 선명하게 만드는 작업이었습니다.