검색 폼 URL을 짧게 바꾸는 방법 | JS 가로채기와 서버 301 처리

게시판 검색 폼을 바꾸고 싶었지만 폼을 만드는 코드는 쉽게 손댈 수 없었습니다. 오래된 게시판 엔진 안쪽에서 hidden 값과 검색어 이름을 만들고 있었고, 입력 이름 하나만 바꿔도 검색 로직이나 페이지네이션이 같이 깨질 수 있는 구조였습니다.

그렇다고 기존 검색 URL을 그대로 두기에는 너무 길고 복잡했습니다.

/list.php?tb=board_856&search_mode=ok&id=mysite&board_keyword=점프&x=0&y=0

목표는 아래처럼 바꾸는 것이었습니다.

/mysite/board/856/search?keyword=점프

이 글은 자바스크립트로 폼을 가로채는 방법만 설명하는 글이 아닙니다. 수정하기 어려운 레거시 검색 폼을 그대로 두면서, JS가 켜진 환경에서는 예쁜 URL로 바로 보내고, JS가 꺼져도 서버 301로 같은 주소에 도착하게 만든 과정을 정리한 글입니다.

검색 폼 URL을 짧게 바꾸는 JavaScript와 서버 301 처리 코드

게시판 검색만 긴 URL로 남은 상황

미니사이트 전체 검색 URL은 이미 짧게 정리해둔 상태였습니다.

/mysite/search?q=키워드

그런데 게시판 검색만 예전 주소 그대로 남아 있었습니다.

/list.php?tb=board_856&search_mode=ok&id=mysite&board_keyword=점프&x=0&y=0

같은 사이트 안에서 검색 URL 형식이 두 가지로 나뉘니 관리가 애매했습니다. canonical 규칙을 세우기도 어렵고, 공유 주소도 지저분했습니다.

특히 x=0&y=0 같은 값은 이미지 버튼 때문에 붙는 좌표 파라미터였습니다. 같은 검색어인데 좌표만 다른 URL이 계속 생길 수 있었습니다.

서버 301을 먼저 안전망으로 구성

가장 먼저 한 일은 서버 쪽 301 처리였습니다. 자바스크립트를 쓰기 전에, 옛 검색 URL로 들어와도 새 주소로 이동하도록 안전망을 깔았습니다.

<?php
if (isset($_GET['search_mode'], $_GET['board_keyword'], $_GET['id'])
    && $_GET['search_mode'] === 'ok') {

    $id = preg_replace('/[^_A-Za-z0-9-]/', '', $_GET['id']);
    $boardNo = null;

    if (isset($_GET['menu'])) {
        $boardNo = preg_replace('/\D/', '', $_GET['menu']);
    } elseif (isset($_GET['tb'])) {
        $parts = explode('_', $_GET['tb']);
        $boardNo = preg_replace('/\D/', '', end($parts));
    }

    if ($boardNo
        && !preg_match('#^/[^/]+/board/[0-9]+/search#', $_SERVER['REQUEST_URI'])) {

        $url = 'https://example.com/'
             . rawurlencode($id)
             . '/board/'
             . (int)$boardNo
             . '/search?keyword='
             . rawurlencode($_GET['board_keyword']);

        header('Location: ' . $url, true, 301);
        exit;
    }
}
?>

tb 값이 board_856처럼 들어오기 때문에 언더스코어 뒤의 숫자를 게시판 번호로 사용했습니다.

board_856 → 856

이 서버 처리만 있어도 기능은 동작합니다.

/list.php?tb=board_856&search_mode=ok&id=mysite&board_keyword=점프
→ 301
→ /mysite/board/856/search?keyword=점프

다만 사용자가 검색할 때마다 요청이 두 번 나갑니다. 긴 URL로 한 번, 301을 받은 뒤 새 URL로 한 번입니다.

자바스크립트로 전송 직전 가로채기

요청을 한 번 줄이기 위해 브라우저에서 폼 전송 직전에 가로챘습니다. 폼 HTML 자체는 바꾸기 어렵지만, 제출 순간에 목적지만 바꾸는 것은 가능했습니다.

document.addEventListener('DOMContentLoaded', function () {
  document.querySelectorAll('form').forEach(function (form) {
    var input = form.querySelector('input[name="board_keyword"]');
    if (!input) return;

    form.addEventListener('submit', function (e) {
      var tb = form.querySelector('input[name="tb"]');
      var id = form.querySelector('input[name="id"]');

      if (!tb || !id) return;

      var boardNo = tb.value.split('_').pop();
      if (!/^[0-9]+$/.test(boardNo)) return;

      e.preventDefault();

      location.href = '/' + encodeURIComponent(id.value)
                    + '/board/' + boardNo
                    + '/search?keyword=' + encodeURIComponent(input.value);
    });
  });
});

핵심은 board_keyword 입력이 있는 폼만 대상으로 삼는 것입니다. 페이지에 로그인 폼, 댓글 폼, 검색 폼이 같이 있어도 게시판 검색 폼만 골라낼 수 있습니다.

또 필요한 값이 없으면 억지로 막지 않고 원래 동작에 맡겼습니다.

if (!tb || !id) return;

이렇게 해야 예상하지 못한 폼 구조에서도 검색 자체가 죽지 않습니다.

JS만 믿지 않은 이유

처음에는 자바스크립트 가로채기만 넣고 끝내고 싶었습니다. 하지만 그렇게 하면 안전하지 않았습니다.

자바스크립트는 언제든 실행되지 않을 수 있습니다.

  • 다른 스크립트 오류 때문에 실행이 중단될 수 있음
  • 광고 차단기나 보안 프로그램이 일부 스크립트를 막을 수 있음
  • 사용자가 JS를 끈 환경일 수 있음
  • 오래된 브라우저에서 일부 문법이 맞지 않을 수 있음
  • 이미 외부에 퍼진 옛 URL은 JS와 관계없이 직접 들어올 수 있음

그래서 구조를 이렇게 잡았습니다.

서버 301: 느리지만 항상 동작하는 안전망
자바스크립트: 가능하면 요청을 줄이는 빠른 경로

JS가 켜져 있으면 바로 예쁜 URL로 이동합니다. JS가 꺼져 있으면 기존 폼이 그대로 제출되고, 서버가 301로 새 주소에 보내줍니다.

최종 주소는 어느 경우에도 하나입니다.

새 검색 경로 리라이트 추가

새 주소가 실제 게시판 검색 페이지로 연결되도록 리라이트 규칙도 필요했습니다.

RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^([A-Za-z0-9_-]+)/board/([0-9]+)/search$ list.php?id=$1&menu=$2&mode=search [L,QSA]

여기서도 QSA가 중요합니다. 이 플래그가 있어야 아래의 keyword 값이 내부로 전달됩니다.

/mysite/board/856/search?keyword=점프

QSA가 없으면 검색어 없이 검색 페이지가 열릴 수 있습니다.

내부 검색 변수로 옮겨 담기

게시판 내부 검색 로직이 board_keyword를 읽는 구조라면 새 URL의 keyword를 내부 변수명으로 옮겨 담아야 합니다.

<?php
if (isset($_GET['keyword']) && !isset($_GET['board_keyword'])) {
    $_GET['board_keyword'] = $_GET['keyword'];
}
?>

이 한 줄짜리 어댑터 덕분에 내부 검색 함수는 계속 기존 변수명을 사용할 수 있습니다. 외부 URL은 새 이름을 쓰고, 내부 로직은 레거시 이름을 유지하는 방식입니다.

레거시 코드에서는 이런 변환층을 하나 두는 것이 전체 검색 로직을 뜯는 것보다 안전했습니다.

검색 결과 페이징 값 유지

검색 결과 2페이지로 넘어갈 때도 검색어가 유지되어야 합니다. 이 부분을 놓치면 1페이지는 잘 나오는데 다음 페이지에서 검색어가 사라집니다.

<?php
$url = 'https://example.com/'
     . rawurlencode($siteId)
     . '/board/'
     . (int)$boardNo
     . '/search?keyword='
     . rawurlencode($keyword);

if (isset($_GET['start']) && $_GET['start'] !== '') {
    $url .= '&start=' . rawurlencode($_GET['start']);
}
?>

출력되는 링크는 이런 형태가 됩니다.

<a href="/mysite/board/856/search?keyword=%EC%A0%90%ED%94%84&amp;start=20">다음 페이지</a>

검색 URL을 바꿀 때는 첫 검색만 보면 안 됩니다. 검색 결과의 다음 페이지, 이전 페이지, 정렬, 필터까지 같이 확인해야 합니다.

검색 결과 canonical과 noindex 기준

검색 결과 페이지를 색인시킬지도 같이 정해야 했습니다. 검색 결과는 사이트마다 판단이 다릅니다.

저는 검색어별로 실제 다른 결과가 나오고, 사용자에게 의미 있는 결과가 있는 경우에는 canonical을 자기 자신으로 두었습니다.

<?php
$canonical = 'https://example.com/'
           . rawurlencode($siteId)
           . '/board/'
           . (int)$boardNo
           . '/search?keyword='
           . rawurlencode($keyword);

echo '<link rel="canonical" href="' . htmlspecialchars($canonical, ENT_QUOTES, 'UTF-8') . '">' . "\n";
?>

다만 결과가 0건이면 색인할 가치가 낮다고 보고 noindex를 붙였습니다.

<?php
if ($resultCount === 0) {
    echo '<meta name="robots" content="noindex,follow">' . "\n";
}
?>

검색 결과 페이지는 URL만 예쁘게 만든다고 끝이 아닙니다. title, H1, canonical, noindex 기준까지 같이 정해야 합니다.

적용 후 확인한 요청 흐름

수정 후에는 JS가 켜진 경우와 꺼진 경우를 나눠 확인했습니다.

JS 켜짐
검색 폼 제출
→ /mysite/board/856/search?keyword=점프
→ 200 OK
JS 꺼짐
검색 폼 제출
→ /list.php?tb=board_856&search_mode=ok&id=mysite&board_keyword=점프
→ 301
→ /mysite/board/856/search?keyword=점프
→ 200 OK

명령줄에서도 옛 URL이 새 URL로 이동하는지 확인했습니다.

curl -sIL "https://example.com/list.php?tb=board_856&search_mode=ok&id=mysite&board_keyword=점프" | grep -i "^HTTP\|^location"

기대 결과는 아래와 같았습니다.

HTTP/2 301
location: https://example.com/mysite/board/856/search?keyword=%EC%A0%90%ED%94%84
HTTP/2 200

적용 후 달라진 점

수정 후 게시판 검색 주소도 전체 검색과 비슷한 구조로 정리됐습니다.

  • 게시판 검색 URL이 /mysite/board/856/search?keyword=점프로 통일
  • JS가 켜진 환경에서는 301 없이 바로 새 주소로 이동
  • JS가 꺼진 환경에서도 서버 301으로 최종 주소 통일
  • x, y 좌표 파라미터가 만드는 중복 URL 감소
  • 검색 결과 페이징에서 keyword와 start 유지
  • 검색 결과별 canonical과 noindex 기준 분리

가장 중요한 것은 “폼을 직접 고치지 않아도 최종 URL을 하나로 만들었다”는 점입니다. 레거시 폼은 그대로 두고, 입구와 출구에서 정리했습니다.

검색 폼 URL 점검 체크리스트

빠른 경로와 안전망을 함께 두기

수정하기 어려운 레거시 폼을 만나면 둘 중 하나만 고르기 쉽습니다. 서버에서 받아서 301로 보낼지, 자바스크립트로 전송 전에 가로챌지 선택하려고 합니다.

하지만 실제 운영에서는 둘 다 필요했습니다. 서버 301은 느리지만 안정적이고, 자바스크립트 가로채기는 빠르지만 실행되지 않을 수 있습니다.

그래서 서버 301을 바닥에 깔고, 그 위에 JS를 얹었습니다. JS가 살아 있으면 사용자는 한 번에 예쁜 URL로 이동하고, JS가 죽어도 서버가 최종 주소로 보내줍니다.

URL 정리의 목표는 예쁜 주소 하나를 만드는 것이 아니라, 어떤 경로로 들어와도 최종 주소가 하나로 모이게 만드는 것이었습니다.

댓글 남기기