다국어 URL은 번역 기능 하나를 더 붙이는 일처럼 보이지만, 실제로는 사이트 구조를 다시 정하는 작업에 가깝습니다. 특히 지금처럼 한국어 블로그로 먼저 운영하다가 나중에 영어 제품이나 미국 대상 웹게임으로 확장하려는 경우에는, 어떤 언어가 어떤 경로를 차지할지, 같은 글의 언어별 대응 관계를 만들 건지, 검색 엔진에 그 관계를 어떻게 알려줄지를 먼저 정해야 합니다. 이걸 나중에 덧붙이면 URL도 바뀌고, 내부 링크도 흔들리고, canonical과 sitemap도 다시 만져야 합니다.
Next.js 공식 문서와 Google 쪽 문서를 같이 보면 중요한 사실이 몇 개 보입니다. Next.js App Router에서는 app/[lang] 같은 구조로 locale 기반 라우팅을 직접 설계하는 편이 기본이고, metadata.alternates.languages나 localized sitemap도 지원합니다. 반면 Pages Router의 built-in i18n은 output: 'export'와 통합되지 않습니다. Google 쪽 문서는 hreflang을 서로 대응되는 언어 버전 페이지에만 써야 한다고 설명하고, canonical도 가능하면 같은 언어의 canonical을 가리키라고 권장합니다.
즉 다국어 URL의 핵심은 “어떻게 번역할까”보다 먼저 “무엇이 서로의 번역본인지, 무엇은 아예 다른 콘텐츠인지, 그리고 정적 배포 구조에서 어떤 라우팅을 선택할지”를 구분하는 것입니다.
이 글은 2026년 4월 8일 기준 Next.js와 Google 공식 문서를 바탕으로, 다국어 URL을 붙이기 전에 먼저 정해야 하는 질문들을 정리한 글입니다. 특히 현재 이 레포처럼 output: "export"를 쓰는 Next.js App Router 프로젝트를 기준으로 설명하겠습니다.
한국어 블로그에서 영어 제품으로 확장할 때 URL, hreflang, canonical, sitemap, App Router 구조를 어떻게 먼저 정해야 하는지 정리합니다. 특히 `output: "export"`를 쓰는 Next.js 프로젝트에서 무엇을 다르게 봐야 하는지도 함께 설명합니다. 핵심 1 핵심 2 핵심 3다국어 URL을 붙이기 전에 꼭 생각해야 할 것들
먼저 결론: 다국어 URL을 붙이기 전에 다섯 가지를 먼저 정한다
먼저 “같은 페이지의 번역본”과 “아예 다른 콘텐츠”를 구분한다
지금 프로젝트처럼 output: "export"를 쓰면 Pages Router built-in i18n보다 App Router의 [lang] 구조가 더 자연스럽다
먼저 결론: 다국어 URL을 붙이기 전에 다섯 가지를 먼저 정한다
제가 먼저 정하는 기준은 아래 다섯 가지입니다.
- 이 사이트는 “같은 콘텐츠의 여러 언어 버전”을 만들 건가, 아니면 “언어별로 서로 다른 섹션”을 운영할 건가
- URL 전략은 sub-path인가, domain 분리인가
- default locale도 prefix를 붙일 건가
- locale 대응 페이지끼리 canonical, hreflang, sitemap 관계를 어떻게 만들 건가
- 지금 프로젝트가
output: "export"라면 App Router에서 locale route를 어떻게 정적으로 생성할 건가
이 다섯 질문을 먼저 정하지 않으면, 나중에 /blog/post-a를 /ko/blog/post-a로 옮기거나, /en과 /ko를 섞다가 내부 링크가 흔들리는 일이 쉽게 생깁니다.
1. 먼저 “같은 페이지의 번역본”과 “아예 다른 콘텐츠”를 구분한다
Google의 hreflang 관련 문서는 이 점을 꽤 분명하게 드러냅니다. Lighthouse 문서도 hreflang 링크는 “all the versions of a page”를 알려주는 용도라고 설명하고, 각 버전은 서로와 자기 자신을 모두 가리켜야 한다고 적고 있습니다. 즉 hreflang은 아무 두 페이지 사이에 붙이는 장치가 아니라, 같은 페이지의 언어/지역 버전 묶음에 쓰는 장치입니다.
이걸 현재 계획에 대입하면 중요한 질문이 나옵니다.
- 한국어 블로그 글과 영어 제품 랜딩은 같은 페이지의 언어 버전인가
- 아니면 한국어 블로그는 블로그이고, 영어 제품은 별도의 섹션인가
제 판단으로는 지금 목표처럼 “한국어 블로그로 시작하고, 나중에 미국 대상 웹게임으로 확장”하는 경우, 둘은 대부분 같은 페이지의 번역본이 아닙니다. 즉 /ko/blog/...와 /en/game/...는 대응 관계가 아니라 별도 콘텐츠인 경우가 많습니다.
이 구분이 중요한 이유는 두 가지입니다.
- 서로 번역 관계가 아닌 페이지끼리 억지로
hreflang세트를 만들 필요가 없습니다. - 언어별 섹션이 다르면 URL 구조도 “페이지 대응”보다 “사이트 정보 구조” 기준으로 설계하는 편이 낫습니다.
즉 다국어 URL을 붙이기 전에 먼저 “이건 번역본 세트인지, 독립 섹션인지”를 분리하는 편이 좋습니다.
2. 지금 프로젝트처럼 output: "export"를 쓰면 Pages Router built-in i18n보다 App Router의 [lang] 구조가 더 자연스럽다
이건 Next.js 공식 문서에서 바로 확인할 수 있는 사실입니다. Pages Router의 국제화 가이드는 built-in internationalized routing이 output: 'export'와는 통합되지 않는다고 명시합니다. 반면 App Router의 국제화 가이드는 app/[lang] 구조와 generateStaticParams()를 사용해 locale별 정적 경로를 생성하는 방식을 보여줍니다.
지금 레포는 이미 output: "export"를 사용하고 있습니다.
next.config.ts
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "export",
};
export default nextConfig;
그래서 이 프로젝트에서 다국어 URL을 붙인다면, 제일 자연스러운 방향은 Pages Router i18n 설정이 아니라 App Router 쪽 locale segment입니다.
예를 들면 이런 구조입니다.
app/
[lang]/
layout.tsx
page.tsx
blog/
[slug]/
page.tsx
about/
page.tsx
그리고 Next.js 국제화 가이드처럼 generateStaticParams()로 locale을 미리 생성합니다.
app/[lang]/layout.tsx
export async function generateStaticParams() {
return [{ lang: "ko" }, { lang: "en" }];
}
export default async function RootLayout({
children,
params,
}: LayoutProps<"/[lang]">) {
const { lang } = await params;
return (
<html lang={lang}>
<body>{children}</body>
</html>
);
}
이 방식의 장점은 명확합니다.
- 정적 export와 잘 맞습니다.
- locale이 URL에서 명시적으로 드러납니다.
- metadata, sitemap, internal links를 locale 기준으로 계산하기 쉽습니다.
3. URL 전략은 sub-path와 domain 중 하나로 일찍 정하는 편이 좋다
Next.js App Router 국제화 문서는 라우팅이 sub-path(\/fr/products)나 domain(my-site.fr/products) 방식이 될 수 있다고 설명합니다. 즉 큰 방향은 두 가지입니다.
- sub-path:
example.com/ko/...,example.com/en/... - domain:
example.kr/...,example.com/...또는ko.example.com,en.example.com
여기서부터는 공식 문서를 바탕으로 한 제 판단입니다. 현재처럼 한 레포에서 한국어 블로그를 먼저 만들고, 나중에 영어 제품이나 게임까지 연결하려면 초반에는 sub-path가 운영 난이도가 더 낮습니다.
이유는 단순합니다.
- 도메인과 DNS를 여러 개 운영하지 않아도 됩니다.
- sitemap과 metadataBase를 하나의 루트 도메인 안에서 관리하기 쉽습니다.
- 내부 링크, 분석, Search Console 운영이 덜 복잡합니다.
예를 들면 이런 식입니다.
/ko/blog/nextjs-blog-folder-structure
/ko/blog/meta-title-description-fixes
/en/game
/en/game/how-to-play
나중에 영어 게임이 완전히 독립 브랜드로 커지면 domain 분리를 다시 고려할 수는 있습니다. 하지만 초반 구조를 정하는 시점에서는 sub-path가 더 현실적입니다.
4. default locale도 prefix를 붙일지 미리 정해야 URL 이관이 덜 아프다
이건 Next.js가 “무조건 이렇게 하라”고 말하는 항목은 아니지만, App Router와 정적 export를 같이 쓰는 프로젝트에서 매우 중요한 설계 선택입니다. Pages Router 문서에서도 default locale은 기본적으로 prefix가 없고, default locale prefix를 붙이려면 별도 workaround가 필요하다고 설명합니다.
실무적으로는 두 가지 선택이 있습니다.
- 한국어를 루트로 두기:
/blog/..., 영어만/en/... - 둘 다 prefix 붙이기:
/ko/blog/...,/en/blog/...
여기서도 제 판단을 분명히 적으면, 지금 같은 프로젝트는 초반부터 둘 다 prefix를 붙이는 편이 나중에 덜 꼬입니다.
이유는 다음과 같습니다.
- 언어별 URL 규칙이 완전히 대칭이 됩니다.
- 나중에 영어가 더 커져도 루트 URL 정책을 다시 뜯지 않아도 됩니다.
- canonical, alternates, sitemap, internal links를 locale map 기준으로 계산하기 쉬워집니다.
즉 /blog/post-a를 나중에 /ko/blog/post-a로 옮기는 migration 비용을 피하려면, 애초에 locale prefix를 명시하는 편이 더 안정적입니다.
5. slug를 언어마다 번역할지, 공통 식별자로 둘지도 먼저 정해야 한다
이 부분은 공식 문서가 정답을 주는 건 아니지만, Google의 localized versions와 canonical 문서를 조합하면 실무 기준을 세우기 쉽습니다. Google은 같은 페이지의 지역/언어 버전 관계를 명확히 보여주길 원하고, canonical도 가능하면 같은 언어의 canonical을 가리키라고 설명합니다. 그러려면 내부적으로 “어느 페이지가 어느 언어의 같은 콘텐츠인지”를 추적할 수 있어야 합니다.
그래서 slug 정책은 미리 정하는 편이 좋습니다.
예를 들면 선택지는 이런 식입니다.
- 공통 콘텐츠 ID + 언어별 slug 표시
- locale마다 완전히 다른 slug
- 내부 ID는 공통, 공개 slug만 locale별 번역
제 판단으로는 블로그와 제품을 함께 운영할 프로젝트라면 “공개 slug는 locale별로 둘 수 있어도, 내부 식별자는 공통으로 유지”하는 편이 가장 안전합니다.
예를 들면 데이터는 이렇게 생각할 수 있습니다.
type LocalizedEntry = {
id: "post-nextjs-folder-structure";
locales: {
ko: { slug: "nextjs-blog-folder-structure" };
en: { slug: "nextjs-blog-folder-structure" };
};
};
혹은 영어 제품 페이지는 이렇게 따로 둘 수도 있습니다.
type ProductEntry = {
id: "browser-game-home";
locales: {
en: { slug: "game" };
};
};
이렇게 하면 어떤 페이지에 hreflang 세트를 붙일 수 있고, 어떤 페이지는 단일 언어 섹션으로 둬야 하는지가 더 선명해집니다.
6. metadataBase, alternates.languages, canonical은 locale 구조와 함께 바뀌어야 한다
Next.js generateMetadata 문서는 metadataBase와 alternates.languages를 이용해 locale별 canonical과 alternate link를 생성할 수 있다고 설명합니다. 이 레포는 이미 루트 layout에서 metadataBase를 쓰고 있습니다.
src/app/layout.tsx
export const metadata: Metadata = {
metadataBase: new URL(getSiteUrl()),
};
다국어 URL을 붙인 뒤에는 각 locale 페이지가 자기 canonical과 alternate를 함께 내보내는 편이 좋습니다.
app/[lang]/blog/[slug]/page.tsx
import type { Metadata } from "next";
export async function generateMetadata({
params,
}: PageProps<"/[lang]/blog/[slug]">): Promise<Metadata> {
const { lang, slug } = await params;
return {
alternates: {
canonical: `/${lang}/blog/${slug}`,
languages: {
ko: `/ko/blog/${slug}`,
en: `/en/blog/${slug}`,
},
},
};
}
여기서 중요한 Google 쪽 규칙이 하나 더 있습니다. Canonical 관련 문서는 hreflang을 쓰는 경우 canonical은 같은 언어의 canonical 페이지를 가리키거나, 그게 없으면 가장 가까운 대체 언어를 가리키는 편이 좋다고 설명합니다.
즉 이건 좋지 않습니다.
/ko/blog/post-a의 canonical이/en/blog/post-a
반대로 이 방향이 더 맞습니다.
/ko/blog/post-a의 canonical은/ko/blog/post-a/en/blog/post-a의 canonical은/en/blog/post-a- 둘 사이 관계는
alternates.languages와 sitemap alternates로 연결
7. hreflang은 self-reference와 x-default까지 같이 생각해야 한다
Chrome 개발자 문서의 Lighthouse hreflang 안내는 꽤 실무적입니다. 각 페이지 버전은 자기 자신을 포함한 모든 버전을 서로 가리켜야 하고, 언어 선택 페이지처럼 default landing이 있으면 x-default를 둘 수 있다고 설명합니다.
그래서 대응되는 번역본이 있다면 최소한 아래 관계를 생각해야 합니다.
- 한국어 페이지는 한국어 자신 + 영어 버전을 모두 가리킴
- 영어 페이지는 영어 자신 + 한국어 버전을 모두 가리킴
- 언어 선택 랜딩이나
/같은 중립 엔트리가 있다면x-default
예를 들면 head 기준으로 이런 형태입니다.
<link rel="alternate" hreflang="ko" href="https://example.com/ko/blog/post-a" />
<link rel="alternate" hreflang="en" href="https://example.com/en/blog/post-a" />
<link rel="alternate" hreflang="x-default" href="https://example.com/" />
다만 다시 강조하면, 이건 “대응되는 같은 페이지 세트”일 때만 붙이는 편이 맞습니다. 한국어 운영기와 영어 게임 소개 페이지처럼 성격이 다른 콘텐츠를 억지로 한 세트로 묶는 건 좋은 신호가 아닙니다.
8. sitemap도 locale 구조를 반영하도록 미리 설계하는 편이 좋다
Next.js의 sitemap.ts 문서는 localized sitemap을 지원하고, alternates.languages를 넣으면 <xhtml:link hreflang="...">가 포함된 sitemap을 만들 수 있다고 설명합니다. 현재 레포의 sitemap은 locale이 없는 단일 구조입니다.
src/app/sitemap.ts
const staticRoutes = ["", "/blog", "/about", "/contact", "/privacy", "/terms"];
다국어 URL을 붙이면 이 레이어도 함께 바뀌어야 합니다. 예를 들면 대응 페이지가 있는 경우 이런 식으로 표현할 수 있습니다.
import type { MetadataRoute } from "next";
export default function sitemap(): MetadataRoute.Sitemap {
return [
{
url: "https://example.com/ko/blog/post-a",
lastModified: new Date(),
alternates: {
languages: {
ko: "https://example.com/ko/blog/post-a",
en: "https://example.com/en/blog/post-a",
},
},
},
];
}
이렇게 해두면 head의 alternate와 sitemap alternate가 같은 방향을 가리키게 됩니다.
9. locale detection은 편리하지만, 검색 엔진과 공유 링크를 생각하면 명시적 URL이 더 중요하다
Next.js 국제화 가이드는 Accept-Language를 보고 proxy에서 사용자를 적절한 locale로 보낼 수 있다고 설명합니다. 이건 편리합니다. 하지만 검색과 공유까지 생각하면 결국 중요한 건 “사용자가 복사하고, 검색 엔진이 읽는 명시적인 locale URL”입니다.
즉 아래 둘을 구분하는 편이 좋습니다.
- 편의 기능:
/에 들어온 사용자를/ko또는/en으로 보내기 - 진짜 구조:
/ko/blog/...,/en/game/...처럼 명시적인 URL을 유지하기
이 구분이 중요한 이유는 locale-adaptive 페이지처럼 URL은 하나인데 보여주는 내용만 바뀌는 방식은 검색과 디버깅이 더 어려워질 수 있기 때문입니다. Google도 locale-adaptive 페이지는 별도로 다룹니다. 처음 구조를 잡을 때는 URL에 locale을 명시하는 편이 훨씬 안전합니다.
10. 지금 프로젝트에 가장 현실적인 초기 구조
여기서부터는 공식 문서를 현재 레포 상황에 적용한 제 판단입니다. 지금 사이트는 한국어 블로그로 시작했고, 나중에 영어 제품 또는 웹게임으로 확장할 계획입니다. 이 경우 초기에 가장 현실적인 구조는 아래에 가깝습니다.
- 블로그는 우선 한국어 중심
- 영어 제품은 별도 영어 섹션으로 시작
- locale prefix는 초반부터 명시
- 대응되는 번역본이 생긴 페이지에만
alternates.languages와hreflang을 붙임
즉 예를 들면:
/ko/blog/nextjs-blog-folder-structure
/ko/blog/search-console-first-checklist
/en/game
/en/game/how-to-play
/en/game/faq
나중에 정말 한국어/영어 블로그를 둘 다 운영하게 되면 그때부터는 대응 글끼리 alternate 관계를 붙이면 됩니다. 반대로 지금 단계에서 없는 영어 글까지 빈 껍데기만 만들어놓는 건 오히려 구조를 약하게 만들 수 있습니다.
실무 체크리스트
- 이 페이지들이 진짜 번역본 세트인지, 그냥 다른 섹션인지 구분했는가
- 현재 프로젝트가
output: "export"인지 확인했는가 - Pages Router built-in i18n 대신 App Router의
[lang]구조가 더 맞는지 검토했는가 - sub-path와 domain 전략 중 하나를 먼저 골랐는가
- default locale도 prefix를 붙일지 미리 정했는가
- locale별 slug와 내부 콘텐츠 식별 방식을 정했는가
metadataBase, canonical,alternates.languages가 같은 규칙을 따르는가- 대응 페이지끼리만
hreflang을 연결하고 있는가 x-default가 필요한 엔트리 페이지가 있는가- sitemap도 locale 구조와 함께 바뀌도록 설계했는가
마무리
다국어 URL은 나중에 번역이 늘어나면 붙이는 장식이 아니라, 초반 정보 구조를 정할 때 함께 결정해야 하는 규칙입니다. 특히 지금처럼 한국어 블로그를 먼저 만들고 영어 제품을 나중에 붙일 계획이라면, “이 둘이 번역본 관계인지, 독립 섹션인지”를 먼저 가르는 것이 가장 중요합니다.
그리고 현재 프로젝트처럼 output: "export"를 쓰는 Next.js App Router라면, Pages Router의 built-in i18n보다 app/[lang]와 정적 route 생성 쪽이 더 자연스럽습니다. URL, canonical, alternate, sitemap이 모두 같은 방향을 가리키게 만들면, 나중에 영어 확장을 시작할 때도 훨씬 덜 아프게 갈 수 있습니다.
참고 자료: