i18n과 moment.js 로케일 동기화

Dam·7일 전

[Project]

목록 보기
8/8
post-thumbnail

4개 언어를 지원하는 예약 사이트를 개발하던 중, 언어를 바꿔도 날짜의 요일 표기만 그대로 남는 이슈를 발견했다. 원인은 i18nmoment.js가 서로 다른 로케일 체계를 사용한다는 점이었다. 두 라이브러리를 언어 변경 이벤트로 동기화하고, 중국어 weekday는 moment 기본 표기(周X)와 디자인 시안(星期X)이 달라 디자이너와 협의를 거쳐 정자형으로 통일한 과정을 정리한다.


1. 문제 인식

사이트 상단의 언어 선택을 한국어에서 영어로 바꾸자 메뉴와 본문 텍스트는 영어로 잘 바뀌었지만, 예약 캘린더의 요일 표기만은 여전히 한국어("일 / 월 / 화 / 수 / 목 / 금 / 토")로 남아있었다.

처음에는 번역 키가 빠진 줄 알고 번역 파일을 확인했다. 그런데 해당 텍스트는 번역 키 자체가 아니었다. 코드를 따라가 보니 그 자리에는 moment.js로 포맷팅한 결과가 들어가고 있었다.

moment(date).format('dddd'); 
// → '월요일' (언어를 영어로 바꿔도 계속 한국어)

호출부에서는 어떤 언어 관련 인자도 전달하고 있지 않았다. 그렇다면 출력 결과를 결정하는 것은 moment 내부의 로케일 상태일 수밖에 없었다. 콘솔에서 moment.locale()을 확인해 보니, 언어를 영어로 바꿔도 값은 계속 'ko'로 남아 있었다. i18n 쪽 언어를 바꿔도 moment의 로케일은 그대로라는 의미였다. 그제야 두 라이브러리가 서로의 로케일을 공유하지 않는다는 점을 알게 됐다.

그렇다면 각 라이브러리가 왜 자기만의 로케일을 갖고 있는지부터 짚어볼 필요가 있었다.


2. 원인 분석

대부분의 i18n 라이브러리(i18next, react-intl 등)는 번역 텍스트만 관리한다. t('greeting')'안녕하세요'로 매핑되는 키-값 구조가 핵심이다.

반면 moment.js(혹은 date-fns, dayjs)는 날짜와 시간 포맷팅을 위한 자체 로케일을 별도로 갖는다. 월 이름, 요일 이름, 시간 형식("오전 9시" vs "9 AM"), 상대 시간 표현("3일 전") 같은 정보가 여기에 들어있다.

// i18n: 텍스트 키-값 매핑
i18n.t('greeting') // → '안녕하세요'

// moment: 날짜 포맷팅에 자체 로케일 사용
moment().format('LL') // → '2025년 11월 11일' (한국어 로케일일 때)

즉, i18n.changeLanguage('en')을 호출해도 moment의 로케일은 바뀌지 않는다. 그래서 번역 텍스트는 전환되지만 날짜 표기는 한국어로 남는, 절반만 동작하는 상태가 된다.

같은 문제는 moment뿐 아니라 브라우저 내장 Intl.NumberFormat, Intl.DateTimeFormat, 통화·숫자 포맷팅 라이브러리 전반에 동일하게 적용된다. 각 라이브러리는 자체 로케일을 가지고 있고, 명시적으로 동기화하지 않으면 따로 움직인다.

결국 두 로케일을 명시적으로 묶어주는 작업이 필요했다.


3. 1차 해결 — 언어 변경 이벤트로 두 로케일 연결

i18next는 언어가 바뀔 때마다 languageChanged 이벤트를 발화한다. 이 이벤트에 moment.locale() 호출을 연결하면 두 로케일이 함께 움직인다.

import i18n from 'i18next';
import moment from 'moment';

// 언어 변경 이벤트 → moment 로케일 동기화
i18n.on('languageChanged', (lng) => {
  moment.locale(lng);
});

한 가지 주의할 점이 있다. moment는 기본적으로 영어 로케일만 들고 있다. 다른 언어를 사용하려면 명시적으로 import해서 등록해야 한다.

import 'moment/locale/ja';     // 일본어
import 'moment/locale/zh-cn';  // 중국어
// 한국어는 'moment/locale/ko'

import만 해두면 moment가 내부적으로 해당 로케일을 등록한다. 등록되지 않은 로케일을 moment.locale('xx')로 호출하면 오류 없이 영어로 폴백되기 때문에, 누락된 import는 런타임에 발견하기 어렵다.

이 설정까지 적용하면 한국어 / 영어 / 일본어 간 전환은 의도대로 동작했다. 다만 중국어 화면에서는 또 다른 이슈가 남아 있었다.


4. 추가 이슈 — 중국어 weekday 표기 차이

QA를 돌리던 중, 중국어 화면의 요일 표기가 디자인 시안과 다른 것을 발견했다.

확인해 보니 momentzh-cn 기본 로케일은 요일을 다음과 같이 출력하고 있었다.

周日 周一 周二 周三 周四 周五 周六

반면 디자인 시안은 정자형이었다.

星期日 星期一 星期二 星期三 星期四 星期五 星期六

두 표기 모두 문법적으로 올바른 중국어다. 차이는 다음과 같다.

표기특성보통 쓰이는 곳
周X간결, 일상모바일 UI / 캘린더 앱 / 일상 회화
星期X격식, 정자공식 문서 / 문어체 / 전통 콘텐츠

라이브러리 기본값(周X)이 틀린 표기는 아니었기 때문에 처음에는 그대로 두는 것도 고려했다. 다만 디자이너가 정자형을 의도해서 선택했을 가능성이 있어 의도를 확인해 보았는데, 한옥 예약 서비스 특유의 전통적이고 격식 있는 톤을 살리기 위해 의도적으로 정자형을 선택한 것이었다.


5. 2차 해결 — moment.updateLocale로 weekday 덮어쓰기

요일 표기만 바꾸기 위해 moment.updateLocale을 사용했다.

const updateMomentLocale = (lng: string) => {
  if (lng === 'zh') {
    // 중국어는 디자인 시안에 맞춰 weekday 커스텀
    moment.updateLocale('zh-cn', {
      weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
    });
  } else {
    moment.locale(lng);
  }
};

여기서 moment.localemoment.updateLocale은 동작이 다르다.

  • moment.locale('zh-cn') — 어떤 로케일을 사용할지 선택한다. 기존 로케일을 그대로 사용한다.
  • moment.updateLocale('zh-cn', { ... }) — 기존 로케일의 일부 설정만 덮어쓴다. 명시한 필드(weekdays)만 바뀌고, 나머지 설정(월 이름, 시간 표기, 상대 시간 등)은 기본값을 유지한다.

updateLocale을 사용하면 weekdays만 정자형으로 바꾸고, 월 이름(一月 / 二月 / ...)이나 시간 표기는 기본 zh-cn 값을 그대로 사용할 수 있다.


6. 최종 코드

앞서 만든 모든 처리를 합치면 다음과 같이 정리된다.

import i18n from 'i18next';
import moment from 'moment';
import 'moment/locale/ja';
import 'moment/locale/zh-cn';

// i18n 언어 → moment 로케일 동기화 함수
const updateMomentLocale = (lng: string) => {
  if (lng === 'zh') {
    // 중국어는 디자인 시안에 맞춰 weekday 커스텀
    moment.updateLocale('zh-cn', {
      weekdays: ['星期日', '星期一', '星期二', '星期三', '星期四', '星期五', '星期六'],
    });
  } else {
    moment.locale(lng);
  }
};

// 1) 페이지 진입 시 1회 동기화
updateMomentLocale(i18n.language);

// 2) 이후 언어 변경마다 동기화
i18n.on('languageChanged', updateMomentLocale);

15줄 남짓의 코드지만, 절반만 동작하던 다국어를 완성된 상태로 만들어 준다.


7. 정리

기술적으로 어려운 작업은 아니었다. momenti18next의 문서를 차근차근 따라가다 보면 패턴이 어렵지 않게 보이는 수준이었다. 다만 이 과정에서 정리해 둘 만한 두 가지가 있었다.

1) 라이브러리마다 로케일은 별도다

i18n 라이브러리가 번역 텍스트의 로케일을 관리한다고 해서, 날짜·통화·숫자 같은 다른 영역의 로케일까지 함께 챙겨주는 것은 아니다. 라이브러리마다 자체 로케일이 따로 있고, 이를 묶어주는 처리가 없으면 동기화되지 않는다.

다국어 사이트를 만들 때는 지금 이 데이터가 어느 로케일을 보고 있는지 항상 점검할 필요가 있다. moment뿐 아니라 날짜·시간·통화·숫자를 다루는 모든 영역이 잠재적인 동기화 포인트다.

2) 라이브러리 기본값과 디자인 의도는 다를 수 있다

momentzh-cn 기본 표기가 틀린 것은 아니었다. 단지 디자인 의도와 달랐을 뿐이다. 코드가 동작하는 것과 의도대로 동작하는 것은 다른 문제다. 그리고 그 의도는 코드만 들여다봐서는 알 수 없는 경우가 많다.

디자이너에게 의도를 확인한 덕분에 서비스 톤의 일관성을 가져갈 수 있었다.


QA에서 마주친 작은 어색함을 지나치지 않은 것, 그리고 라이브러리 기본값을 그대로 받아들이지 않고 한 번 더 확인한 것 — 결국 이 두 가지가 서비스의 디테일을 만들어 준 과정이었다.

profile
🌐 DOM 위에서 살아남기

0개의 댓글