Next.js Git Blog (7): 국제화(1) - 프로젝트 구조 설계 및 라우팅
목차
소개#
아무래도 개발의 영역은 한 국가에 국한되지 않고 세계적으로 통용되다 보니, 국제화 기능을 넣으면 좋을 것 같기는 한데... 으음 이 부분이 정말 고민이 많이 됐다.
실제로 나 역시 영어로 된 문서도 많이 참고하고 있으니, 내가 공유하는 내용도 언어에 국한되지 않았으면 하는 바람은 있는데 단순히 생각했을 때 아래 문제들이 좀 걸렸다.
- 나는 작문을 잘 못한다. 한국말도 가끔 꼬이는데, 영어로 표현하는 건 더 어렵지 않을까 싶다.
- 관리가 번거로워지는 것이 눈에 훤하다. 영문으로 작성된 같은 글이 물리적으로 또 필요하고 그만큼 관리 포인트가 늘어날 것이다.
그렇지만 보통 이렇게 생각을 가져버리면 결국 실행해보는 것 같다. 영어 공부를 글도 쓰면서 편하게 한다고 생각하면서, 한 번 진행해보고자 한다.
그리고 나중에 너무 피곤해지면 슬쩍 기능을 꺼버리자👍
구현 계획#
시작에 앞서 먼저 어떻게 구현할지 계획을 해보자.
- 사용자 언어 감지: 블로그에 처음 접근한 사용자의 브라우저 언어 설정을 파악해 초기 언어를 설정하자.
- 설정 제어 및 저장: 대부분의 사이트처럼 상단에 언어 토글 버튼을 구현하고 설정값은
localStorage에 저장해 재방문 시에도 유지되도록 구현해보자. - URL 구조 설계: 언어별 마크다운 파일 이름은 동일하게 유지하되, URL에
/en/같은 prefix를 붙여 분기를 나누어 보여주자. - 자동 리다이렉트:
localStorage에 설정값이 있다면 해당 언어로, 없다면 브라우저 기본 언어를 토대로 적절하게 리다이렉트하자. - 무결성 검증: 영문 마크다운 파일이 누락될 경우를 생각해볼 수 있는데, 빌드 시점에 언어별 파일 쌍이 존재하는지 비교하고 하나라도 빠졌다면 빌드 자체가 실패하도록 유도해서 이를 방지해보고자 한다.
URL로 분기를 하지 않고, 설정값에 따라 동적으로 마크다운 파일을 불러오는 방식도 고려해볼 수 있는데,
SEO를 고려한다면 URL로 직접 분기하는 것이 더 좋을 것 같다.
구현#
최종적으로 계획한 URL 구조는 다음과 같다.
- 한국어: htbae.gitblog.io/ko/blog/recent
- 영어: htbae.gitblog.io/en/blog/recent
이를 고려해서 진행해보자.
1. 프로젝트 구조 변경
기존의 Next.js Pages Router 구조에 언어 설정을 담당할 [lang] 디렉토리를 추가하여 계층을 한 단계 확장해야한다.
- 기존 구조
단일 언어를 기준으로 섹션과 상세 페이지가 구성되어 있다.
pages
├── [section]
│ ├── [slug].tsx
│ └── index.tsx
└── index.tsx- 새 구조
최상위에[lang]동적 경로를 추가하여, 동일한 로직 내에서 콘텐츠를 분기 처리할 수 있도록 하자.
pages
├── [lang]
│ ├── [section]
│ │ ├── [slug].tsx
│ │ └── index.tsx
│ └── index.tsx
└── index.tsx2. 동적 경로 로직 수정
폴더 구조가 깊어짐에 따라, 빌드 시점에 경로를 생성하는 getStaticPaths와 데이터를 가져오는 getStaticProps 로직에도 변화가 필요하다.
가장 하위 페이지인 [slug].tsx를 예로 들어보자
// pages/[lang]/[section]/[slug].tsx
const LANGS = ['ko', 'en'];
export const getStaticPaths: GetStaticPaths = async () => {
const sections = getAllSections();
// 언어(LANGS)와 섹션, 슬러그를 조합하여 3중 중첩 경로를 생성한다.
const paths = LANGS.flatMap((lang) =>
sections.flatMap((section) =>
getAllPostIds({ section }).map(({ slug }) => ({
params: { lang, section, slug }, // lang 추가
})),
),
);
return {
paths,
fallback: false, // 생성되지 않은 경로는 404 처리
};
};경로 생성뿐만 아니라, 컴포넌트 내부에서 네비게이션이나 링크 이동을 처리할 때도 현재 언어 정보가 필요해졌다.
따라서 getStaticProps에서 lang 값을 추출하고 props로 넘겨주도록 수정했다.
// pages/[lang]/[section]/[slug].tsx
export const getStaticProps: GetStaticProps = async ({ params }) => {
const lang = params?.lang as string;
const section = params?.section as string;
const slug = params?.slug as string;
if (!lang || !section || !slug) return { notFound: true };
try {
...
return {
props: { lang, section, slug },
};
} catch (err) {
console.error(`[SlugPage] Error loading ${section}/${slug}:`, err);
return { notFound: true };
}
};
3. 루트 리다이렉트 처리
기존의 pages/index.tsx는 이제 pages/[lang]/index.tsx로 옮겨져 언어별 메인 페이지 역할을 하도록 변경했다.
이제 비어있는 루트 경로(/)에 접속했을 때, 설정이나 환경에 맞춰 적절한 언어 페이지로 리다이렉트하는 로직을 구현해보자.
// pages/index.tsx
const RootPage = () => {
const router = useRouter();
useEffect(() => {
// 저장된 설정값 확인
const savedLang = localStorage.getItem('lang') as LangType;
// 브라우저 기본 언어 감지 (한국어면 ko, 그 외엔 en)
const browserLang: LangType = navigator.language.startsWith('ko')
? 'ko'
: 'en';
const targetLang = savedLang || browserLang;
// 결정된 페이지로 이동
router.replace(`/${targetLang}`);
}, [router]);
return null;
};마무리#
여기까지 pages/[lang]/... 구조로 변경이 끝났다. 이제 사용자가 접속했을 때 언어별로 독립된 URL을 가지게 되었고,
동적 라우팅을 통해 각 언어에 맞는 글을 보여줄 준비를 마쳤다.
다음 글에서는 사용자가 직접 언어를 전환할 수 있는 '언어 설정 토글 버튼' 을 구현할 것이다.
그리고 localStoage에 저장된 설정값에 따라 적절한 마크다운 파일을 매칭하여 불러오는 로직을 추가하여
국제화 기능 구현을 마무리하도록 하겠다.