국내 서비스라고 하더라도 글로벌 사용자를 고려해 다국어 지원을 지원하고 있습니다. 이번 프로젝트에서는 `next-intl`을 사용해 간단하고 일관성 있는 다국어 처리 구조를 만들었고, 그 구조와 사용 패턴에 대해 소개해보려고 합니다.
next-intl 사용 이유 ✨
서버와 클라이언트에서 일관된 처리
`next-intl` 은 Next.js의 서버 사이드 렌더링(SSR)과 클라이언트 사이드 렌더링(CSR) 모두에서 일관된 방식으로 데이터를 처리할 수 있습니다.
자동 로딩 및 타입 지원
다국어 메시지 파일을 자동으로 로드하고, Typescript와의 호환성을 제공하여 타입 안정성을 보장해 주기 때문에 실수를 줄이고 편리하게 작업할 수 있습니다.
locale 기반 포맷팅 지원
숫자, 날짜, 시간 등 다양한 포맷을 locale에 맞게 자동으로 처리할 수 있어 포맷팅 로직을 신경 쓸 필요 없이 간편하게 구현할 수 있습니다.
폴더 구조 📁
/messages
├── en.json // 영어 번역
├── ko.json // 한국어 번역
└── en.d.json.ts // 타입 선언 (optional)
/i18n
├── request.ts // API 요청과 관련된 다국어 로직
└── type.d.ts // 다국어 타입 정의
/next.config.mjs
messages 폴더
// messages > en.json
{
"OnboardingMsg": {
"slide1-title": "Welcome to KINS!",
"slide1-subTitle": "KINS is the largest job opening/job search service in Korea"
},
"FacebookLogin": "Continue with Facebook",
...
}
// messages > ko.json
{
"OnboardingMsg": {
"slide1-title": "KINS에 오신걸 환영해요!",
"slide1-subTitle": "KINS는 국내 최대 Nanny 구인/구직 서비스에요."
},
"FacebookLogin": "Facebook으로 계속하기",
...
}
// messages > en.d.json.ts
declare const messages: {
"OnboardingMsg": {
"slide1-title": "Welcome to KINS!",
"slide1-subTitle": "KINS is the largest job opening/job search service in Korea",
},
"FacebookLogin": "Continue with Facebook",
"TabLabel": {
"Home": "Home",
"Recruiting": "Recruiting",
"JobSearch": "JobSearch",
"Chat": "Chat",
"Mypage": "Mypage"
},
}
타입 안정성을 위해 `en.d.json.ts` 파일을 두고 `messages` 객채의 타입을 정의했습니다.
request.ts 파일
`request.ts` 파일은 서버에서 다국어 메시지를 로드하고 포맷팅 규칙을 설정하는 파일입니다.
`getRequestConfig`와 `Formats`를 활용해 서버사이드 렌더링에서 사용할 locale 및 메시지를 설정했습니다.
import type { Formats } from 'next-intl';
import { cookies } from 'next/headers';
import { getRequestConfig } from 'next-intl/server';
import { EnumCookieKeys } from '@src/types/EnumCookieKeys';
export const formats = {
number: {
precise: {
maximumFractionDigits: 5
}
},
list: {
enumeration: {
style: 'long',
type: 'conjunction'
}
},
dateTime: {
short: {
day: 'numeric',
month: 'short',
year: 'numeric'
},
ampm: {
// ex) 오전 9:30 | PM 6:20
hour12: true,
hour: 'numeric',
minute: 'numeric'
},
withDay: {
// ex) 2025년 0월 00일 0요일 | Wednesday, April 2, 2025
month: 'long',
day: 'numeric',
weekday: 'long',
year: 'numeric'
},
reportDate: {
// ex) 4/2/2025, 3:48 PM
hour12: false,
day: 'numeric',
hour: 'numeric',
year: 'numeric',
month: 'numeric',
minute: 'numeric'
}
}
} satisfies Formats;
export default getRequestConfig(async () => {
// Provide a static locale, fetch a user setting,
// read from `cookies()`, `headers()`, etc.
// const headersList = headers();
const cookieStore = cookies();
const locale = (cookieStore.get(EnumCookieKeys.locale)?.value as KINS_LOCALE) || 'en';
return {
locale,
formats,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
export type KINS_LOCALE = 'ko' | 'en';
formats 설정
formats 객체는 날짜, 시간, 숫자 등의 포맷 규칙을 통해 locale을 정의합니다.
💡 `short`, `ampm`, `withDay` 형식으로 다양하게 설정할 수 있습니다.
dateTime: {
short: {
day: 'numeric',
month: 'short',
year: 'numeric'
},
ampm: {
hour12: true,
hour: 'numeric',
minute: 'numeric'
},
withDay: {
month: 'long',
day: 'numeric',
weekday: 'long',
year: 'numeric'
},
}
getRequestConfig 설정
서버에서 쿠키에 있는 현재 locale을 결정하고 해당 locale에 맞게 메시지를 동적으로 로드합니다.
이를 통해 locale에 맞는 메시지를 `messages` 폴더에서 불러와 반환합니다.
// i18n/request.ts
export default getRequestConfig(async () => {
const cookieStore = cookies();
const locale = (cookieStore.get(EnumCookieKeys.locale)?.value as KINS_LOCALE) || 'en';
return {
locale,
formats,
messages: (await import(`../../messages/${locale}.json`)).default
};
});
type.d.ts 파일
`type.d.ts`파일은 next-intl의 타입을 확장하는 역할을 합니다.
import messages from '@src/../messages/en.json';
import { formats } from './request';
declare module 'next-intl' {
interface AppConfig {
Locale: 'ko' | 'en';
Formats: typeof formats;
Messages: typeof messages;
}
}
`AppConfig` 인터페이스를 통해 `Locale`, `Formats`, `Messages` 에 대한 타입을 정의 및 안정성을 확보하고 다국어 메시지를 안전하게 사용할 수 있도록 합니다.
next.config.mjs 설정
import createNextIntlPlugin from 'next-intl/plugin';
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: './messages/en.json'
}
});
/**
* @type {import('next').NextConfig}
*/
const nextConfig = {
experimental: {
appDir: true
},
reactStrictMode: false,
...
};
export default withNextIntl(nextConfig);
experimental.createMessagesDeclaration 설정
다국어 메시지 선언 파일을 자동으로 생성하는 기능을 제공합니다.
const withNextIntl = createNextIntlPlugin({
experimental: {
createMessagesDeclaration: './messages/en.json'
}
});
아래와 같은 형식으로 자동 생성됩니다.
// messages > en.d.json.ts
declare const messages: {
"OnboardingMsg": {
"slide1-title": "Welcome to KINS!",
"slide1-subTitle": "KINS is the largest job opening/job search service in Korea",
"slide2-title": "Various Recruitment Announcements",
"slide2-subTitle": "KINS offers a variety of custom job openings/job openings",
"slide3-title": "Easy and Easy to Write a Resume",
"slide3-subTitle": "KINS' resume is easy and easy to write",
"slide4-title": "It's easy to chat",
"slide4-subTitle": "Let's find a job/job easily through chatting"
},
"FacebookLogin": "Continue with Facebook",
"TabLabel": {
"Home": "Home",
"Recruiting": "Recruiting",
"JobSearch": "JobSearch",
"Chat": "Chat",
"Mypage": "Mypage"
},
}
사용법 📖
useTranslations 훅
next-inlt의 `useTranslations` 훅을 사용하면 메시지를 안전하게 가져올 수 있습니다.
import { useTranslations } from 'next-intl';
const t = useTranslations();
t('OnboardingMsg.slide1-title'); // Welcome to KINS!
컴포넌트 내부에서 각 메시지 키를 직접 호출하며 사용되고, Chat, jobSearch, Mypage 등 각 도메인별로 네이밍을 구분해 관리했습니다.
locale 전환
언어 전환 시 페이지 전체를 새로고침 하지 않고 `useLocale` 훅으로 현재 언어를 확인하는 버튼 UI를 구성했습니다.
const HomeMainSection = () => {
const t = useTranslations();
const locale = useLocale(); // ✅ 현재 언어 확인
return (
<div className="overflow-y-auto scrollbar-hide">
<Header
leftIcon={<ICON_MAP.KinsLogo />}
rightIcon={
<NormalButton
variant="outline"
size="thin"
className="aspect-square rounded-full"
onClick={() => {
changeLocale(); // ✅ 현재 언어 변경
}}
>
{locale === 'en' ? 'EN' : 'KO'}
</NormalButton>
}
/>
...
</div>
);
};
`locale`전환은 `changeLocale()` 함수를 따로 유틸로 분리해서 처리했습니다.
export const changeLocale = async () => {
const cookieStore = await cookies();
const currentLocale = (cookieStore.get(EnumCookieKeys.locale)?.value as KINS_LOCALE) || 'en';
const newLocale = currentLocale === 'en' ? 'ko' : 'en';
cookieStore.set(EnumCookieKeys.locale, newLocale);
return newLocale;
};
ChatRoom에 적용
`useChatRoomSettings` 훅 안에서도 다국어 처리를 활용했습니다.
const t = useTranslations();
const language = useLocale();
const format = useFormatter();
const jobTypeText =
currentRoom?.post_type === 'seeker'
? t('ChatRoom.jobTypeText.seeker')
: t('ChatRoom.jobTypeText.employer');
또한, 날짜도 아래와 같이 다국어로 포맷팅 해서 사용할 수 있습니다.
const formattedDate = format.dateTime(new Date(curr.created_at), 'withDay')
withDay: {
// ex) 2025년 0월 00일 0요일 | Wednesday, April 2, 2025
month: 'long',
day: 'numeric',
weekday: 'long',
year: 'numeric'
},
테스트 화면 📖
회고 🧐
첫 번째 목차
이번 `next-intl`을 적용하면서 가장 크게 느낀점은
서버와 클라이언트 모두 자연스럽게 locale을 처리할 수 있도록 설계하는 과정을 경험해 보면서
타입 안정성과, 포맷 관리, 쿠키 기반의 locale 추적 등 재사용 가능한 형태로 확장할 수 있다는 점에서 의미가 있었던 작업이었습니다.
앞으로 더 다양한 언어를 지원할 수 있도록 확장할 계획입니다.

아직 많이 부족하기 때문에 조언은 언제나 환영입니다 :)
출처 🏷️
'공부 > Next' 카테고리의 다른 글
[성능 최적화] Next.js HydrationBoundary를 활용한 서버-클라이언트 데이터 최적화 (0) | 2024.11.16 |
---|---|
Supabase에서 타입 설정하기 (0) | 2024.11.11 |
[리팩토링] queries 폴더 구조 변경 및 메서드명 수정 (1) | 2024.11.07 |
[리팩토링] Icon 컴포넌트 리팩토링 (0) | 2024.11.02 |
Next 커스텀 훅을 사용한 모달창 기능 구현 (1) | 2024.11.01 |
[리팩토링] 데이터 로직 분리 및 서버 상태 관리 최적화 (0) | 2024.10.28 |
Next 카카오맵을 활용한 시도별 가로 스크롤 버튼 이동 구현 (0) | 2024.10.28 |
[리팩토링] Next 카카오 맵 폴리곤 렌더링 리팩토링 - 커스텀 훅, 유틸 함수 구조 개선 (0) | 2024.10.24 |