Outsider's Dev Story

Stay Hungry. Stay Foolish. Don't Be Satisfied.
RetroTech 팟캐스트 44BITS 팟캐스트

사이드 프로젝트로 만든 GitHub 번역용 크롬 익스텐션

작년 11월에 구글 번역신경망이 적용되면서 그동안 형편없던 영어-한글 번역에 새로운 세상이 왔음을 알게 되었다. 많은 개발자가 이를 보고 뭔가 이용할 수 있는 게 없을까 하는 생각을 했을 테고 내 주변에도 간단한 도구나 확장으로 번역해 보는 걸 만들었지만, API가 없어서 제대로 뭔가를 만들기는 어려웠다. 나도 그러던 중 뭔가 만들어 볼 수 있지 않을까 해서 Google 클라우드 번역 API를 보다 보니 언제 나왔는지 몰라도 신경망에 기반을 둔 번역이 프리미엄으로 제공되는 것을 알게 되었다.

Standard 번역이 기존의 기계번역이고 신경망 번역이 Premium인데 아직 베타 상태라 신청을 해서 허락을 받아야 사용할 수 있다. 원래는 만들어서 공개할 생각이었지만 일단 신청서는 팀 내부에서만 쓰려고 이런 번역 크롬 익스텐션을 만들려고 한다고 신청서를 작성해 놓고 기대도 안 하고 잊고 있었다.

구글에서 받은 승인 메일

그랬더니 웬걸 갑자기 승인되었다고 메일이 왔다. 앗, 이러면 진짜 만들어 봐야겠는데.... 하고는 만들게 되었다.

GitHub의 댓글 번역

영문 글도 많이 보고 GitHub도 많이 쓰는데 블로그나 기사는 맘먹고 집중해서 읽으면 그나마 읽을 수 있는데 GitHub 이슈나 Pull Request에서 이뤄지는 다양한 논의는 이해하기가 쉽지 않았다. 해커뉴스의 댓글도 마찬가지. 이런 글은 정리해서 쓴 글도 아니고 여러 사람이 다양한 의견을 내놓으므로 내 어설픈 영어로는 문맥을 파악하기가 어려웠고 집중해서 읽기에는 글이 너무 많았다. 나는 한글로 댓글을 보듯이 하나하나 정독하는 게 아니라 훑어보면서 대략적인 문맥만 파악하기를 원했다.

Google 번역 크롬 익스텐션이 있지만 나는 이런 도구로는 내용을 파악하기가 어려웠다. 왜냐하면, 원래 글의 모양 그대로 보여주는 것이 아니라 어떤 문단이 어떻게 번역되었는지 비교하기가 어려웠고 구글 번역의 경우 줄 바꿈이 있으면 별도의 문장으로 번역하는데 GitHub은 마크다운을 많이 쓰다 보니 결과 번역 품질이 좋지 않았다.

내가 원한 건...

  1. GitHub 화면에서 간단한 액션으로 번역 결과를 봐야 한다.
  2. 번역이 이상하면 원문을 봐야 하므로 원문이 같이 나왔으면 좋겠다.
  3. GitHub은 코드나 이미지도 있고 마크다운으로 스타일도 주므로 번역 글을 쉽게 보려면 이러한 스타일이 최대한 유지되어야 한다.

이 정도 목표를 가지고 크롬 익스텐션을 만들기 시작했다.

대충 보면 알겠지만, 처음에는 쉽게 생각했다. 내용을 가져와서 번역하고 그 결과를 화면에 뿌려주면 되는 게 끝이다. 원래는 연초의 일주일 정도 기분전환용 프로젝트로 시작했다. 하지만 1월 내내 이거만 붙잡고 있었지만...

Issue Translator for GitHub

GitHub의 트레이드마크를 침해하지 않으려고 위와 같은 이름을 사용했다. 개발과정은 뒤에 설명하겠지만 일단 결론부터 얘기하면 다음과 같은 크롬 익스텐션을 만들었다. 저장소 참고.

익스텐션의 옵션 화면

먼저 Issue Translator for GitHub를 크롬에 설치하고 API 키와 번역할 언어를 선택한다.(기준은 항상 영어)

데모

이제 GitHub에서 이슈나 풀 리퀘이스트에 가면 댓글마다 오른쪽 위에 지구본 모양의 번역 버튼이 나타난다. 이 버튼을 누르면 구글 번역 API로 번역한 결과를 아래에 보여준다. 이때는 원글의 스타일이나 링크를 최대한 유지한다. 생각보다는 잘 나와서 GitHub을 볼 때 애용하고 있다.

하지만

이 크롬 익스텐션은 Chrome 웹 스토어에 아직 등록하지 않았다. 왜냐하면, 구글 번역 API 프리미엄이 아직 베타 상태이기 때문이다. 그래서 공개하더라도 구글에 신청해서 키를 받지 않는다면 이 플러그인을 쓸 수가 없다. 2월 중에는 베타가 끝나고 공개되지 않을까 해서 기다리고 있었는데 계속 나오지 않아서 그냥 공개하고 나중에 베타가 끝나면 스토어에 올리거나 하기로 했다. 참고로 구글 번역 API는 유료 API이므로 내가 과금을 하지 않으려면 사용자가 구글 클라우드에 가입해서 API를 사용하게 해야 한다.

일단 이 글은 개발 정리용이다.

초기 개발

처음에는 아주 간단히 생각했다. 한꺼번에 전체를 다 번역하려고 하다 보니 API가 유료라서 불필요한 글자 수를 줄이기 위해 댓글마다 버튼을 추가하고 버튼을 누르면 댓글 내용을 구글 API에 던졌다가 받아서 아래에 보여주면 끝날 거라고 생각했다. 그래서 며칠이면 될 것 같았다.

일단 버튼이 필요하므로 GitHub의 Octicons에서 지구본 모양을 골랐다. Octions 모듈에서 API를 잘 제공해서 아이콘의 SVG 패스를 바로 출력해 준다.

$ octicons.globe.path
'<path fill-rule="evenodd" d="M7 1C3.14 1 0 4.14 0 8s3.14 7 7 7c.48 0 .94-.05 1.38-.14-.....'

이를 크롬 익스텐션이 로드될 때 댓글 영역을 찾아서 댓글 상단에 버튼을 넣었더니 원래 있던 듯 자연스럽게 어울렸다.

번역 버튼

크롬 익스텐션을 만들 때 편한 점은 크로스 브라우징을 신경 쓰지 않아도 된다는 거다. 그래서 jQuery나 다른 라이브러리도 사용하지 않고 V8에서 지원하는 JavaScript로만 작성하려고 querySelector등을 열심히 사용하고 API 요청도 fetch API를 사용했다. 하다 보니 API가 초당 10개가 넘으면 오류가 발생해서 요청마다 약간의 딜레이를 주어서 1초에 10개 이상이 나가지 않도록 제어해야 했다.

대충 GitHub의 댓글 DOM 구조를 파악해보니 한 댓글의 각 문단이 <p>태그로 묶이는 걸 알 수 있었다. 댓글 내용 중에 코드 블럭이나 이미지는 블럭할 필요가 없으므로 이는 제외하고 다른 <p>태그만 가져와서 구글 번역을 했더니 다행히도! 번역하면서 HTML 마크업까지 그대로(사실 100%는 아니다) 돌려주었다. 이를 댓글 하단에 보여주니까 제법 그럴듯했다.

그래서 이것저것 테스트하면서 다양한 케이스를 좀 살펴본 다음에 지인 중심으로 플러그인을 뿌렸다. 어차피 Premium API가 비공개이므로 시간이 충분하다는 생각이었다.

익스텐션 개선

피드백 받다 보니 번역 버튼이 나오지 않는다는 얘기가 나왔다. 난 테스트를 하느라고 다양한 댓글을 여러 탭에 띄워놓고 테스트하느라고 몰랐는데 일반적인 패턴에서 사용하다 보니 다른 페이지에서 이슈나 풀 리퀘스트로 넘어가면 버튼이 보이지 않고 페이지를 갱신해야 보였다. 상황을 이해했을 때 대충 짐작이 갔지만, GitHub이 pjax를 쓰고 있어서 메뉴를 바꿀 때 페이지를 새로 로드하는게 아니라 내용 부분만 바꿔친다. 크롬 익스텐션은 페이지 로딩을 방해하지 않으려고 페이지 로딩 후에 익스텐션을 활성화하는데 이게 한 번만 실행되므로 pjax로 메뉴가 바뀌면 댓글에 번역 버튼이 추가되지 않았다. 나는 페이지가 로딩되면 댓글이 있는지 확인 후에 버튼을 넣어주도록 구현해 놓고 있었다.

먼저 GitHub의 동작을 파악하기 위해서 테스트해보니 push state로 조작하는 걸 알 수 있었고 push state의 상태변경을 탐지하기 위해 찾아보니 chrome.webNavigation로 할 수 있는 걸 알게 되었다. manifest.json에서 webNavigation에 대한 권한을 얻은 후 백그라운드 스크립트에서 다음과 같이 히스토리가 변경되는 이벤트를 받았다. 문서에 잘 나와 있지 않아서 꽤 헤맸는데 페이지에서 어떤 스크립트를 동작하게 하는 것을 크롬 익스텐션에서는 콘텐츠 스크립트라고 부르는데 이 이벤트 탐지는 백그라운드 스크립트에서 해야 한다.

if (chrome.webNavigation && chrome.webNavigation.onHistoryStateUpdated) {
  chrome.webNavigation.onHistoryStateUpdated.addListener((details) => {
    chrome.tabs.sendMessage(details.tabId, {action: 'rerun', url: details.url});
  });
}

여기서 받은 이벤트를 chrome.tabs.sendMessage로 해당 탭에 메시지를 보내도록 했다. 콘텐츠 스크립트에서는 다음과 같이 받아서 재실행을 해주었다.

chrome.extension.onMessage.addListener(function(msg) {
  if (msg.action === 'rerun') {
    if (msg.url === location.href) {
      // 버튼 추가 재 실행
    }
  }
});

조건문이 있는 이유는 테스트하다 보니 페이지가 바뀔 때 현재 페이지 URL과 다음 페이지 URL 이렇게 2번씩 이벤트가 발생하므로 1개는 무시하기 위해서 넣은 것이다.

그리고 번역품질이 좋지 않다는 피드백이 있었다. 피드백 받은 댓글로 테스트를 해보니 실제로 번역품질이 많이 안 좋았다. 구글 번역에서 좀 더 테스트를 해보니까 특수기호가 구글 번역결과에 많은 영향을 준다. 아주 간단한 예를 들어 다음과 같은 문장을 보자.

This commit implements the Web IDL USVString conversion, which mandates all unpaired Unicode surrogates be turned into U+FFFD REPLACEMENT CHARACTER.

이는 다음과 같이 번역된다.

이 커밋은 모든 <b> 비 유니 코드 </ b> 대리자가 U + FFFD REPLACEMENT CHARACTER로 변경되도록하는 Web IDL USVString 변환을 구현합니다.

하지만 <b> 태그가 없다면 다음처럼 더 자연스럽게 번역이 된다.

이 커밋은 웹 IDL USVString 변환을 구현합니다.이 변환은 모든 비 유니 코드 사로 게이트를 U + FFFD REPLACEMENT CHARACTER로 변환하도록 요구합니다.

이는 아주 사소한 비교이지만 실제로는 더 다양한 케이스가 있고 태그하나 들어가냐 아니냐에 따라서 번역품질이 많이 달라졌다. ㅠ 결국은 번역을 해주고자 하므로 스타일 등을 버리고 문장만 반역한다면 훨씬 좋은 결과를 얻을 수 있었고 테스트한 지인에게 물어봐도 번역품질이 더 좋은 게 낫다는 얘기를 했다.

번역 품질 개선

하지만 원래 목표에서 스타일을 유지하는 게 목표였고 실제로 이 마크다운의 스타일을 유지하니까 보기가 편해서 스타일을 버리고 싶지 않았다. 주변 사람들한테 이 문제점을 얘기하다가 갑자기 좋은 생각이 났다. HTML 태그는 아무래도 장황한데 어차피 Markdown으로 입력한 내용이니까 HTML을 Markdown으로 변환한 뒤 번역을 하고 Markdown을 다시 HTML로 바꾸어서 출력하면 결과가 훨씬 나으면서 스타일도 유지할 수 있지 않을까 한 것이다. 마크다운 기호는 아무래도 HTML 태그보다는 더 글쓰기 기호에 가까우니 영향이 더 적을 거라고 생각했다.

HTML을 마크다운으로 바꿀 때는 to-markdown을 쓰고 마크다운을 다시 HTML로 바꿀 때는 markdown-it을 사용했다. 실제로 해보니 번역결과가 더 좋아진 것을 알 수 있었다.

대신 마크다운 기호를 구글 번역이 임의로 막 바꾸는 경우가 많았다. 이건 일정 패턴이 있는 게 아니라 문장에 따라 임의로 바꾸어서 스타일이 깨지므로 이를 최대한 정리해주어야 했다. 예를 들어 > * Chris> *Chris가 된다거나 [text](url)[text] (url)로 바꾼다거나 하는 등이다. 이를 하나씩 찾아서 수정하다 보니 더는 기억으로 제어할 수 없는 수준이 되어서 결국은 경우별 테스트 케이스를 작성해서 관리할 수밖에 없었다. (구글. 부들부들)

특히 이미지랑 링크는 골치 아팠는데 마크다움에서 링크는 [github](https://github.com/)처럼 만들어지는데 이 URL이 깨지는 경우가 많았고 [github](https://github.com/#anchor)와 같이 앵커가 붙은 경우는 # 뒤의 부분만 링크에서 떨어져나와서 번역문이 이상해지는 경우가 많았다. 이미지는 사용자가 업로드를 하므로 링크와 이미지가 섞여서 [![screentshot](img-url)](url)처럼 되는데 이때는 더 심각하게 꼬여버린다. 그래서 이모지로 플레이스 홀더를 만들어 번역 전에 갈아치운 후 번역이 끝나면 다시 복구하는 노가다 방식을 선택했다.

번역된 화면

결국, 다 좋은 결과가 나오는 건 아니지만, 내용을 파악하고 읽기 쉬운 형태 정도로는 나왔다.

마무리

Primium API 오픈 안 한다고 소스를 다듬다 보니 처음에는 익스텐션 코드밖에 없었는데 결국은 babel, webpack 2, karma 테스트까지 모두 사용하게 됐다. 기능은 거의 다 되었으므로 간격 같은 것도 맞추고 로딩 바 등을 넣어서 사용성을 개선하고 무료 아이콘을 찾아다가 넣었다. 처음 승인 메일이 올 때 1월까지만 스탠다드 요금을 받고 2월부터를 Primium 요금을 받는다고 해서 2월에는 열릴 줄 알았는데 영 안 열려서 더 시간 지나기 전에 저장소를 공개로 바꾸고 오픈을 했다.

누군가 구글에 요청해서 승인을 받는다면 사용할 수 있을 것이다. 참고로 구글 클라우드에 가입하면 60일 동안 $300 크레딧을 주는데 덕분에 무료로 잘 썼다. 맘 놓고 주변에도 뿌리고 실컷 테스트했는데 20달러 정도밖에 안 나가서 댓글 읽을 때 드는 시간을 생각하면 나로서는 충분히 지급할 정도의 금액이다.

2017/02/22 09:00 2017/02/22 09:00