Top 4 Contributor가 말해주는 오픈소스(es-hangul)기여 경험

Top 4 Contributor가 말해주는 오픈소스(es-hangul)기여 경험

오픈소스 기여를 통해 성장하자

소개

안녕하세요! 프론트엔드 개발자 김동규입니다. 저는 평소에 오픈소스를 구경하거나 기여하는 것을 즐겨하는데요. 오늘은 제가 JavaScript 기반의 국내 유일 한글 라이브러리 'es-hangul'에 기여했던 이야기를 풀어보고자 해요. 하나의 이슈를 담당하여 PR부터 Merge까지의 과정들을 공유해보고자 해요.

es-hangul에 기여하게 된 계기

2024년 4월. 지인으로부터 한글을 다루는 오픈소스가 공개되었다는 소식을 접했어요. 그때까지만 해도, 다양한 오픈소스에 기여를 해왔지만, 한글과 관련된 오픈소스는 단 한 번도 없었어요. 많은 기대감을 안고 오픈소스를 훑어봤어요. 제공되는 기능이 적었지만 매우 흥미가 생겼었어요. 그리고 "한국인으로서 한글 라이브러리에 기여하지 않는다면 누가 할 수 있을까?"라는 생각을 가졌었어요.

저는 오픈소스 기여를 할 때는 항상 같은 흐름을 가져가는데요. 먼저 Contributing 가이드를 읽어봅니다. 그리고 package.json을 훑어보고 main source code를 파악해요. 그런 다음에는 issue, pull request 탭을 순서대로 읽어보는 편이에요.

가장 오래된 Issue가 눈에 띄었어요. 지금은 Closed 된 상태지만 당시에는 Open 이었어요.

es-hangul issue

한국어를 입력받으면 로마자로 변경하여 반환해 주는 기능을 가진 함수에 대한 이슈 레이징이었는데요. 4월 12일 올라왔지만, 5월이 다 지나도록 아무도 기능 구현에 시도하지 않았었어요. 보통 이런 경우는 기능 구현이 어렵거나 관심을 가질만한 이슈가 아니거나 둘 중의 하나라고 생각해요. 하지만 저는 그런거는 잘 신경쓰지 않아요. 일단 부딪쳐봅니다.

해당 기능 구현에 대해 흥미를 느끼기도 했고 예상 사용 사례를 보고 나서 기여해야겠다고 마음을 먹었어요.

🤔
이렇게 유용한 기능을 내가 담당하면 얼마나 많은 분들께 도움이 되는 걸까?

저는 항상 제가 작성한 코드가 다른 개발자분들께 조금이라도 도움이 되면 좋겠다는 생각을 가지고 오픈소스 기여에 임해요. 작지만 저에게는 정말 소중한 마음가짐이에요.

2024년 6월 1일.

해당 기능 구현을 위해 힘차게 달리기 시작합니다.

오픈소스 기여 허락

그럼 어떻게 구현할거야?

처음에는 코드 오너분의 레퍼런스 제공으로 국립국어원의 자료를 살펴봤어요. 하지만 제공해 주신 자료만으로는 다양한 한글의 발음에 대응하는 로직을 구현하는 것이 쉽지 않다는 것을 알게 됐어요. 이유는 다음과 같아요.

한글을 로마자로 변경하는 작업은 단순히 한글의 자음과 모음(이하 자모)에 로마자를 대입하여 해결할 수 없었어요. 먼저, 한글에 '표준 발음법'을 적용하여 표준 발음을 가진 한글이 필요했어요.

표준 발음법은 표준어의 실제 발음을 따르되, 국어의 전통성과 합리성을 고려하여 정함을 원칙으로 한다. - 표준어 규정 제2부 표준 발음법 -

받침에 따라 발음이 달라질 수도 있고, 음의 동화 현상에 의해 받침이 사라질 수도 있어요. 또는 경음화 현상에 의해 된소리로 발음해야 하는 경우도 고려해야 해요. 이외에도 고려해야 할 조건이 너무 많았어요.

그래서 해당 이슈를 해결하기 위해 '한글에 표준 발음법을 적용하는 함수'와 '한글을 로마자로 변경하는 함수' 2가지 기능을 구현해야 한다고 판단했어요.

한글에 표준 발음법을 적용해보자

이 기능을 구현하면서, 태어나서 처음으로 '한국어 어문 규범'을 보게 되었어요. 문화체육관광부 고시 제2017-13호 표준어 규정집을 제1부 1장부터 제2부 7장 30항까지가 표준 발음법 기능을 구현하기 위해 필요한 범위예요.

한국어 어문 규범

처음에는 자음과 모음을 상수화했어요. 그리고 표준 발음법 적용을 위한 조건들을 코드로 반영하면서 굉장히 어렵고 복잡하다는 것을 느꼈어요.

복잡하다고 느낀 이유

12항

제12항을 적용하기 위해서는 주요 조건 1개와 부가 조건 4개가 붙습니다. 그런데 다른 조항들도 12항과 마찬가지로 대부분 부가 조건을 가지고 있어요. 조건문을 어떤 식으로 작성해야 코드를 읽을 때 편할지에 대한 고민을 가장 많이 했었어요. 실제로 코드 리뷰를 받았을 때도 해당 부분에 대한 논의가 나와 수많은 리팩토링을 거쳤어요.

그리고 실질 형태소와 형식 형태소까지 구분해야 하는 경우도 있었어요.

실질 형태소

실질 형태소와 형식 형태소의 구분은 형태소 분석기 없이는 100% 구분할 수가 없는 한글 단위에요. 그래서 완벽하게 기능을 제공하지 못하는 조항도 있어요. 그래도 최대한 적용할 수 있는 만큼은 노력했어요.

어렵다고 느낀 이유

그럼, 제가 어렵다고 느낀 점은 무엇일까요?

바로 조항의 순서입니다.

한국어 어문 규범에서는 1항부터 30항까지 차례대로 알려주고 있지만 실제로 코드에서는 순서대로 적용하면 안 된다는 것을 개발 도중에 알게 되었어요.

예를 들어, '밟는'이라는 한글이 있어요. 만약 1항부터 30항까지의 순서로 로직을 적용하면 '밟는'은 '발른'이 돼요. 하지만 이것은 틀려요. 우리들은 '밤는'이라고 발음하거든요.

이유가 무엇일까요?

제10항. 겹받침 ‘ㄳ’, ‘ㄵ’, ‘ㄼ, ㄽ, ㄾ’, ‘ㅄ’은 어말 또는 자음 앞에서 각각 [ㄱ, ㄴ, ㄹ, ㅂ]으로 발음한다.

제18항. 받침 ‘ㄱ(ㄲ, ㅋ, ㄳ, ㄺ), ㄷ(ㅅ, ㅆ, ㅈ, ㅊ, ㅌ, ㅎ), ㅂ(ㅍ, ㄼ, ㄿ, ㅄ)’은 ‘ㄴ, ㅁ’ 앞에서 [ㅇ, ㄴ, ㅁ]으로 발음한다.

'밟는'이라는 한글은 18항이 적용되어서 '밟'이 '밤'이 되어야 하는데 10항이 먼저 적용된 탓에 '밟'이 '발'이 되었어요. 이때는 너무 당황스러웠어요. 국가에서 제공해 주는 그 어떤 규정집에서도 표준어 발음 적용을 위해 적용되어야 하는 표준어 규정 순서를 확인할 수 없었거든요.

그래서 조항들을 코드로 옮기면서 올바른 순서를 위해 일일이 확인해봤어요. 이때 정말 많은 도움이 됐던 게 테스트 코드에요. Vitest 기반의 단위 테스트 코드로 미리 작성해 놨었는데, 조항의 적용 순서를 바꿀 때마다 기능이 정상적으로 동작하는지 쉽게 알 수 있어서 정말 편리했어요.

한국어 어문 규범에 제공하는 조항 중 형태소와 관련된 조항들을 제외하고 다음과 같은 순서를 따라가야 표준 발음법이 올바르게 적용된다는 것을 알게 되었어요.

경음화 > 16항 > 17항 > 19항 > 동화작용 > 18항 > 20항 > 12항 > 13,14항 > 9,10,11항

순서를 적용할 때, 스누메뉴부산대 로마자 변환기도 큰 도움이 되었습니다. 감사합니다!

고려했던 점

각 조항 중에 딱 경음화만 적용되고 동화작용만 적용되는 조항이 없어서, 경음화와 동화작용은 일반 조항들과는 무관하게 따로 적용해야 했어요. 여기서 신경 썼던 점은 경음화를 통해 '된소리'가 적용되는 부분을 옵셔널하게 선택할 수 있도록 했어요.

function standardizePronunciation(
  // 한글 문자열을 입력합니다.
  hangul: string,
  options: {
    // 경음화 등의 된소리를 적용할지 여부를 설정합니다. 기본값은 true입니다.
    hardConversion: boolean;
  } = { hardConversion: true }
): string;

그 이유는 한글을 로마자로 변경할 때는 된소리가 적용되지 않아야 하기 때문이에요. 그리고 로마자로 변경할 때 말고도 사용자들이 표준 발음법 함수를 사용할 수도 있으니, 옵셔널로 값을 넘길 수 있도록 선택했던 게 두 번째 이유기도 해요.

된소리되기는 적용하지 않는다

로직이 상당히 복잡하고 코드 길이가 꽤 길어서 코드 분리, 선언형 프로그래밍, 변수 이름을 한글로 짓기, 꼼꼼한 테스트 케이스 작성에 최대한 공들였어요.

standardizePronunciation 에 대한 단위 테스트는 70+개에요. 테스트 케이스를 만들 때 시간이 꽤 걸렸었는데 완성하고 엄청 뿌듯했어요.

테스트 커버리지

한글을 로마자로 바꿔보자!

이 기능은 앞에서 만들었던 '한글을 표준 발음법으로 변경하는 함수'보다는 비교적 쉽게 구현했어요. 표준 발음법으로 반환받은 한글에 대조되는 영문으로 치환해 주면 끝이에요. 해당 기능은 문화체육관광부 고시 제2014-42호를 참고하여 구현되었어요.

사용 방법은 다음과 같아요.

function romanize(hangul: string): string;

고려했던 점

첫번째, 변수명을 고려했어요.

저는 어려운 한글 단어를 영어로 번역해서 사용할 때마다 어색함과 불편함을 느꼈어요. 이러한 경우에는 그냥 한글로 이름을 지어서 사용하는 편이에요. 아래는 제가 romanize 함수를 만들 때 사용했던 상수명이에요.

export const 종성_알파벳_발음 = {
  ㄱ: 'k',
  ㄴ: 'n',
  ㄷ: 't',
  ㄹ: 'l',
  ㅁ: 'm',
  ㅂ: 'p',
  ㅇ: 'ng',
  '': '',
} as const;

JONGSUNG_ALPHABET_PRONUNCIATION 영문으로 바꿔봤어요. 어떤 게 가독성이 좋고 이해하기 편하신가요? 매우 상대적이지만 저는 한글이 훨씬 편하다고 느껴요. 제가 말씀드리고 싶은 건, 영문 변수명에 너무 함몰되어서 억지로 더욱 길고 어렵게 만들 필요는 없다는 것 이에요.

두 번째, 테스트 케이스를 최대한 꼼꼼히 작성하였어요.

romanize에 대한 단위 테스트를 하기 위해 작성한 테스트 케이스는 총 10개에요.

테스트 커버리지는 물론 100%입니다.

테스트 커버리지

덕분에 코드 오너분께 칭찬을 받아서 기분이 매우 좋았었어요.

테스트 케이스

한국어 어문 규범은 완벽하지 않다

전문 용어가 부족하다

제가 standardizePronunciationromanize 함수를 만들면서 느낀 점은 한국어 어문 규범이 표준 발음법을 위한 모든 제반 사항을 마련하지 않은 것 같았어요. 사실 국립국어원에서 제공하는 정보만으로는 제가 만들고자 하는 기능의 완성도를 높이는 데는 한계가 있었어요.

"ㅏ", "ㅓ", "ㅗ", "ㅜ" 등을 구성하는 데 필요한 "∙"는 뭐라고 불러야 하는 걸까요? 한국학중앙연구원에서 제공하는 한국민족문화대백과사전을 보면 다음과 같이 나와요.

중성의 기본 문자인 ‘아래아(ㆍ), ㅡ, ㅣ’는 각각 하늘, 땅, 사람을 본뜬 것으로... 중략

'아래아'라고 불러도 되지만 '하늘아'도 가능해요. 복잡한 조건문들 때문에 로직에 주석을 달기도 했었는데요. 그때 꼭 필요한 용어였어요.

ㄴ/ㄹ이 덧나는 조건이 완벽하지 않다

한국어 어문 규범에는 정확히 어떤 상황에서 "ㄴ"과 "ㄹ"이 덧나는지 설명이 없어요. 그래서 저는 훈민정음 유튜브와 KOCW에서 제공하는 한국어문규정 과목의 도움을 받았어요. 그런데 이러한 자료들조차 알려주지 않는 예외 케이스도 발견되었어요.

합성어에서 둘째 요소가 ‘야, 여, 요, 유, 얘, 예’ 등으로 시작되는 말이면 ‘ㄴ, ㄹ’이 덧난다

'고양이'라는 단어는 위의 덧나는 조건에 의해 '고양니'가 되는데요.

실제로 입으로 말해보세요. 어떻게 들리시나요?

'고양이'는 그대로 '고양이'로 발음됩니다. 이상하지 않나요? 분명히 위의 조건대로 했을 뿐인데 왜 표준 발음이 제대로 적용되지 않는 걸까요?

이 정보는 정말 찾기가 어렵더라고요. 그래서 전문가분이 아니면 안 될 것 같다고 판단하여 '세종 규칙 한글 연구소'를 운영하고 계시는 장덕진 선생님께 질문을 드려 답을 찾았었어요.

장덕진 선생님

ㄴ/ㄹ이 되기 위한 조건이지만 현재 음절의 중성의 ∙(아래아)가 하나가 아닐 경우에는 덧나지 않고 연음규칙이 적용된다

"ㅑ", "ㅕ", "ㅠ", "ㅛ" 등의 2개 이상의 아래아를 가진 중성은 "ㄴ/ㄹ"로 덧나지 않고 연음 규칙이 적용된다는 조건을 추가하고서 해당 버그를 고칠 수 있었어요.

이렇게까지 해야 해? 라는 생각이 들 수 있어요. 왜냐면 제가 실제로 이 기능을 만들면서 지인에게 들었던 소리입니다. 그렇게까지 "deep" 하게 들어갈 필요가 있느냐더군요.

저는 2가지로 답변드릴 수 있어요.

첫 번째, 코드의 완성도를 헤치는 버그를 찾았는데 수정하지 않는 것은 오픈소스를 보는 개발자들에 대한 예의가 아니라고 생각해요.

두 번째, "내가 아니면 누가 해?"라는 마음으로 책임감을 가지고 임하고 있어요. 비판적인 사고를 갖고 있지 않으면 쉽게 지나치기 쉬운 버그들이 오픈소스 세계에는 상당히 많다는 것을 알고 있어요. 만약, 제가 "고양니"로 변환되는 버그를 수정하지 않고 넘어갔다면, 그리고 그대로 main 브랜치에 merge 되었다면, 그대로 쭈욱 수정되지 않는 상태로 많은 개발자분들이 사용했을 수도 있어요.

물론, 오픈소스 특성상 언젠가는 발견되는 버그이고 누군가는 수정을 할 거에요. 하지만, 그건 책임감을 떠넘기는 행위라고 생각해요. 내가 만든 기능은 최대한 내 선에서 버그가 수정되어 배포되었으면 하는 생각을 갖고 있어요. 이러한 행동은 후에, 불필요한 bug-fix 이슈레이징을 예방한다고 생각하기도 해요.

오픈소스 기여를 하는 이유

저는 오픈소스 기여를 본격적으로 시작한 지 1년 4개월 정도 됐어요. 다양한 배경과 경험을 가진 개발자들의 코드에 많은 관심을 가지고 주기적으로 오픈소스 기여 활동을 하고 있어요.

오픈소스를 기여하면서 '오픈소스 기여를 통해 이 거대한 개발 세상에서 이름을 날려보겠다', '오픈소스 기여를 통해 개발 실력을 뽐내고 싶다' 등의 거창한 목표를 갖고 있지는 않아요.

처음부터 지금까지 작지만 소중한 목표를 갖고 있는데요. 제 기여를 통해 오픈소스를 처음 보는 개발자들이 보다 쉽게 이해할 수 있었으면 좋겠어요. 그래서 저는 테스트 코드, 새로운 기능, 버그 수정, 문서 수정, 각종 도구(번들러, 린팅, 포맷팅, 모노레포, 컴파일러) 등 가리지 않고 기여하고 있어요. 모든 부분에서 어려움이 없었으면 좋겠거든요.

JavaScript와 TypeScript 언어로 구축된 오픈소스에만 기여를 했었는데요. 최근에는 Rust 언어로 구축된 오픈소스들(Turbo, SWC, Biome.js)에 관심이 생겨 기여를 시도해 보고 있어요.

PR, Merge 그리고 후기

6월 5일에 올린 첫 PR은 87번의 추가 커밋, 2명의 리뷰어들과 31번의 논의를 통해 8월 5일에 main 브랜치에 merge 되었어요. 38개의 File changed과 2,100+줄의 방대한 소스 코드에 대한 리뷰를 해주신 리뷰어분들께 정말 감사했습니다.

기여

저는 오픈소스에 기여하면서 소소한 목표를 세우는데요. 바로 코드 오너, 메인테이너와 github-bot을 제외하고 TOP 컨트리뷰터가 되는 것인데요. 오늘 말씀드린 기능을 구현하면서 짬짬이 이것저것 기여를 했더니, 목표를 이루어서 정말 기뻤어요.

요즘 주변 지인들이 오픈소스 기여를 해보고 싶기는 한데 어떻게 시도해야 하는지, 어떻게 접근해야 하는지, PR을 올렸는데 틀리면 어떻게 하지? 등의 고민으로 생각만 하시는 분들이 꽤 계시더라고요.

만일 자신의 실수가 무서워서 고민하고 계신다면 "일단 해봐라."라고 말씀드리고 싶어요. 실수하면 다양한 개발자분들이 실수를 향한 피드백을 해줍니다. 절대 혼내지 않아요!

요즘 오픈소스와 관련된 스터디가 많아진 것 같아요. 접근 방법을 잘 모르신다면 스터디에 참석해 보시는 것도 좋은 선택일 것 같아요.

오픈소스와 관련해서 개인적으로 궁금하신 사항이 있으시다면 저에게 연락해 주셔도 좋습니다!

마무리

오늘 공유해 드린 오픈소스 기여 경험은 단연코 제 오픈소스 기여 중에서도 손에 꼽을 정도로 재밌게 했다고 말씀드릴 수 있어요. 덕분에 한글에 관한 공부도 하고 깊게 들어가면 한글이 얼마나 어려운지도 알게 되었어요.

세종대왕, 집현전 학자님들께 감사의 인사를 올립니다.

오픈소스는 개발자들만의 축제라고 생각해요. 이 축제를 저희가 즐기지 않으면 누가 즐길까요? 더군다나, 이 축제는 초대장이 필요 없고 드레스 코드를 신경 쓸 필요가 없어요. 먹거리는 다양하고 공짜면서 무제한 제공이에요.

시간제한도 없어요. 힘들면 쉬고 불타오를 때는 달리면 돼요.

오픈소스는 영업 종료가 없어요. 언제나 열려있어요.

한번 즐겨보세요.

아! 모두 es-hangul 라이브러리를 사랑해주세요! 관심도 가져주시면 감사합니다!

링크 제공

Issue(#33): https://github.com/toss/es-hangul/issues/33

PR(#115): https://github.com/toss/es-hangul/pull/115

es-hangul: https://es-hangul.slash.page/