dohy-eon
토스 오픈소스에 PR이 머지되기까지
← 블로그

토스 오픈소스에 PR이 머지되기까지

2026.10.154분 읽기Open Source

토스에서 만든 한글 처리 라이브러리 es-hangul에 기능을 제안하고, 직접 구현한 PR이 머지되었습니다. 이슈를 올리고 머지되기까지의 과정을 정리해봅니다.

문제: 숫자와 영문이 사라진다

es-hangul의 getChoseong은 문자열에서 초성을 뽑아주는 함수입니다.

getChoseong('사과'); // 'ㅅㄱ'
getChoseong('띄어 쓰기'); // 'ㄸㅇ ㅆㄱ'

초성 검색이나 자동완성 필터를 만들 때 자주 쓰이는데, 한 가지 아쉬운 점이 있었습니다. 한글 초성과 공백을 제외한 모든 문자를 제거한다는 점입니다.

getChoseong('네이버123'); // 'ㄴㅇㅂ'
getChoseong('IP 주소'); // ' ㅈㅅ'

네이버123의 123, IP 주소의 IP처럼 의미 있는 정보가 함께 사라집니다. 검색 UI에서는 한글만 초성으로 바꾸고 숫자나 영문은 그대로 두고 싶은 경우가 많은데, 지금은 이를 위해 사용하는 쪽에서 별도의 전처리를 해야 했습니다.

이슈 올리기

먼저 #381 이슈로 한글 외 문자를 유지할지 선택하는 옵션을 제안했습니다.

알고 보니 같은 요구가 이미 #309에 올라와 있었습니다. 2024년 말에 올라온 이슈로, 초성 검색을 구현하다가 IP 주소가 ㅈㅅ이 되는 문제를 겪었다는 내용이었습니다. 이 이슈에서는 다음처럼 세분화된 옵션이 제안되어 있었습니다.

getChoseong('abc가나다!@#', { allowAlphabets: true }); // 'abcㄱㄴㄷ'
getChoseong('abc가나다!@#', { allowSymbols: true }); // 'ㄱㄴㄷ!@#'

저는 이보다 단순하게, 한글이 아닌 문자는 전부 유지하는 단일 옵션 keepNonHangul로 방향을 잡았습니다. 초성 검색에서 영문만 남기고 기호는 지우는 식의 세밀한 제어가 필요한 경우는 많지 않다고 봤고, 옵션이 늘어날수록 조합에 따른 동작을 설명하기도 어려워지기 때문입니다.

구현: NFD로 분해한 뒤 중성·종성만 지우기

기존 getChoseong의 동작 원리는 유니코드 정규화(NFD)입니다. normalize('NFD')를 거치면 완성형 한글 한 글자가 초성·중성·종성 자모로 분해됩니다.

'각'.normalize('NFD'); // 'ᄀ' + 'ᅡ' + 'ᆨ' (U+1100, U+1161, U+11A8)

기존 코드는 분해된 문자열에서 초성 자모·호환 자모·공백을 제외한 나머지를 전부 삭제한 뒤, 남은 초성 자모(U+1100~)를 우리가 흔히 쓰는 호환 자모(ㄱ, ㄴ …)로 바꾸는 방식이었습니다.

그래서 접근을 반대로 뒤집었습니다. "초성만 남기기" 대신 "중성과 종성만 지우기" 입니다. 이렇게 하면 숫자, 영문, 기호는 정규식에 걸리지 않으니 자연스럽게 그대로 남습니다.

export function getChoseong(word: string, options: { keepNonHangul?: boolean } = {}) {
  const { keepNonHangul = false } = options;
 
  if (keepNonHangul) {
    return word
      .normalize('NFD')
      .replace(REMOVE_NFD_JUNG_JONG_REGEX, '') // NFD 중성·종성만 제거
      .replace(CHOOSE_NFD_CHOSEONG_REGEX, $0 => CHOSEONGS[$0.charCodeAt(0) - 0x1100]);
  }
 
  // 기존 동작
  // ...
}

중성·종성을 지우는 정규식은 라이브러리에 이미 정의된 자모 범위 상수(JASO_HANGUL_NFD)를 그대로 활용했습니다.

const REMOVE_NFD_JUNG_JONG_REGEX = new RegExp(
  `[\\u${JASO_HANGUL_NFD.START_JUNGSEONG.toString(16)}-\\u${JASO_HANGUL_NFD.END_JUNGSEONG.toString(16)}` +
    `\\u${JASO_HANGUL_NFD.START_JONGSEONG.toString(16)}-\\u${JASO_HANGUL_NFD.END_JONGSEONG.toString(16)}]+`,
  'ug'
);

기본값은 false로 두어 기존 사용자에게는 아무 영향이 없도록 했습니다. 그 밖에 함께 챙긴 것들은 다음과 같습니다.

리뷰와 머지

PR(#382)은 3월 25일에 올렸고, 7월 9일에 머지되었습니다. 3개월 넘게 기다린 셈입니다.

메인테이너분께서 리뷰가 늦어진 것에 대해 사과와 함께, #309에서 제안된 세분화 옵션과 단일 옵션 사이의 고민도 살펴봤다고 남겨주셨습니다. 결론은 대부분의 유스케이스를 단순하게 커버하는 단일 keepNonHangul이 낫다는 것이었고, 두 이슈의 요구사항을 모두 만족한다는 것도 확인해주셨습니다.

getChoseong('네이버123', { keepNonHangul: true }); // 'ㄴㅇㅂ123'  (#381)
getChoseong('IP 주소', { keepNonHangul: true }); // 'IP ㅈㅅ'    (#309)

머지와 함께 #309, #381 두 이슈가 닫혔고, 다음 버전 릴리스에 포함되었습니다.

돌아보며

작은 변경이지만, 평소 쓰던 라이브러리에 직접 기여해볼 수 있어서 의미 있는 경험이었습니다.