블로그를 준비하고 있어요
잠시만 기다려주세요...
Nextjs에 다국어 설정하기
내 블로그에 대한 유입을 늘리기 위해 어떻게 해야 할까 고민하다가 다국어 설정을 해보기로 했다.
다양한 방법을 찾아 보았는데, 일단 첫걸음은 언어에 대해 다른 라우팅을 하는 것.
next-intl을 이용해 해보기로 했다.
1. 설치
yarn add next-intl
2. 적용
적용이 다소 까다롭다.
대충 아래의 순서로 진행하면 된다.
app 내부의 폴더 라우팅 전체를 [locale] 폴더 내에 넣음.
i18n 폴더를 만들고 내부에 config.ts, request.ts, ko.json, en.json 파일 만들고 설정하기
middleware.ts, next.config.ts에서 설정
layout에서 params와 provider로 children 감싸주기
컴포넌트에서 메세지 불러오기
3. 실행해보기
1) [locale] 폴더
여기서 app 하위에서 api, uploads를 제외한 모든 폴더들을 [locale]로 감싸주려고 한다.
.next가 있는 상태에서 폴더를 이동하면 권한 오류가 뜨기에, sudo rm -r .next 로 제거 후 폴더를 옮겨줌.
주의할 점은, 폴더 구조가 바뀌었기에 상대 경로로 호출하는게 있다면 바꿔줘야 한다.
2) i18n 폴더 만들기
src 폴더가 있는 분들은 src 하위에, 그냥 app만 쓰는 분들은 루트폴더에 그냥 만들어도 상관없다.
나는 i18n 폴더에 config.ts, request.ts를 만들고, message 폴더 안에 ko.json, en.json을 만들어 주었다.
그리고 config.ts는 아래와 같이 설정한다.
나는 아무것도 없는 기본 경로 "/"를 한국어로 설정해주었다.
export const locales = ['ko', 'en'] as const;
export type Locale = (typeof locales)[number];
export const defaultLocale: Locale = 'ko';
export const pathnames = {
'/': '/',
'/about': {
ko: '/about',
en: '/about',
},
'/post/[slug]': {
ko: '/post/[slug]',
en: '/post/[slug]',
},
} as const;
그리고 request.ts에서는 아래와 같이 설정한다.
locale 정보를 호출해 서버에 돌려주는 방식이다.
import { getRequestConfig } from 'next-intl/server';
import { defaultLocale } from './config';
export default getRequestConfig(async ({ requestLocale }: { requestLocale: Promise<string | undefined> }) => {
const locale = await requestLocale || defaultLocale;
return {
locale,
messages: (await import(`./message/${locale}.json`)).default,
};
});
3) middleware.ts, next.config.ts 설정하기
먼저 middleware.ts는 아래와 같이 설정해준다.
그냥 위쪽에 createMiddleware를 export로 추가해준다고 생각하면 쉽다.
import createMiddleware from 'next-intl/middleware';
import { locales, defaultLocale } from './i18n/config';
export createMiddleware({
locales,
defaultLocale,
localePrefix: 'as-needed', // 기본(ko)은 경로에 prefix 없음
});
//...기존 설정들...//
export default withAuth(
function middleware(req) {
//...기존 설정들...//
});
그리고 next.config.ts에도 아래와 같이 createNextIntlPlugin을 만들어 준뒤, 기존 설정들을 한번 감싸준다.
import type { NextConfig } from "next";
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin()
const withNextIntlConfig: NextConfig = withNextIntl({
기존 next.config 설정들...
});
export default withNextIntlConfig;
4) 레이아웃 감싸주기
이제 레이아웃에서 getMessage로 locale을 받아온 뒤, provider로 감싸 하위 컴포넌트에서 이용할 수 있도록 하면 된다.
import { NextIntlClientProvider } from "next-intl";
import { getMessages, getLocale } from "next-intl/server";
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
const messages = await getMessages();
const locale = await getLocale();
return (
<html
lang={locale}
>
<NextIntlClientProvider locale={locale} messages={messages}>
<body>
{children}
</body>
</NextIntlClientProvider>
</html>
);
}
5) 메세지 써보기
먼저 서버측에서 랜더링할 때와 클라이언트 측에서 랜더링할 때 함수명이 다르다.
import {useTranslations, useLocale, useFormatter} from 'next-intl'; // 'use client' 컴포넌트에서 호출
import {getTranslations, getLocale, getFormatter} from 'next-intl/server'; // server 컴포넌트에서 호출
구분 | 클라이언트 컴포넌트 | 서버 컴포넌트 / 서버 함수 | 설명 |
|---|
Translations | useTranslations(namespace?)
| getTranslations(namespace?)
| ko.json, en.json 등의 번역 메시지를 가져와서 t('key') 형태로 사용
|
Locale | useLocale()
| getLocale()
| 현재 활성화된 로케일(/en, /ko, …)을 문자열로 반환 |
Formatter | useFormatter()
| getFormatter()
| 날짜, 시간, 숫자 등을 로케일에 맞게 포맷팅 |
Messages | — | getMessages()
| 현재 요청의 전체 번역 메시지 객체(보통 layout에서 Provider용으로 사용) |
Request Locale 설정 | — | unstable_setRequestLocale(locale)
| SSG·SSR 캐싱 시 로케일 정합성을 보장할 때 layout에서 사용 |
현재 나의 /about 컴포넌트는 아래와 같이 하드코딩 된 상태기 대문에 바꾸기 좋아 보였다.
import {
HeroSection,
JourneySection,
ProjectsSection,
} from "@/components/about";
const baseUrl = process.env.NEXT_PUBLIC_URL||"";
export default function AboutPage() {
...
const heroData = {
...
};
const journeyData = {
title: "My Journey",
items: [
.....
]
};
const projectsData = {
title: "Featured Projects",
projects: [
{
title: "대입 통계분석",
...
},
...
],
};
return (
<div className="min-h-screen bg-gray-900 ">
<main className="rounded-lg container relative mx-auto scroll-my-12 overflow-auto">
{/* Language Selector */}
<HeroSection {...heroData} />
<JourneySection {...journeyData} />
<ProjectsSection {...projectsData} />
</main>
</div>
);
}
이제 ko.json과 en.json을 설정해주고 컴포넌트는 getTranslations로 텍스트를 넣어주면 된다.
그런데 문제는 getTranslations로 불러온건 string 구조로만 나온다는 것.
export default async function AboutPage() {
...
const t = await getTranslations("about");
const heroData = JSON.parse(JSON.stringify(t("hero")));
const journeyData = JSON.parse(JSON.stringify(t("journey")));
const projectsData = JSON.parse(JSON.stringify(t("projects")));
...
}
그렇게 해서 하위 컴포넌트에 전부 프롭으로 넘겨주었다.
4. 후기
이때까지 영어 블로그를 손으로 전부 해야 한다고만 생각했는데, 이제 AI도 있고 자동화된 툴들이 많아 쉽게 가능할 것 같다.
남은건 블로그의 글들을 전부 번역해서 db에 집어 넣는 일.
그리고 db에 locale을 추가하고, locale로 검색해서 post를 반환하면 될 것이다.
댓글을 불러오는 중...