타입 안전한 i18n — 번역 키를 타입으로 보장하기
타입 안전한 i18n은 번역 키를 타입으로 관리 해서, 존재하지 않는 키 사용이나 보간 변수 누락을 컴파일 타임에 잡습니다.
문제: 타입 없는 i18n
// 일반적인 i18n — 문자열 키로 접근
function t(key: string): string {
return translations[key] ?? key;
}
t('greeting'); // OK
t('greating'); // ⚠️ 오타인데 에러 없음! 런타임에 키가 그대로 표시됨
번역 파일에서 타입 추출
// translations/ko.ts
const ko = {
common: {
greeting: '안녕하세요, {{name}}님!',
confirm: '확인',
cancel: '취소',
},
auth: {
login: '로그인',
logout: '로그아웃',
welcome: '{{name}}님, 환영합니다!',
},
errors: {
notFound: '{{resource}}을(를) 찾을 수 없습니다',
serverError: '서버 오류가 발생했습니다',
},
} as const;
export type Translations = typeof ko;
export default ko;
점(.) 표기법으로 키 추출
// 중첩 객체의 모든 경로를 점 표기법으로 추출
type DotPath<T, Prefix extends string = ''> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? DotPath<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
type TranslationKey = DotPath<Translations>;
// 'common.greeting' | 'common.confirm' | 'common.cancel' |
// 'auth.login' | 'auth.logout' | 'auth.welcome' |
// 'errors.notFound' | 'errors.serverError'
타입 안전한 t 함수
// 점 경로로 값의 타입을 가져오기
type GetByPath<T, Path extends string> =
Path extends `${infer Key}.${infer Rest}`
? Key extends keyof T
? GetByPath<T[Key], Rest>
: never
: Path extends keyof T
? T[Path]
: never;
// 보간 변수 추출 ({{name}} 패턴)
type ExtractInterpolations<S extends string> =
S extends `${string}{{${infer Var}}}${infer Rest}`
? Var | ExtractInterpolations<Rest>
: never;
// 타입 안전한 t 함수
function t<K extends TranslationKey>(
key: K,
...args: ExtractInterpolations<GetByPath<Translations, K> & string> extends never
? []
: [params: Record<ExtractInterpolations<GetByPath<Translations, K> & string>, string>]
): string {
// 구현 (경로 탐색 + 보간 치환)
return '';
}
// ✅ 타입 안전한 사용
t('common.confirm'); // 보간 변수 없음 — params 불필요
t('common.greeting', { name: '홍길동' }); // name이 필수
t('errors.notFound', { resource: '사용자' }); // resource가 필수
// ❌ 컴파일 에러
// t('common.greating'); // 오타 — Error
// t('common.greeting'); // name 누락 — Error
// t('common.greeting', { nama: '홍길동' }); // 오타 — Error
next-intl / react-i18next와의 통합
// next-intl 예시
import { useTranslations } from 'next-intl';
// 네임스페이스별 타입
type Messages = typeof import('../messages/ko.json');
declare module 'next-intl' {
interface IntlMessages extends Messages {}
}
// 사용 — 타입 안전
function Component() {
const t = useTranslations('common');
return <p>{t('greeting', { name: '홍길동' })}</p>;
// ✅ 키와 보간 변수가 타입 체크됨
}
번역 키 누락 감지
// 모든 언어가 같은 키를 가지고 있는지 검증
type ValidateTranslations<
Base extends Record<string, any>,
Target extends Record<string, any>
> = {
[K in keyof Base]: K extends keyof Target
? Base[K] extends Record<string, any>
? Target[K] extends Record<string, any>
? ValidateTranslations<Base[K], Target[K]>
: never // 구조 불일치
: Target[K] extends string
? true
: never
: never; // 키 누락
};
// 한국어 기준으로 영어 번역 검증
const en = {
common: {
greeting: 'Hello, {{name}}!',
confirm: 'Confirm',
cancel: 'Cancel',
},
auth: {
login: 'Login',
logout: 'Logout',
welcome: 'Welcome, {{name}}!',
},
errors: {
notFound: '{{resource}} not found',
serverError: 'Server error occurred',
},
} as const satisfies Record<keyof Translations, any>;
satisfies로 영어 번역이 한국어와 같은 구조를 가지는지 검증합니다.
정리
- 번역 파일에
as const를 붙이면 리터럴 타입으로 키를 추출할 수 있다 - Template Literal Types로
{{name}}패턴의 보간 변수를 자동 추출한다 - 점 표기법 경로 타입으로 중첩된 번역 키를 안전하게 접근한다
satisfies로 여러 언어의 번역 구조 일치를 컴파일 타임에 검증한다- next-intl, react-i18next 등의 라이브러리와 통합할 수 있다
댓글 로딩 중...