1. 개요
웹 애플리케이션을 다국어로 제공하는 것은 글로벌 사용자 경험을 고려할 때 필수적인 요소다.
이번 프로젝트에서는 i18next
라이브러리를 기반으로 React 앱에서 다국어 전환 기능을 구현하였다. 사용자가 명시적으로 언어를 선택하지 않아도 기본 언어를 자동 인식하고, 사용자가 클릭 한 번으로 언어를 즉시 전환할 수 있도록 useLanguage
라는 사용자 정의 훅(Custom Hook) 을 설계하였다.
이 포스팅에서는 지난 포스팅에 이어 해당 사용자 정의 훅의 구조와 구현 방식, 그리고 실제로 어떻게 활용했는지에 대해 정리해보려고 한다.
2. 사용자 정의 훅
React에는 상태 관리와 생명주기 처리를 보다 선언적으로 구현할 수 있도록 도와주는 훅(Hook) 이라는 개념이 존재한다. useState
, useEffect
등과 같은 내장 훅 외에도, 상황에 따라 여러 훅을 조합해 사용자 정의 훅(Custom Hook) 을 만들 수 있다.
커스텀 훅은 단순히 use
로 시작하는 함수이며, 그 안에서 다른 훅들을 사용할 수 있도록 설계된 함수다. 본문에서 소개할 useLanguage
역시 이러한 커스텀 훅으로, 다국어 전환 및 번역 출력을 위해 설계되었다.
2.1. 목적
useLanguage
훅은 다음의 기능을 수행한다:
- 브라우저 및 Local Storage 로부터 초기 언어 설정 감지
- 사용자의 언어 전환 toggle 및 저장
- i18next 번역 함수
t()
,o()
래핑 제공 - 줄바꿈(
\n
) 이 포함된 텍스트 자동 렌더링
이를 통해 컴포넌트마다 언어 설정을 반복하지 않고, 전역적으로 간단히 호출할 수 있도록 한다.
3. 코드
3.1. 훅 전체 코드
"use client";
import { useState, useEffect } from "react";
import { useTranslation } from "react-i18next";
import i18n from "@/locales/lib/i18n";
import React from "react";
// 다국어 전환을 지원하는 커스텀 훅
const useLanguage = () => {
const { t: rawT } = useTranslation();
// 브라우저 또는 localStorage로부터 초기 언어 설정 가져오기
const getInitialLanguage = (): string => {
if (typeof window === "undefined") return "ko"; // SSR 환경에서는 'ko' 기본
const stored = localStorage.getItem("language");
return stored ?? "ko";
};
// 현재 언어 및 반대 언어 상태
const [language, setLanguage] = useState<string>("ko");
const [reverse, setReverse] = useState<string>("en");
// 첫 렌더링 시 초기 언어 설정 적용
useEffect(() => {
const initialLang = getInitialLanguage();
i18n.changeLanguage(initialLang); // i18n 설정 변경
setLanguage(initialLang); // 현재 언어 저장
setReverse(initialLang === "ko" ? "en" : "ko"); // 반대 언어 설정
}, []);
// 언어 전환 함수
const switchLanguage = (lng: string) => {
i18n.changeLanguage(lng); // i18n 언어 변경
setLanguage(lng); // 현재 언어 상태 업데이트
setReverse(lng === "ko" ? "en" : "ko"); // 반대 언어 상태 업데이트
localStorage.setItem("language", lng); // localStorage에 저장
};
// 언어 토글 함수 (ko <-> en)
const toggleLanguage = () => {
const newLang = language === "ko" ? "en" : "ko";
switchLanguage(newLang);
};
// 현재 언어 및 반대 언어 라벨 (UI 표시용)
const languageLabel = language === "ko" ? "Korean" : "English";
const reverseLabel = reverse === "ko" ? "Korean" : "English";
// 단순 번역 함수
const t = (key: string, options: any = {}): any => rawT(key, options);
// 객체 또는 줄바꿈 포함 문자열 번역 렌더링 함수
const o = (key: string, options: any = {}): any => {
const rawValue = rawT(key, { ...options, returnObjects: true });
// 타입 유틸
const isString = (value: unknown): value is string => typeof value === "string";
const isArray = (value: unknown): value is any[] => Array.isArray(value);
const isObject = (value: unknown): value is Record<string, unknown> =>
typeof value === "object" && value !== null && !Array.isArray(value);
// 줄바꿈 포함 텍스트 처리
const render = (text: string, i: number = 0) => {
if (!text.includes("\n")) return text;
const fragmentMapper = (line: string, j: number) => (
<React.Fragment key={`mapper-${j}`}>{line}<br /></React.Fragment>
);
const child = text.split("\n").map(fragmentMapper);
return <React.Fragment key={`render-${i}`}>{child}</React.Fragment>;
};
// 값 형태에 따라 재귀적으로 변환
const transform = (value: unknown): any => {
if (isString(value)) return render(value);
if (isArray(value)) return value.map(render);
if (isObject(value)) {
const result: Record<string, unknown> = {};
for (const [k, v] of Object.entries(value)) result[k] = transform(v);
return result;
}
return value;
};
return transform(rawValue);
};
// 외부로 상태 및 함수 반환
return {
language, // 현재 언어 (ko/en)
languageLabel, // 현재 언어 라벨
reverse, // 반대 언어 (en/ko)
reverseLabel, // 반대 언어 라벨
t, // 번역 함수 (단순)
o, // 번역 함수 (개행 처리)
switchLanguage, // 언어 설정
toggleLanguage // 언어 토글
};
};
export default useLanguage;
3.2. i18n 초기화 코드
"use client";
import i18n from "i18next";
import { initReactI18next } from "react-i18next";
import en from "@/locales/en.json";
import ko from "@/locales/ko.json";
// i18n 초기화
i18n
.use(initReactI18next)
.init({
resources: {
en: { translation: en },
ko: { translation: ko },
},
lng: "ko", // 초기 언어
fallbackLng: "en", // 대체 언어
interpolation: { escapeValue: false },
returnObjects: true, // 객체 형태로도 반환 허용
defaultNS: "translation", // 기본 네임스페이스
});
export default i18n;
주요 옵션 설명
옵션명 | 설명 |
---|---|
resources |
실제 언어 데이터(JSON)를 등록하는 객체 |
lng |
초기 언어 설정 (ko 로 시작) |
fallbackLng |
번역 실패 시 사용할 예비 언어 (en ) |
interpolation.escapeValue |
React 는 JSX에서 자동으로 XSS 방지: false |
returnObjects |
o() 와 같이 객체 반환 지원 시 true 필수 |
defaultNS |
기본 네임스페이스 ("translation" ) |
이 설정은 클라이언트 전용 환경("use client"
)에서만 사용되며, Next.js
환경에 최적화되어 있다.
4. 번역 리소스(JSON) 구조
실제 번역 문자열은 JSON 파일로 locales/
디렉토리에 저장된다.
4.1. ko.json
{
"header": {
"logo": "글로컬대학",
"login": "로그인",
"logout": "로그아웃",
"welcome": "<0>{{username}}</0>님"
},
...
}
4.2. en.json
{
"header": {
"logo": "Glocal",
"login": "Login",
"logout": "Logout",
"welcome": "Hi, <0>{{username}}</0>"
},
...
}
5. 사용 예시
5.1. 언어 전환 버튼
import { useLanguage } from "@/hooks";
const LocaleButton: React.FC = () => {
const { reverseLabel, toggleLanguage } = useLanguage();
return (
<FillButton
text={reverseLabel}
onClick={toggleLanguage}
width={80}
height={39}
/>
);
};
5.2. 번역 문자열 출력
import { useLanguage } from "@/hooks";
import { Trans } from "react-i18next";
const { o } = useLanguage();
// 단순 텍스트 번역 (줄바꿈 포함 가능)
<span>{o("header.login")}</span>
// JSX 컴포넌트 포함형 번역
<Trans
i18nKey="header.welcome"
values={{ username: member?.name }}
components={[<span key="username" />]}
/>
'Web' 카테고리의 다른 글
[Web] Responsive Web (반응형 웹) 개발하기 (0) | 2025.04.18 |
---|