개발/Next.js

en_US인가 en-US인가 그것이 문제로다(feat. i18n)

핸디(Handy) 2024. 8. 27. 21:55

들어가며

이번 글에서는 다국어 지원을 도입한 프로젝트에서 겪은 표준 에러에 대해 설명한다. 이 글은 에러의 식별, 원인 분석, 그리고 문제 해결 과정을 다루며, 동일한 문제를 겪을 수 있는 개발자들에게 유용한 정보를 제공한다.

에러 식별

9월을 앞두고 이번 분기에 진행한 개발 작업을 정리하던 중, Sentry에서 특정 에러가 발생한 것을 확인했다. 해당 에러는 다국어 지원을 위해 사용한 locale 파일과 관련이 있었다.

우선 일차적으로 locale 파일을 가져와서 json 형태로 만들고 이를 타입을 추론하여 사용하기 때문에 만약 없는 locale key라면 빌드단계에서 막혀야한다. (프로젝트 빌드시에 타입체크를 하도록 강제해두었기 때문에)

원인분석1. locale key가 없다?

그런데도 에러가 발생했고 소스맵을 확인해보니 아래 코드에서 발생하고 있었다. 그리고 더 찾아보니 비슷한 에러가 여럿 보였다.

export const DocsPrivacyButton = () => {
  const t = useTranslations("common");
  return (
    <Button
      variant="link"
      className="p-0 text-xs text-primary underline md:text-sm"
    >
      <LocaleLink href={RouterPath.docsPrivacy}>
        {t("privacy_policy", {
          ns: "common",
        })}
      </LocaleLink>
    </Button>
  );
};

이상했다. 코드를 보면 아무런 이상이 없었기 때문이다. privacy_policy 키가 없는 것도 아니었다.

그리고 없는 키로 요청을 하면 에러가 뜬다. 

그러니 locale key가 없어서 발생한 문제는 아니었다.

원인분석2.  dynamic values문제

다시 sentry 로그를 확인해보았다. 자세히 확인해보니 dynamic values를 가진 텍스트에서만 문제가 발생한 것을 확인하였다.

여기서 말하는 동적값이란 로케일문서에 변수를 추가하고 변수를 통해 자바스크립트상에서 넣는 값을 의미한다.

특히나 번역을 함에 있어선 이런 동적값은 거의 필수적이다.
예를 들어 하나의 문장을 두개의 언어로 번역하면 다음과 같다.
한국어 : 1000원 입니다.
영어 : The price is 1000.

이때 1000이란 숫자는 자바스크립트에서 정하는 건데 만약 동적인 값이 없다면 아래처럼 번역을 준비하고
"message" : "원 입니다."
"messgae" : "The price is"
실제 랜더링하는 부분에서 분기를 타야한다.

if(locale == "ko") return price + message;
if(locale == "en") return message + price;

이건 다행히 순서만 바꾸는거지만 실제론 이렇게 간단한 문자열 순서바꾸는것으로 해결되지 않는다. 따라서 동적값으로
"message_ko" : {price}원 입니다.
"message_en" : The price is {price}
이렇게 만들어두고 사용하면 되는 것이다.

이제 동적값이 무엇인지 알았는데.. 그래서 뭐가 문제라는 것이냐 여기서부터 이제 힘겨워지기 시작했다.

문제가 발생한 locale은 중국어에서 발생했다. 프로젝트에서 지원하고 있는 언어는 "en", "ko"만 지원했다가 "de", "es", "fr", "it", "ja", "nl", "pt", "zh_cn", "zh_tw" 를 추가했다. 그리고 문제는 "zh_cn", "zh_tw"  경우에서만 발생했다.

그렇다 동적인 값이 문제면 모든것이 안되야하는데 "zh_cn", "zh_tw"  언어일때 그리고 동적인 값인 경우에만 안되는 거였다. 이제 문제는 동적인 값이라기보다는 "zh-cn", "zh-tw" 이렇게 언더바(_)이 있는 언어가 문제일 거라 생각했다.

원인분석3.  locale의 언더바문제

바로 사용하고 있는 라이브러리의 공식문서와 타입을 찾아봤는데, 어디에도 내용을 찾아볼수 없었다. 

타입은 간단하게 string으로 되어있었다. 그래서 라이브러리 next-intl를 clone해와서 같은 코드부터 따라가기 시작했다.

정확히 문서를 다시 정의해보자면 2개의 상황이 중첩되어야 하는 것을 확인했다.

  1. 언더바을 가진 locale인 경우
  2. 동적인 값인 경우

다행히 에러가 난 코드를 쉽게 찾을 수 있었다.

대략 라이브러리의 translateFn, translateBaseFn함수를 찾아보면 될 것 같았다.

그리고 next-intl/packages/use-intl/src/core/createBaseTranslator.tsx 에서

시작부분은 218줄부터 시작되고 초반에 얼리리턴패턴으로 에러를 반환하고 273부터 시작이다. 

평문일때는 해당 로직을 타고 값을 리턴해준다. 동적값이 없는 메시지의 경우 제대로 동작했는데 이 로직에서 return 되서 그런듯싶었다.

그리고 그 뒤에 새로운 단어들이 등장하기 시작한다. formatter가 나왔다.

그리고 이때부터 loacles가 등장하는것을 봤을때 여기다 싶었다. 그리고 주석에서 formatjs를 사용한다고 한다. 그리고 얼마후에 원인을 찾았다.

원인 발견

일반적으로 locale은 규약이 있다. 당연한 일이다. 한국이 영어로 korea라고 해서 일반적으로 쓰이는 ko와 kr 대신에 ka로 쓴다고 하면 바로 혼날것이다.

그리고 해당 문서에도 힌트가 있었다.

valid한 unicode locale tag가 필요하다고 한다. 그리고 예시로는 "en"과 "en-GB" 즉 하이픈이 있는 것이 옳은 것이었다.

곧장 formatjs를 설치하고 테스트를 진행해봣다.

  const cache = createIntlCache();

  const intl = createIntl(
    {
      locale: "zh-tw",
      messages: {},
    },
    cache
  );

  console.log(locale, intl.formatNumber(20)); // 정상동작

그리고 다행히? 언더바인 경우 에러가 발생했다.

  const cache = createIntlCache();

  const intl = createIntl(
    {
      locale: "zh_tw",
      messages: {},
    },
    cache
  );

  console.log(locale, intl.formatNumber(20)); // 에러발생

 

en_US인가 en-US인가 그것이 문제로다

이런건 챗형보다는 스택오버플로어에 계신 형님들의 인사이트가 훌륭하다.

 

en_US or en-US, which one should you use?

Assume you want to store the locale of user preference in database, which value you will use? en_US or en-US They are two standards, but which one you prefer to use as part of your own applicatio...

stackoverflow.com

그리고 형님들이 말씀하시길 하이픈이 근본이라고 하셨다. 더이상 반론은 없다. 반론을 하면 당신이 틀린거다.

비추받아서 아래로 하락한 답변이다.

이제 문제를 해결하러 가보자.

문제 해결

해결방법은 간단하다. valid locale tag를 사용하면 된다.

그리고 실제로 코드를 작성하고 나서는 하이픈을 사용하여 제대로 사용하고 있었다. 하지만 웹서비스를 만들고나서 앱을 만들게 되었고 앱(플러터)에서 처리를 하려다보니 하이픈(-)보다는 언더바(_)가 편해서 바꿔줄수 있냐는 앱팀의 문의에 당연히 of coruse를 외치고 빠르게 커밋한 나의 잘못이었다.

이제 내일 출근해서 싸워야한다. 내가 이기면 하이픈을 써서 고민없이 해결하면 되는것이고, 내가 만약 진다면 로케일문서를 받아와서 언더바로 되어있는 키를 하이픈으로 변경해야한다.

(실제로는 그렇게 큰 고민은 아니다. 어차피 자동화 스크립트를 만들어놔서 각 json 파일을 만들고 마지막에 언더바의 모든 키를 하이픈으로 변경하는 코드만 추가하면 되는 상황이다)

마무리

모든 기능에는 표준이 있으며, 이를 준수해야 문제를 피할 수 있다. 이 프로젝트에서는 JSON -> TypeScript로 1차 체크, 빌드 시 타입 체크로 2차 체크를 통해 문제를 예방하고자 했으나, 결국 에러가 발생하고 말았다. 다행히 Sentry를 통해 문제를 빠르게 발견할 수 있었고, 이를 통해 중요한 교훈을 얻을 수 있었다.