- Published on
- ·14 min read
Next.js - 블로그 다국어(i18n)로 만들기 Part 2
개요
Next.js - 블로그 다국어(i18n)로 만들기 Part 1 포스팅에서 블로그 글을 제외한 사이트에 대한 다국어 처리를 했습니다.
블로그 글의 경우 .md(x) 파일을 읽어서 글을 표시합니다. 앞선 내용의 {t('key')}
처럼 관리할 수 없다는 것입니다.
선택된 언어에 맞게 등록된 글을 표시하고 글 목록, 태그 목록 등을 달리 표시해야 합니다.
내용이 길어져 Part 1 ~ 4 로 나누어서 작성합니다.
Part 2에서는 동적인 블로그 글들을 선택된 언어에 맞게 표시하는 방법을 알아봅니다.
- Part 1: next-i18next 설치와 사이트 다국어 처리
- Part 2: 등록된 글들에 대한 다국어 처리
- Part 3: Rss와 Sitemap 처리
- Part 4: SEO를 위한 lang, alternate 그리고 날짜 형식 처리
gitHub, i18n-tailwind-nextjs-starter-blog를 참고하여 만들었습니다.
Tailwind CSS Blog Starter 템플릿 사용을 결정하셨고, 프로젝트를 처음 시작한다면 위 프로젝트로 시작하는 것도 좋을 것 같습니다.
제 경우 이미 프로젝트가 진행 중이라서, 위 프로젝트를 참고하여 진행했습니다.
파일명으로 언어 구분
- 기본 언어(
ko
)의 경우 기존 파일명과 동일하게 만들면 됩니다.
(예: nextjs-blog-multi-language-part2.mdx) - 다른 언어(
en
)의 경우 기본 언어 파일명 뒤에.{locale}
을 붙여서 사용합니다.
(예: nextjs-blog-multi-language-part2.en
.mdx)
공통 함수 수정
글 목록을 가져오거나, slug에 따라 글 내용을 가져오는 함수 등 공통으로 사용되는 함수들을 수정합니다.
lib/mdx.js
filterLocaleFiles
추가- 파일 목록 중 선택된 언어와 일치하는 파일만 필터링 합니다.
- defaultLocale일 경우
.{locale}
이 붙지 않은 파일만 필터링 합니다.
// 언어별 파일 필터링
function filterLocaleFiles(files, locale, defaultLocale, locales) {
let filterFiles = files
if (locales && defaultLocale && locales) {
filterFiles =
locale !== defaultLocale
? files.filter((path) => path.includes(`.${locale}.`))
: files.filter((path) => !locales.some((locale) => path.includes(`.${locale}.`)))
}
return filterFiles
}
getFiles
수정- 위에서 추가한
filterLocaleFiles
를 사용하여 현재 언어에 맞는 파일만 필터링 합니다.
- 위에서 추가한
export function getFiles(type, locale, defaultLocale, locales) { // locale, defaultLocale, locales 추가
const prefixPaths = path.join(root, 'data', type)
const files = getAllFilesRecursively(prefixPaths)
const filterFiles = filterLocaleFiles(files, locale, defaultLocale, locales) // 추가
return filterFiles.map((file) => file.slice(prefixPaths.length + 1).replace(/\\/g, '/')) // files -> filterFiles로 변경
}
formatSlug
수정- slug(파일명)에 포함된 locale(예:.en)을 제거합니다.
export function formatSlug(slug, locales = null) {
let result = slug.replace(/\.(mdx|md)/, '')
// 추가: slug에서 locale 제거
if (locales && locales.length > 0) {
locales.forEach((locale) => {
result = result.replace(`.${locale}`, '')
})
}
return result
}
getFileBySlug
수정- 선택된 언어와 일치하는 파일을 읽어올 수 있도록 slug에 locale을 추가합니다.
export async function getFileBySlug(type, slug, locale = null, defaultLocale = null) { // locale = null, defaultLocale = null 추가
// defaultLocale이 아닐경우 해당 언어의 파일을 가져오도록 수정
const mdxPath =
locale === defaultLocale
? path.join(root, 'data', type, `${slug}.mdx`)
: path.join(root, 'data', type, `${slug}.${locale}.mdx`)
const mdPath =
locale === defaultLocale
? path.join(root, 'data', type, `${slug}.md`)
: path.join(root, 'data', type, `${slug}.${locale}.md`)
const source = fs.existsSync(mdxPath)
? fs.readFileSync(mdxPath, 'utf8')
: fs.readFileSync(mdPath, 'utf8')
// 그 외 변경사항 없음
}
getAllFilesFrontMatter
수정filterLocaleFiles
을 사용하여 현재 언어에 맞는 파일만 필터링 합니다.
export async function getAllFilesFrontMatter(folder, locale, defaultLocale, locales) { // locale, defaultLocale, locales 추가
const prefixPaths = path.join(root, 'data', folder)
const files = getAllFilesRecursively(prefixPaths)
const filterFiles = filterLocaleFiles(files, locale, defaultLocale, locales) // 추가
const allFrontMatter = []
filterFiles.forEach((file) => { // filterFiles로 변경
// ...
if (frontmatter.draft !== true) {
allFrontMatter.push({
...frontmatter,
slug: formatSlug(fileName, locales), // formatSlug에 locales 추가
date: frontmatter.date ? new Date(frontmatter.date).toISOString() : null,
})
}
})
return allFrontMatter.sort((a, b) => dateSortDesc(a.date, b.date))
}
lib/tags.js
getAllTags
수정- 필터가 적용되도록 locale, defaultLocale, locales 파라미터를 추가합니다.
export async function getAllTags(type, locale, defaultLocale, locales) { // locale, defaultLocale, locales 추가
const files = await getFiles(type, locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
// ...
}
pages 폴더 내 파일 수정
pages/index.js
- 필터가 적용되도록 locale, defaultLocale, locales 파라미터를 추가합니다.
export async function getStaticProps({ locale, defaultLocale, locales }) { // locale, defaultLocale, locales 추가
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
return { props: { posts } }
}
메인 페이지 목록이 언어별로 필터링된 것을 확인할 수 있습니다.
pages/blog.js
- 필터가 적용되도록 locale, defaultLocale, locales 파라미터를 추가합니다.
export async function getStaticProps({ locale, defaultLocale, locales }) { // defaultLocale, locales 추가
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
// ...
}
전체 글 목록이 언어별로 필터링된 것을 확인할 수 있습니다.
pages/blog/page/[page].js
- 동적 라우팅의 경우
getStaticPaths
부분을 모든 언어별 경로를 생성하도록 수정합니다. (path가 없으면 404 에러가 발생합니다.)
import { getAllFilesFrontMatter, getFiles } from '@/lib/mdx' // getFiles 추가
export async function getStaticPaths({ defaultLocale, locales }) { // defaultLocale, locales 추가
// 모든 언어별 경로 생성
const localePaths = (
await Promise.all(
locales.map(async (locale) => {
// getAllFilesFrontMatter -> getFiles 로 변경 및 locale, defaultLocale, locales 추가
const posts = await getFiles('blog', locale, defaultLocale, locales)
const totalPages = Math.ceil(posts.length / POSTS_PER_PAGE)
return Array.from({ length: totalPages }, (_, i) => ({
params: { page: (i + 1).toString() },
locale,
}))
})
)
).flat()
// paths 수정
return {
paths: localePaths.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps(context) {
// locale, defaultLocale, locales 추가
const {
params: { page },
locale,
defaultLocale,
locales,
} = context
const posts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
// ...
}
페이징이 언어별로 생성된 것을 확인할 수 있습니다.
pages/tags.js
- 필터가 적용되도록 locale, defaultLocale, locales 파라미터를 추가합니다.
export async function getStaticProps({ locale, defaultLocale, locales }) { // locale, defaultLocale, locales 추가
const tags = await getAllTags('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
return { props: { tags } }
}
태그가 언어별로 생성된 것을 확인할 수 있습니다.
pages/tags/[tag].js
- 동적 라우팅의 경우
getStaticPaths
부분을 모든 언어별 경로를 생성하도록 수정합니다.
export async function getStaticPaths({ locales, defaultLocale }) { // locales, defaultLocale 추가
// 모든 언어별 경로 생성
const localeTags = (
await Promise.all(
locales.map(async (locale) => {
const tags = await getAllTags('blog', locale, defaultLocale, locales)
return Object.keys(tags).map((tag) => ({
params: { tag },
locale,
}))
})
)
).flat()
// paths 수정
return {
paths: localeTags.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps({ params, locale, defaultLocale, locales }) { // locale, defaultLocale, locales 추가
const allPosts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
// ...
}
태그 선택시 해당 태그가 언어별로 생성된 것을 확인할 수 있습니다.
pages/blog/[...slug].js
- 동적 라우팅의 경우
getStaticPaths
부분을 모든 언어별 경로를 생성하도록 수정합니다. formatSlug
호출에도locales
파라미터가 추가된 것을 잊지마세요.
export async function getStaticPaths({ locales, defaultLocale }) { // locales, defaultLocale 추가
// 모든 언어별 경로 생성
const localePosts = (
await Promise.all(
locales.map(async (locale) => {
const posts = getFiles('blog', locale, defaultLocale, locales)
return posts.map((post) => ({
params: {
slug: formatSlug(post, locales).split('/'), // locales 추가
},
locale,
}))
})
)
).flat()
// paths 수정
return {
paths: localePosts.map(({ params, locale }) => ({
params,
locale,
})),
fallback: false,
}
}
export async function getStaticProps({ params, locale, defaultLocale, locales }) { // locale, defaultLocale, locales 추가
const allPosts = await getAllFilesFrontMatter('blog', locale, defaultLocale, locales) // locale, defaultLocale, locales 추가
const postIndex = allPosts.findIndex(
(post) => formatSlug(post.slug, locales) === params.slug.join('/') // locales 추가
)
// ...
const post = await getFileBySlug('blog', params.slug.join('/'), locale, defaultLocale) // locale, defaultLocale 추가
// ...
}
글이 선택된 언어에 맞게 표시되는 것을 확인할 수 있습니다.
(선택 언어로 작성된 글이 없는 경우 404 에러가 발생합니다.)
localhost 확인 시 속도
localhost에서 확인하는데 느립니다. (아직 배포전이라 production에서 확인은 못했습니다.)
이에 대한 원인은 Next.js 공식 문서: getStaticPaths 에서 확인할 수 있습니다.
위 내용을 발췌하면 아래와 같습니다.
In development (next dev), getStaticPaths will be called on every request.
해석하면 아래와 같습니다.
개발 모드(next dev)에서는 getStaticPaths가 모든 요청마다 호출됩니다.
정리
목록, 글 보기 등 제 눈에 보이는 부분은 정상 동작하는 것을 확인했습니다.
(누락된 부분과 오류가 있는 부분이 있을 지도 모르겠습니다. 😥)
이제 남은 건 눈에 보이지 않는 부분인 SEO, Sitemap, RSS 등을 추가하는 것입니다.
Next.js - 블로그 다국어(i18n)로 만들기 Part 3 에서 SiteMap, RSS를 다국어로 만드는 방법을 알아보겠습니다.
코드
변경된 코드는 commit history 를 참고해주세요.