Frontend
해외파트너센터 개발 오픈기- 3.글로벌 대응을 위한 다국어 처리 작업
이은지(Jonny) / 파트너웹개발팀여기어때
2024년 12월 5일
원문에서 보기 ↗안녕하세요. 저는 프론트개발실 파트너 웹 개발팀에서 B2B 서비스 개발을 하고 있는 죠니입니다.
해외 파트너 센터를 개발한 지도 벌써 세 달이 지났습니다. (세월이여) 그동안 기존 파트너 센터에 해외 서비스를 추가하면서 쌓여 있던 레거시 코드들을 틈틈이 리팩토링하고 있습니다. “왜 이렇게 작성했지?”라며 스스로 반성하기도 하고 “그때는 어쩔 수 없었다!” (당당하게)라는 변명을 하기도 하며 해외 파트너 센터를 개발하던 순간들을 되돌아보게 되었는데요. 이 글을 통해 해외 파트너 센터 개발 과정에서 마주했던 어려움과 앞으로의 개선점을 조금 더 구체적으로 나누고자 합니다.
특히 이번에는 글로벌 서비스라면 필수적인 요소인 다국어 처리 작업에 대한 경험을 공유하겠습니다.
다국어 처리의 필요성과 최적의 라이브러리 선정
이번 프로젝트에서는 아래와 같은 이유로 다국어 처리가 필요했습니다.
- 기존 파트너 센터 레포에서 국내와 국외 작업을 모두 처리해야 함 새로 레포를 구성할 시간이 부족해 기존 레포에서 국내외 작업을 함께 처리하는 방안을 선택했습니다.
- 국내/해외 사용자에게 동일한 메뉴와 기능 제공 프로젝트 초반엔 국내외 사용자에게 동일하게 제공해야 했지만, 프로젝트가 진행되면서 일부 메뉴와 기능은 차이가 발생했습니다.
- 기존에 공통으로 사용하는 레이아웃과 디자인 시스템 컴포넌트 활용 페이지 개발 시 기존 공통 레이아웃과 디자인 시스템 컴포넌트를 그대로 사용해야 했습니다.
저는 세 가지 주요 조건을 모두 충족하는 라이브러리를 찾아야 했기 때문에 팀 동료들의 추천에 따라 먼저 i18n 라이브러리를 검토했습니다. 이 라이브러리가 각 조건에 대해 제공하는 기능과 한계를 명확하게 파악할 수 있었고 이를 기반으로 다른 라이브러리들과의 비교에 소요되는 시간을 크게 줄일 수 있었습니다. 결과적으로 비교적 짧은 시간 내에 프로젝트에 가장 적합한 라이브러리를 선택할 수 있었습니다.
React-Intl, Lingui.js 등 다양한 인기 라이브러리가 있었지만 최종적으로i18next 와 react-i18next 조합을 선택하게 되었는데요. 간단히 특징과 함께 선택한 이유를 설명드리겠습니다.

두 라이브러리의 특징만 보아도 상호 보완적으로 작동한다는 것을 알 수 있는데요. 이 조합을 사용하면 커스터마이징이 비교적 자유로워 파트너센터에서 해외 서비스를 어떻게 다룰지 불확실한 상황에서도 유연하게 대응할 수 있을 것이라 판단했습니다.

각 라이브러리 사용 추이
결정적으로 팀원들이 이미 사용해 본 익숙한 라이브러리였다는 점 과 React 애플리케이션에서 다국어 지원으로 가장 많이 사용되고 있다는 점이 최종 선택에 큰 영향을 주었습니다.
앞으로 우리의 해외 서비스는 더욱 확장될 것이고 새로운 동료들도 계속 합류할 예정입니다. 그렇기 때문에 누가 오더라도 서비스를 지속적으로 유지보수할 수 있도록 구축해 두는 것이 중요합니다. 예를 들어 사용하던 라이브러리가 갑자기 업데이트를 중단하거나 주요 정보에 접근하기 어려워진다면 큰 어려움에 처할 수 있습니다. 그래서 우리는 향후에도 지속적으로 업데이트되고, 많은 정보를 쉽게 접근할 수 있는 라이브러리를 사용하는 것이 적합하다고 생각했습니다.
자 그럼 라이브러리도 찾았다 ! 자, 그럼 간단한 세팅 과정에 대해 설명드리겠습니다.
react-i18next 라이브러리 초기 세팅 과정
저희는 react-i18next 공식 문서(https://react.i18next.com/)에 따라 초기 세팅을 진행했는데요. 문서가 명확하고 상세하게 설명되어 있기 때문에 제 코드는 참고만 하시고, 실제 적용은 공식 홈페이지를 따라 하시는 것을 추천드립니다!
1. 패키지 설치
yarn add i18next react-i18next i18next-browser-languagedetector
2. 실행 파일 작성
✅ 언어 감지 방식 설정
i18next의 언어 감지(Language Detection) 기능을 확장하여 도메인에 따라 자동으로 언어를 설정해줬습니다.
import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';
import LanguageDetector from 'i18next-browser-languagedetector';
// i18next에서 언어를 자동으로 감지하기 위해 작성
const languageDetector = new LanguageDetector();
// 언어 감지 방식을 커스텀 하기 위한 객체
// lookup 메서드 기능: 도메인 이름에 따라 언어를 결정
const domainDetector = {
name: 'domain',
lookup(options) {
let locale = 'ko';
if (typeof window !== 'undefined') {
locale = window?.location.hostname.indexOf('oversea') > -1 ? 'en' : 'ko';
}
return locale;
},
};
// 위에서 커스텀 감지 방식을 i18next에 추가
languageDetector.addDetector(domainDetector);
실행 파일 중 특히 아래 코드가 왜 필요한지 궁금하신 분들이 있을 것 같습니다.
if (typeof window !== 'undefined') {
locale = window?.location.hostname.indexOf('oversea') > -1 ? 'en' : 'ko';
}
return locale;
파트너 센터는 Next.js를 사용하고 있기 때문에 window 객체는 클라이언트 측에서만 접근이 가능합니다. 즉 서버에서 실행되는 코드에서 window 객체에 접근하려고 하면 Next.js는 이를 처리할 수 없어 에러를 발생시킵니다. 이 때문에 Next.js에서 window 객체를 사용할 때는 서버에서 실행되는 동안 window 객체에 접근하지 않도록 조건을 만들어줘야 하는데요.
if (typeof window !== 'undefined'){...} 코드는 바로 이러한 조건을 만들어 주는 역할을 합니다!
위 코드의 흐름을 다시 정리하자면, 서버 사이드 렌더링이 진행될 때는 window 객체에 접근하지 않고 기본값인 locale을 사용하며 클라이언트에서 실행될 때는 window.location.hostname에 접근하여 조건에 따라 locale 값을 설정하게 됩니다.
✅ i18next , react-i18next 기본 설정
init내 옵션은 상황에 맞춰 자유롭게 사용하시면 됩니다.
i18n
// 유저 환경에 따라 언어를 자동으로 감지하는 역할.
.use(languageDetector)
// i18n을 react-i18next로 전달.
.use(initReactI18next)
// i18next 초기화 설정.
.init({
resources,
debug: isDevelop, // 로드가 작동 하지 않는 문제를 찾는데 도움을 줌(기본값: false)
fallbackLng: 'ko', // 사용자의 언어로 번역을 지원 하지 않을 경우 대체할 언어
...
});
export default i18n;
resources 객체는 번역 데이터를 저장하는 곳 입니다. 각 언어별로 번역된 문자열을 정의하고 활성화된 언어에 따라 동적으로 언어를 변경합니다. 아래 코드를 보시면 알겠지만 resources 객체는 언어별로 분리된 형태의 구조로 관리되기 때문에 언어가 추가되더라도 쉽게 확장할 수 있습니다.
import en from './locales/en';
..
..
const resources = {
en: {
translation: en,
},
ko: {
translation: ko,
},
// 추가되는 언어가 있다면 차례로
};
resource 객체 구조에 대해 좀 더 자세하게 살펴보자면,
- 언어 코드: 각 언어는 en, ko처럼 언어 코드로 구분됩니다. 각 언어 코드는 ISO 639–1 표준에 따라 설정하시면 됩니다.
- translation 객체: 번역 키와 해당하는 번역된 값이 포함됩니다. 예를 들어
import한en을 살펴보면 다음과 같습니다.
// en/index.js
const en = {
// 페이지 별로 키값을 관리합니다.
…home,
…login,
}
// login.json
{
// translation 객체: 번역키 : 해당하는 번역된 값
“t_login_id”: “Enter your ID”
}
3. 최상단에서 I18nextProvider를 통해 전역에서 사용하도록 설정
import { I18nextProvider } from 'react-i18next';
import i18n from '@/i18n';
function PartnerService({ Component, pageProps }) {
return (
<I18nextProvider i18n={i18n}>
...
</I18nextProvider>
);
}
export default PartnerService;
여기까지 했다면 다 한 것과 다름이 없습니다. 적용은 아래 코드와 같습니다.
import { useTranslation } from 'react-i18next';
function Home() {
const { t } = useTranslation();
return (
<Wrapper>
{t('home.home_desc')}
</Wrapper>
);
}
export default Home;
추가로 i18next의 t 함수는 번역 키와 함께 변수를 처리할 수 있습니다. 이를 통해 동적으로 변수를 삽입하고 적용할 수 있는데요! 예를 들어 {count}라는 변수를 사용해 ‘n 개 선택됨’ 이라는 문장을 번역한다면 다음과 같이 작성할 수 있습니다.
t('reservation_selected_count', { count: 5 }); // "5개 선택됨"
이렇게 적용하면 끝날 거 같았지만 한 가지 문제가 남아있었습니다.
바로 home.home_desc 와 같은 번역 key 값을 어떻게 관리할 것인가.
적용 과정에 나온 어려움
라이브러리를 적용할 때 JSON 관리 방식은 서버 사용 여부에 따라 결정됩니다.

💁🏽♀️ 서버를 이용하는 경우
- 구글 시트에서 JSON 관리: 구글 시트에 데이터 관리.
- Google Sheets API 사용: Google Sheets API를 통해 구글 시트에서 JSON 데이터를 서버로 가져오기.
- 서버에서 i18n 파일로 저장: 서버는 가져온 JSON 데이터를 i18n 형식으로 변환하여 저장 및 적용
장점
- 구글 시트에서 수정된 데이터를 서버에서 실시간으로 업데이트할 수 있어 유지 보수가 용이합니다.
단점
- 서버 개발 리소스가 필요하며 추가적인 관리 비용이 발생할 수 있습니다.
- 서버와의 통신 지연으로 인해 데이터 로드가 느려질 수 있습니다.
💁🏽♀️ 서버를 이용하지 않는 경우
- 구글 시트에서 JSON 관리: 구글 시트에 데이터 관리.
- JSON 데이터를 저장해두기: 수동으로 구글 시트의 데이터를 가져와 파일로 저장.
- 리액트에서 i18n 설정: 미리 저장해둔 JSON 데이터를 리액트 애플리케이션에 직접 적용하여 다국어 설정을 진행
장점
- 서버 개발 없이 빠르게 적용할 수 있어 개발 속도가 빠릅니다.
단점
- 번역 데이터가 변경될 때마다 수동으로 업데이트해야 하는 불편함이 있습니다.
- 실시간 데이터 업데이트가 불가능하므로 관리 효율성이 떨어질 수 있습니다.
플로우를 보시면 아시겠지만 각 방식마다 뚜렷한 장점이 있습니다. 그러나 장기적인 프로젝트 관리 측면에서는 서버를 통해 JSON을 관리하는 것이 더 효율적이라고 판단하여 서버 기반 관리 방식을 선택하려 했었는데요. 하지만 프로젝트를 진행하다 보면 늘 최상의 조건으로 개발할 수 없다는 걸 아실 겁니다. 당시 여러 프로젝트가 동시에 진행되고 있었기 때문에 서버 리소스를 할당받을 수 없는 상황이었고, 결국 서버 개발 없이 프론트엔드에서 직접 JSON을 관리하는 방식으로 진행하게 되었습니다.
이론적으로 최상은 아니더라도 주어진 상황에서 최선의 조건을 찾는 것이 실무자들의 일 ! 😄
💻 개발 과정
1. 구글 시트에 PO분들과 페이지 별로 수기로 작성하기

구글 시트 예시
모든 페이지 key 값은 통일성 있게 규칙을 잡기로 했습니다. key 값을 엉망으로 작성하면 추후 코드를 보면서 이게 뭐지? 여긴 뭐지?의 수렁으로 빠지기 쉽기 때문입니다.
2. 1번에서 픽스된 key값은 i18n 폴더에 en / ko 나눠서 작성
// i18n > locales > en
{
"t_login_id": "Enter your ID",
"t_login_pw": "Enter your Password",
...
}
// i18n > locales > ko
{
"t_login_id": "아이디",
"t_login_pw": "비밀번호",
...
}
i18next에서 JSON 파일 불러와서 translate할 때 locale 값으로 판단하기 때문에 공통으로 사용하는 컴포넌트나 향후 다국어 확장 가능성을 고려하여 ko와 en으로 나눠 JSON 파일을 관리하였습니다.
3. 적용 후 화면까지 테스트

해외 파트너센터 로그인 화면
const { t } = useTranslation();
const LOGIN_FORM = [
{
...
placeholder: t('t_login_id'),
...
},
...
]
Json 파일을 수동으로 관리하기 위해선 모두의 꼼꼼함이 발휘되어야 하는데요. 3단계를 거치면서 혹시라도 있을 휴먼에러를 방지하기 위해 그 어느 때보다 많은 테스트 과정을 거친 것 같습니다. 업데이트 상황이 있을 때마다 직접 처리해야 하는 아쉬움은 있었지만, 부족한 리소스를 가지고 주어진 기간 동안 개발하기엔 이 방식이 가장 편리했습니다.
그리고 PO분들이 열심히 서포트해 주신 덕분에 무사히 기간 안에 적용할 수 있었어요 ! 모두 감사합니다.
앞으로의 과제. . TODO

백로그에 정착된 티켓
백로그에 박혀있는 이 티켓은 저의 마음속 짐이 되어있습니다. 언젠가 꼭 해야 할 일이라고 생각하는데요. . 🙂 앞으로 해외 파트너 센터에서 제공하는 페이지는 더 많아질 것이고 지금보다 더 복잡한 요구사항이 늘어날 것이라고 생각합니다. 그럴 때마다 수기로 JSON 관리를 하는 것은 결국 리소스 낭비로 이어질 수 있기 때문에 새로운 프로젝트가 시작되기 전에 정리하고 싶은 TODO 입니다.
제가 준비한 내용은 여기까지입니다. 해당 포스트가 다국어 처리를 관리하는데 작게나마 도움이 됐길 바랍니다.
저는 TODO를 DONE으로 만들 때 다시 돌아오겠습니다!
해외 파트너 센터 많이 사랑해 주세요.