i18n(internationalization)은 글로벌 시장에서 제품과 서비스를 제공하기 위한 중요한 요소 중 하나이며, 전 세계적으로 다양한 언어와 문화를 고려한 서비스 제공을 위해 필수적인 기술입니다. 이번 포스팅에서는 Next.js에서 국제화 혹은 다국어 지원하는 방법에 대해 알아보겠습니다.
많은 자료들을 참고하여 작성하였지만, 개인적인 생각이 포함되어 있으며 사실과 다를 수 있습니다.
일반적으로 다국어를 지원하기 위해서는 유저가 어떤 locale
의 유저인지 알 수 있어야 합니다. 유저가 브라우저를 사용하고 있는 환경이라면 Web API을 이용할 수 있습니다. navigator.language
는 유저가 사용 중인 브라우저 UI 언어를 저장하고 있으며 이는 브라우저가 직접 세팅합니다. navigation.language
를 가져오면 ko, en, en-US 등 과 같은 UTS Locale Identifiers 형식의 string 을 가져올 수 있습니다.
const locale = window.navigator.language
다만 Next.js는 기본적으로 모든 페이지를 서버 사이드 렌더링하기 때문에, 수화 이후에 locale을 가져오는 방법은 UX를 고려한다면 썩 좋은 방법은 아닙니다. 따라서 서버에서 유저가 사용 중인 언어를 알 수 있어야 합니다. Next.js는 사용자가 웹 애플리케이션의 루트에 방문했을 때 요청하는 HTTP의 Accept-Language
헤더에 기반하여 사용자의 locale을 감지합니다. 이 Accept-Language
는 navigator.language
와 마찬가지로 브라우저가 설정합니다.
Accept-Language는 브라우저의 버전 및 사용자 설정과 함께 핑거프린팅이 될 수 있기 때문에 직접 변경하는 것은 권장되지 않습니다.
lang
속성을 추가합니다.왜 Next.js는 i18n을 위해 라우팅하는 방법을 선택했을까?
사용자는 충분히 다른 언어로 애플리케이션을 이용하고 싶을 수 있습니다. 하지만 수화 이후 메모리 값을 통해 언어를 수정하는 방법은 마찬가지로 Next.js가 기본적으로 서버 사이드 렌더링을 하기 때문에 UX를 고려한다면 좋은 선택은 아닙니다. 위에서 언급한대로 HTTP 헤더에서
Accept-Language
를 직접 수정하는 방법도 권장되지 않습니다. 따라서 Next.js에서는 정적 페이지에서 i18n을 지원하는 방법(URL을 기반으로 i18n을 지원)을 따라가고 있다고 생각합니다.
핵심은 Next.js에서 i18n을 제공하기 위해서는 example.com/ko, example.com/en-US 와 같은 식으로 라우팅이 이루어져야 한다는 것입니다. 다행히도 Next.js는 v10.0.0
부터 i18n Routing을 기본적으로 제공합니다. 이를 이용하기 위해서 next.config.js
를 설정해야 합니다.
//next.config.js
module.exports = {
...
i18n: {
locales: ['en-US', 'en', 'ko'],
defaultLocale: 'en-US',
},
...
}
locales
에는 지원한 언어를 설정합니다. 언어 코드는 여기서 확인해볼 수 있습니다. 모든 언어를 지원한다면 더할 나위 없이 좋겠지만 아주 글로벌한 서비스를 운영하는 것이 아니라면 그럴만한 여유가 없을 것입니다. 따라서 기본적으로 제공할 locale을 defaultLocale
에서 설정할 수 있습니다. 다음은 위의 config 코드를 기반으로 defaultLocale을 설정하는 순서입니다.
locale
은 ko
가 됩니다.locale
은 en
이 됩니다.locale
은 defaultLocale
인 en-US
가 됩니다.이러한 특성을 고려하여 locales 와 defaultLocale을 잘 설정하는 것이 좋겠습니다. 만약 한국어와 영어만 제공할 예정이라면 영어를 defaultLocale로 설정하는 것이 글로벌한 서비스를 위해 적절할 것입니다.
Next.js 은 locale strategies 로 두가지 방법을 제공합니다.
- Sub-path Routing: example.com/ko, example.com 처럼 서브 경로로 라우팅됩니다.
- Domain Routing: example.com, example.fr 처럼 다른 도메인으로 라우팅 됩니다.
자세한 내용은 여기를 참고해주세요.
default locale 에도 sub-path를 적용하고 싶다면 Next.js의 미들웨어를 사용해야 합니다. 먼저 next.config.js
를 다음과 같이 수정해주세요.
// next.config.js
module.exports = {
i18n: {
// default는 잘못 입력한 것이 아닙니다. default를 제외하고 수정 해주세요.
locales: ['default', 'en', 'ko'],
defaultLocale: 'default',
localeDetection: false,
},
trailingSlash: true,
}
그 다음 pages
디렉토리가 존재하는 디렉토리 내에 middleware.ts 를 생성한 뒤 다음과 같이 작성해주세요.
// middleware.ts
import { NextRequest, NextResponse } from 'next/server'
const PUBLIC_FILE = /\.(.*)$/
export async function middleware(req: NextRequest) {
if (
req.nextUrl.pathname.startsWith('/_next') ||
req.nextUrl.pathname.includes('/api/') ||
PUBLIC_FILE.test(req.nextUrl.pathname)
) {
return
}
if (req.nextUrl.locale === 'default') {
// 만약 default locale을 수정하고 싶다면 여기의 `en`을 변경하세요.
const locale = req.cookies.get('NEXT_LOCALE')?.value || 'en'
return NextResponse.redirect(
new URL(`/${locale}${req.nextUrl.pathname}${req.nextUrl.search}`, req.url)
)
}
}
i18n Routing 을 위한 설정은 모두 마쳤지만 아직 다국어를 지원하는 것은 아닙니다. 애플리케이션 내의 텍스트를 추출하고 locale
에 따라 다른 언어를 제공해야 할 것입니다. 저는 next-i18next 라이브러리를 이용하였습니다.
next-i18next는 i18next 와 react-i18next 패키지를 사용합니다. i18next는 다국어 지원을 위한 훌륭한 라이브러리이며 현재도 지속적으로 커밋이 이뤄지고 있습니다. 또 다른 라이브러리들과는 다르게 서버 사이드 렌더링을 제공합니다. 저는 이러한 점 때문에 next-i18next를 선택했지만 SSR이 필요없는 서비스라면 유명한 react-intl 를 사용하여도 무방합니다.
앞서 next.config.js 에 작성하였던 i18n을 next-i18next.config.js로 옮겨줄 것입니다. 굳이 이렇게 하는 이유는 next-i18next에게 defaultLocale이나 기타 locale이 무엇인지 알려주어 서버에서 번역을 preload하기 위함입니다.
//next-i18next.config.js
module.exports = {
i18n: {
locales: ['default', 'en', 'ko'],
defaultLocale: 'default',
localeDetection: false,
},
}
//next.config.js
const { i18n } = require('./next-i18next.config')
module.exports = {
...
i18n,
...
}
appWithTranslation는 i18nextProvider
를 추가하기 위한 HOC입니다. _app
을 감싸주어야 합니다.
import { appWithTranslation } from 'next-i18next'
const MyApp = ({ Component, pageProps }) => (
<Component {...pageProps} />
)
export default appWithTranslation(MyApp)
이제 앱 내에서 번역을 위한 텍스트를 추출할 단계입니다. public 디렉토리에 아래 처럼 번역을 위한 json 파일을 생성합니다.
.
└── public
└── locales
├── en
| └── common.json
| └── header.json
| └── ...
└── ko
└── common.json
└── header.json
└── ...
다국어를 위해 텍스트를 json 에 모아두는 것은 굉장히 일반적입니다. 위의 예시에서 en
과 ko
에 각각 동일한 파일 이름, 동일한 key로 번역할 텍스트를 맵핑하여 Translation 기능을 제공할수 있습니다. 예를 들면 이런식 입니다.
// public/locales/ko.json
{
"title" : '제목입니다.'
"author": {
"name": '작성자 이름입니다.',
"email": '작성자 이메일입니다.'
}
}
// public/locales/en.json
{
"title" : "This is title."
"author": {
"name": "This is author's name.",
"email": "This is author's email ."
}
}
서버 사이드 렌더링을 지원하기 위해서는 마지막 이 단계가 필수적입니다. getStaticProps
혹은 getServerSideProps
에서 다음과 같이 입력해주세요. default locale이 en
이 아니라면 다른 것을 입력하면 됩니다.
import { serverSideTranslations } from 'next-i18next/serverSideTranslations'
export const getServerSideProps: GetServerSideProps = async (ctx) => {
return {
props: {
...생략,
...(await serverSideTranslations(ctx.locale ?? 'en', ['common', 'result-page'])),
},
};
};
이제 앱 내에 텍스트를 주입하기 위한 과정은 끝났습니다. 이제 앱 내에 텍스트를 주입해봅시다. useTranslation
은 public/locales/ 경로의 json 파일들의 텍스트를 앱에 주입하기 위한 훅입니다. 아래와 같이 사용할 수 있습니다.
// 하나의 json 에서 가져오는 경우
import { useTranslation } from 'next-i18next'
export const Footer = () => {
const { t } = useTranslation('footer')
return (
<footer>
{/* 여기서 description은 footer.json 내 key 입니다. */}
<p>{t('description')}</p>
</footer>
)
}
// 여러 json 파일을 사용할 경우
import { useTranslation } from 'next-i18next'
export const Footer = () => {
// string 대신 배열을 넣습니다.
const { t } = useTranslation(['footer', 'common'])
return (
<footer>
{/* key 앞에 `filename:` 을 추가합니다. */}
<p>{t('footer:description')}</p>
</footer>
)
}
번역할 텍스트 내에 변수가 포함되는 일은 비일비재하게 일어납니다. 예를 들면 다음과 같이 번역할 텍스트에 변수를 추가할 수 있습니다.
// json
{
"greeting": "Welcome, {{name}}!"
}
import { useTranslation } from 'next-i18next'
export const Header = () => {
const { t } = useTranslation(['header', 'common'])
const name = '전병민'
return (
<header>
<p>{t('header:greeting', {name})}</p>
</header>
)
}
그 외의 내용은 i18next 공식문서를 확인해주세요.
번역이 추가되면서 기존 UI가 변경되어야 할 수 있습니다. 시간을 선택할 수 있는 Select
를 예로들 수 있겠습니다. 한국어로 제공해야 한다면 아래의 배치가 자연스럽습니다.
[ Start Time Select ] 부터 [ End Time Select ] 까지
하지만 만약 해당 부분을 영어로 제공해야 한다면 아래와 같이 변경해야 합니다.
From [ Start Time Select ] to [ End Time Select ]
하지만 언어마다 다른 UI 구조를 변경하는 것이 과연 옳은 방법일까요? 저는 그렇게 생각하지 않습니다. 만약 영어가 아니라 프랑스어, 독일어 등 다른 언어를 추가해야 한다면? 언어를 추가할 때마다 UI 변경을 고려할 수는 없습니다. 뿐만 아니라 언어마다 UI가 달라지는 것은 통일성 측면에서도 좋지 않으며 코드의 유지보수 측면에서도 좋지 않을 것입니다. 이 문제는 다음과 같이 해결할 수 있겠습니다.
// 한국어
시간을 선택해주세요.
[ Start Time Select ] ~ [ End Time Select ]
// 영어
Please select time
[ Start Time Select ] ~ [ End Time Select ]
이처럼 언어와 상관없는 UI를 만들어 앱의 통일성을 유지하는 것이 중요할 것 같습니다.
계속 고민 중인 상태이지만 많은 개발자들은 page 단위로 json 파일을 나누는 것 같습니다.
만약 배포를 위해 Vercel을 사용한다면 다음과 같이 next-i18next.config.js
를 수정해야 합니다.
const path = require('path');
module.exports = {
i18n: {
defaultLocale: 'en',
locales: ['en', 'cs'],
// 추가된 부분
localePath: path.resolve('./public/locales')
},
};
좋은 글 정말 감사합니다.
영어권에서는 시간을 나타낼때 ~ 이 아닌 -를 사용한다고 합니다.
외국 이력서의 대부분에서 ~이 아닌 -가 주로 보이는 이유도 이러한 부분이 영향이 있다고 합니다.