← 블로그 목록
2026-08-22ossi18nlocale-matchko

같은 버그를 두 번 고쳤다면 그건 라이브러리다

AskLinq 위젯은 13개 언어를 지원합니다. 그런데 어느 날 이런 보고를 받았습니다.

"홍콩말 대만말이지 본토는 절대 아니야. 그런데 본토말이 지원되는 것 같은데?"

확인해 보니 맞았습니다. 우리 중국어는 번체 전용(홍콩·대만)인데, 중국 본토 브라우저(zh-CN)에도 번체 문구가 나가고 있었습니다. 간체를 쓰는 사람에게 번체를 주는 것은 "지원"이 아니라 잘못된 글자를 주는 것입니다.

첫 번째 수정 — 그리고 첫 번째 착각

원인은 단순해 보였습니다. 매칭 로직이 zh-CN을 기본형 zh로 접었고, zh는 우리 번체 문구 세트의 키였습니다. 그래서 zh-CN을 명시적으로 거부하도록 고쳤습니다.

배포했고, 프로덕션에서 다시 확인했습니다. 여전히 번체가 나갔습니다.

두 번째 버그 — 순위 목록의 구제 함정

본토 브라우저는 Accept-Language를 이렇게 보냅니다.

Accept-Language: zh-CN,zh;q=0.9

우리 매처는 목록을 순서대로 시도합니다. zh-CN은 방금 만든 규칙이 거부했지만 — 바로 뒤의 맨 zh가 매칭을 구제했습니다. 거부당한 정확한 태그를, 같은 사람이 보낸 더 모호한 태그가 되살린 겁니다.

여기서 얻은 교훈이 이 라이브러리의 첫 번째 설계 규칙이 됐습니다.

순위 목록에서는 모호한 항목이, 방금 거부한 정확한 항목을 살려낸다.

두 층 규칙

최종 규칙은 입력의 모양으로 가릅니다.

  • 선언된 단일 값 (?lang= 파라미터, 쿠키, 테넌트 설정): 문자 체계를 명시하지 않아도 통과시킵니다. 맨 zh는 우리 번체 세트의 내부 키이기도 해서, 여기서 거부하면 자기 자신을 거부하게 됩니다.
  • 순위 목록의 항목 (Accept-Language): 번체를 명시해야만 통과합니다. zh-HK, zh-TW, zh-Hant는 통과하고, zh-CN과 맨 zh는 통과하지 못합니다.

같은 태그라도 어디에 적혀 있었는지에 따라 다르게 읽는 것 — 이것이 locale-match의 핵심 추상화입니다.

가드는 추측하지 않는다

"이 언어는 문자 체계를 명시해야 한다"는 가드를 개발자가 손으로 쓰게 하면 같은 실수가 반복됩니다. 그래서 가드는 서비스가 지원한다고 선언한 로케일 목록에서 자동으로 유도됩니다. 방문자가 무엇을 원할지 추측하는 게 아니라, 자기가 지원한다고 적어 둔 목록을 읽는 것입니다.

RFC를 일부러 어기는 부분

RFC 4647의 lookup 알고리즘은 pt-PT(포르투갈) 방문자에게 pt-BR(브라질)만 있는 사이트라면 매칭 실패를 돌려줍니다. locale-match는 이 "옆걸음"을 일부러 합니다 — 포르투갈 사람에게 브라질 포르투갈어는 영어보다 훨씬 낫기 때문입니다.

그런데 바로 그 관대함이 중국어에서는 틀립니다. 간체 사용자에게 번체는 "조금 다른 방언"이 아닙니다. 옆걸음의 유용함과 중국어 가드는 서로 반대말이 아니라 한 설계의 양면입니다: 기본은 관대하게, 문자 체계가 갈라지는 언어만 명시를 요구하게.

라이브러리가 된 이유

이 버그를 고치고 보니, 같은 로케일 협상 코드가 우리 제품 곳곳에 각자의 버전으로 살고 있었습니다. 가장 잘 고쳐진 버전은 한 곳에 갇혀 있었고, 가장 미숙한 버전이 프로덕션에서 실제 방문자에게 잘못된 글자를 주고 있었습니다.

같은 버그를 두 번 고쳤다면 그건 라이브러리 신호입니다. 그래서 @devslab/locale-match로 추출했고, 지금은 세 서비스가 같은 구현을 소비합니다. 문서 사이트의 플레이그라운드는 실제 배포된 패키지를 로드해서 돌아가고, 그 페이지 자신의 언어도 이 라이브러리가 고릅니다 — 데모가 구현과 어긋날 수 없게.