개발 slecs

블로그 URL을 ID 기반으로 전환하고 기존 slug 링크 보존

목차

블로그 포스트의 URL을 기존 slug 방식에서 /p/{id} 형태의 ID 기반 canonical URL로 전환했다. 기존 slug URL은 301 permanent redirect로 처리해서 기존 링크와 SEO 가치는 그대로 보존하는 작업이었다.

URL 정규화를 해야 했던 이유

URL 구조는 단순한 라우팅 문제가 아니라 팀의 유지보수성, 검색 엔진 최적화, 장기 시스템 안정성에 직결된다. 이전에 slug 기반으로 운영하다 보니 몇 가지 문제가 생기고 있었다.

첫째, slug는 포스트의 제목에 의존한다. 제목을 수정하면 URL도 바뀌어야 하는데, 기존 링크들이 깨진다. 외부에서 링크하는 경우도 마찬가지고, 시간이 지나면서 고아 URL이 쌓인다. ID 기반이면 제목을 아무리 바꿔도 URL은 변하지 않는다.

둘째, slug의 변경 이력 관리가 복잡하다. "혹시 예전 slug로 접근하는 사용자가 있을 수도 있고"라는 가정 하에 여러 slug를 동시 지원해야 하는 경우도 생긴다. 반면 ID는 불변이라 관계도 간단하다.

셋째, 데이터베이스 쿼리 효율성이 다르다. slug는 unique index에서 문자열 매칭을 하지만, id는 정수 PK로 훨씬 빠르다. 포스트 조회가 블로그의 핵심 동작이니 이건 무시할 수 없다.

어떻게 구현했나

구성 요소 변경 내용 목적
app/main.py /p/<int:post_id> 새 라우트 추가, /blog/<slug>/p/{id} 301 redirect 추가 라우팅 중앙화, 기존 URL 호환성
app/templates/ (4개 파일) 모든 포스트 링크 생성 로직을 url_for('post', post_id=post.id) 형태로 통일 새로운 canonical URL 참조
scripts/gen_blog_feed.py RSS/JSON feed 피드의 포스트 URL도 새 형식으로 업데이트 구독자들이 받는 링크의 일관성

가장 중요한 건 라우팅 레이어에서 두 경로를 모두 지원하는 것이다. 새로운 canonical URL(/p/{id})이 메인이지만, 기존 slug URL(/blog/{slug})에서 오는 요청도 301로 리다이렉트해서 처리한다. 검색 엔진은 301을 따라가서 canonical URL로 크롤링하고, SEO 점수도 정상 이관된다.

# 예시 구조 (실제 구현은 프레임워크/DB별로 다름)
@app.route('/p/<int:post_id>')
def post(post_id):
    # ID로 조회 — 빠른 쿼리
    post = db.get_post_by_id(post_id)
    return render_template('post.html', post=post)

@app.route('/blog/<slug>')
def post_legacy(slug):
    # slug로 대응하는 id 찾아서 리다이렉트
    post = db.get_post_by_slug(slug)
    return redirect(f'/p/{post.id}', code=301)

마이그레이션 과정에서 챙긴 것들

한 가지 신경 썼던 부분은 모든 URL 생성 지점을 일관되게 처리하는 것이었다. 4개의 템플릿(홈, 검색, 관리자 대시보드, 포스트 페이지)에서 포스트 링크를 만드는데, 하나라도 빠지면 사용자가 클릭했을 때 slug URL로 가게 된다. 물론 301이 처리하긴 하지만, 불필요한 리다이렉트 요청이 늘어나고 모바일 환경에선 지연이 체감될 수 있다.

또한 feed 스크립트도 함께 업데이트한 이유는, 외부 RSS 리더나 소셜 미디어에 공유될 링크까지 고려했기 때문이다. 이들도 새로운 canonical URL을 보면 앞으로 그 형식으로 인식하게 된다.

배운 점 & 회고

이 작업을 하면서 느낀 건 URL 구조는 한 번 정하면 변경 비용이 크다는 것이다. 초기 설계 단계에서 "ID 기반이 나을까, slug가 나을까" 고민했다면 지금처럼 여러 지점을 손볼 일이 없었을 거다.

다만 실제로는 초기엔 slug가 직관적이고 SEO상 이점이 있어 보였을 수 있다. slug URL은 사람이 읽기 쉽고(clean URL), 검색 엔진도 keyword를 인식한다. 하지만 콘텐츠가 자주 업데이트되거나 팀이 커지면 유지 비용이 커진다. 이건 우리가 경험한 거다.

지금 301 redirect로 처리해서 기존 링크를 보존했지만, 이상적으로는 처음부터 id 기반으로 설계하고, SEO가 필요하면 <link rel="canonical"> 태그에 keyword를 섞은 URL을 명시적으로 두는 방식도 있다. 하지만 지금 상황에선 id만으로도 충분해 보인다.

또 하나, 비슷한 URL 리팩토링을 하는 팀들은 보통:
- 302 (temporary) 대신 301 (permanent)을 쓰는 게 표준이다. 검색 엔진 최적화와 브라우저 캐싱 관점에서 유리하다.
- 데이터 마이그레이션이 선행되면 좋은데 (DB에 새로운 canonical URL 컬럼 추가 등), 선택사항이다. 우린 slug와 id를 병행하는 방식으로 진행했다.
- 로그/분석에서 리다이렉트 빈도를 모니터링하면 언제쯤 기존 URL 트래픽이 충분히 줄어들었는지 파악할 수 있다. 그 이후 slug URL 지원을 단계적으로 제거할 수도 있다.

이 정도 규모의 변경이면 대게 며칠 내에 수렴되지만, 장기적으로 봤을 때 URL 구조 정규화는 팀의 토론과 문서화까지 함께 가야 의미 있다. "왜 /p/{id}로 했고, 앞으로 새 기능은 이 패턴을 따를 것"을 팀과 공유하는 게 좋다.


🛒 이 글과 어울리는 추천 상품

*위 링크는 쿠팡파트너스 활동의 일환이며, 일정액의 수수료를 제공받을 수 있습니다.

댓글 0

첫 댓글 달아줘.