레거시 경로를 새 구조로 301 리다이렉트 구성
목차
레거시 URL 구조(/{slug})에서 새로운 경로(/p/{id})로 마이그레이션하면서 기존 링크들을 301 영구 리다이렉트로 연결하는 작업을 했다. Astro의 동적 라우팅을 활용해 SEO 손실을 최소화하면서 깔끔하게 처리했는데, 이런 류의 경로 마이그레이션은 생각보다 신경 쓸 부분이 많더라.
왜 URL 구조를 바꿨는가
시간이 지나면서 서비스의 콘텐츠 모델이 진화한다. 처음엔 /{slug} 형태로 간단하게 시작했지만, 같은 slug 값이 여러 타입의 리소스에서 충돌할 수 있다는 걸 발견했다. 예를 들어 "guide"라는 제목의 글도 있고 "guide"라는 카테고리도 있으면 라우팅이 모호해진다. 그래서 /p/{id} 형태로 변경해서 ID 기반 주소 공간으로 옮겼다.
문제는 외부에서 링크되어 있던 기존 URL들이 죽으면 SEO에 큰 타격을 입는다는 것. 검색 엔진이 404를 만나면 그 페이지를 색인에서 제거하고, 외부 사이트에서 링크한 권한(링크 주스)도 사라진다. 따라서 301 리다이렉트로 "이 페이지는 여기로 이사했습니다"라고 명확히 신호를 보내야 한다.
301 리다이렉트의 의미
HTTP 상태 코드 301은 "영구적 이동(Moved Permanently)"을 뜻한다. 이게 302 (임시 이동)과 다른 점은 검색 봇 관점에서 매우 중요하다:
| 구분 | 301 영구 리다이렉트 | 302 임시 리다이렉트 |
|---|---|---|
| 의미 | 이 주소는 영원히 새 주소로 옮겨갔음 | 일시적으로 다른 곳에서 제공 중 |
| SEO 영향 | 링크 권한(authority) 대부분 이전됨 | 권한이 원래 주소에 남음 |
| 북마크 동작 | 브라우저가 새 주소로 갱신 | 사용자 북마크는 여전히 기존 주소 |
| 캐싱 | 브라우저가 결과를 캐싱할 수 있음 | 캐싱 불가 (매번 재요청) |
내가 한 작업은 영구적 구조 변경이므로 301이 맞다. 만약 "이 주소는 임시로 다른 곳에서 제공된다"면 302를 썼을 텐데, 예를 들어 메인터넌스 기간에 다른 서버로 트래픽 우회할 때처럼.
Astro 동적 라우팅에서 구현하기
src/pages/[...slug].astro 는 모든 경로를 catch하는 와일드카드 라우트다. 이 파일에서 들어온 요청의 slug 값을 읽고, 그게 레거시 형식이면 새 경로로 리다이렉트 시킨다.
기본 패턴은 이렇다:
// src/pages/[...slug].astro
export async function getStaticPaths() {
// 옛 데이터베이스에서 레거시 slug → 새 id 매핑 가져오기
const legacyMappings = await getLegacySlugToIdMap()
return legacyMappings.map(({slug, id}) => ({
params: { slug },
props: { redirectTo: `/p/${id}` }
}))
}
const { redirectTo } = Astro.props
if (redirectTo) {
// 301 리다이렉트 응답 반환
return Astro.redirect(redirectTo, 301)
}
Astro의 Astro.redirect() 는 상태 코드를 명시적으로 지정할 수 있다. 기본값은 302인데, 영구 이동이므로 301로 명시해야 한다.
구현하며 고민했던 부분들
1. 매핑 데이터 관리
옛 slug에서 새 id로 매핑하는 데이터가 필요하다. 처음엔 데이터베이스 쿼리를 직접 했는데, 빌드 타임에 모든 레거시 경로를 정적으로 생성해야 한다면 각 slug마다 쿼리하면 너무 느리다. 결국 한 번에 전체 매핑을 가져와서 메모리에 올려두는 방식으로 최적화했다.
2. 404 처리
만약 요청받은 slug가 매핑에 없으면? 순수하게 없는 경로라면 404를 반환해야 한다. 이 부분을 빠뜨리면 모든 요청이 어딘가로든 리다이렉트되어, 검색 봇이 존재하지 않는 페이지도 추적하게 된다. 명확히 구분하는 게 좋다.
// 존재하지 않는 경로는 명확히 404
if (!post) {
return new Response('Not Found', { status: 404 })
}
3. Canonical 헤더와의 조화
렌더링되는 페이지들이 이미 <link rel="canonical" href="/p/{id}" /> 를 가지고 있다면, 리다이렉트와 중복된다. 검색 봇 입장에선 301 리다이렉트면 충분하지만, 시간이 지나면서 점진적으로 canonical만 참고하는 봇도 있을 수 있다. 깔끔한 구조를 위해 리다이렉트되는 경로는 canonical을 넣지 않는 게 낫다.
왜 이 작업이 팀 전체에 중요한가
검색 트래픽 손실 방지: 문서나 블로그 같은 콘텐츠를 제공하는 서비스라면, 검색 엔진 유입이 중요하다. 301을 빠뜨리고 URL을 바꾸면 일주일 안에 검색 순위가 급락한다. 본 적 있다.
외부 링크 유지: 다른 사이트에서 우리 글을 링크해줬을 때, 그 링크가 유효하게 유지되어야 한다. 404가 되면 사용자는 그냥 떠나고, 우리는 트래픽을 잃는다.
크롤 효율: 검색 봇의 크롤 예산은 제한적이다. 모든 레거시 경로가 404면 봇이 그걸 색인 제거 목록에 더해야 하고, 301 리다이렉트를 따라가며 새 주소를 업데이트하는 것보다 비효율적이다.
배우고 느낀 점
내가 이 작업을 할 때 처음엔 Astro.redirect()의 기본값이 302라는 걸 놓쳤다. 테스트 환경에서는 문제 없어 보였지만, 검색 봇은 302를 임시 이동으로 해석해서 기존 주소를 유지해야 한다고 판단한다. 결국 좀 더 신경 써서 확인했고, 명시적으로 301을 지정했다.
작은 것 같지만 URL 구조 변경은 큰 작업이다. 특히 공개된 서비스일수록. 다음부터는 URL 설계할 때 처음부터 안정성을 생각하고, 변경이 필요하면 마이그레이션 플랜을 미리 세운다. 리다이렉트 로직, 모니터링, 롤백 계획 같은 것들.
🛒 이 글과 어울리는 추천 상품
*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.
댓글 0
첫 댓글 달아줘.