비슷한 주제를 다루던 페이지 여러 개를 대표 페이지 몇 개로 합치면서, 예전 링크로 들어오는 사람들을 어떻게 처리할지 고민했던 적이 있어요. img-compressor 저장소next.config.js redirects()로 실제로 어떻게 정리했는지 보여드릴게요.

경로 매핑은 배열 말고 객체로 관리했어요

async redirects() {
  const legacyRedirects = {
    "naver-blog-image-size-guide": "blog-image-optimization-checklist",
    "google-image-seo-alt-filename-guide": "jpg-png-webp-format-choice",
    "product-thumbnail-compression-checklist": "ecommerce-product-image-compression-guide",
    "image-quality-60-70-80-comparison": "image-compression-benchmark-results",
    // ...
  };

  return Object.entries(legacyRedirects).map(([source, destination]) => ({
    source: \`/:locale(ko|en)/\${source}\`,
    destination: \`/:locale/\${destination}\`,
    permanent: true,
  }));
}

예전 경로랑 새 경로를 { 예전: 새로운 } 객체로 한 곳에 모아두고, Object.entries로 돌려서 Next.js가 원하는 { source, destination, permanent } 형태로 바꿔요. 배열로 하나씩 나열하는 대신 객체로 관리하니까, 어떤 예전 경로가 어디로 합쳐졌는지 한눈에 보이고 항목 추가할 때도 배열 구조를 반복해서 안 써도 되더라고요.

동적 세그먼트로 언어별 경로를 한 번에 처리했어요

/:locale(ko|en)/naver-blog-image-size-guide처럼 source에 정규식 들어간 동적 세그먼트를 쓰면, /ko/.../en/...를 따로 등록 안 해도 규칙 하나로 같이 처리돼요. destination:locale도 매칭된 값을 그대로 받아서, 리다이렉트 후에도 보던 언어가 그대로 유지되고요.

permanent: true를 고른 이유

permanent: true는 HTTP 301로 응답한다는 뜻이에요. 페이지를 합쳐도 예전 URL로 들어오는 검색 유입이랑 외부 링크는 계속 있을 수 있어서, 302(임시)가 아니라 301로 응답해서 검색 엔진한테 "이 페이지 색인 신호는 새 URL로 옮겨가라"고 확실히 알려줬어요. 반대로 페이지 통합이 임시적이거나 나중에 되돌릴 수도 있다면 301은 안 쓰는 게 안전해요. 한 번 301로 알린 관계는 검색 엔진 캐시에도 오래 남더라고요.