🔽 코드

auto-translator
treephesiansUpdated Jul 8, 2025
notion image
notion image
금색 한글 텍스트가 Chrome Extension을 활용하여 생긴 번역 텍스트이다.

문제 상황


Udemy에서 강의를 야심차게 구매했지만, 한글자막이 없는 강의였다..😂
잘 모르는 분야를 공부하는 것이기에 이미 속도가 느릴텐데 영어로 학습하면 학습 속도가 현저히 느려질까봐 걱정되었다.
 
유튜브는 자동 번역이라도 있던데, 혹시 크롬의 이런 extension이 있지 않을까 싶어서 찾아보았다.
Substital: Add subtitles to videos and movies - Chrome Web Store
그나마 찾은 것이 “Substitial” 인데 이것은 자막 파일이 별개로 필요했다..
 
내가 원했던 기능은 영상 속 자막을 탐지하고 즉시 한글로 번역해주는 기능이다.
좀 더 찾아보면 내가 원했던 extension이나 프로그램이 있을 수도 있는데,
나는 개발자가 아닌가? 한 번 만들어보기로 했다.
 

꼭 Chrome Extension이어야 할까?


나는 가장 먼저 Chrome의 extension이 아니더라도 원하는 기능을 만들 수 있을지 조사해보았다.
 
  1. 데스크톱 앱 + 화면 캡쳐 + OCR
    1. 애초에 데스크톱 앱까지 제작할 프로젝트가 아닌 것 같고, 또 브라우저에서 자막을 추출할 수 있는데 굳이 OCR을 사용할 필요도 없다고 생각한다.
  1. Tampermonkey
    1. 얘는 Tampermonkey extension을 추가해주고, 내가 원하는 스크립트만 넣어주면 된다.
    2. 애초에 chrome extension을 내가 만들고 배포해야하는 과정이 줄어들기 때문에, 내 학습 용도로만 사용하기에는 Tampermonkey가 더 적합하다고 생각이 들어서 한 번 사용해보았다.
 

Tampermonkey 사용 후기

notion image
notion image
이렇게 Tampermonkey extension을 설치 후 활성화 해준 다음,
오른쪽 사진 처럼 자막을 추출하는 스크립트를 만들어보았다.
 
notion image
 
추출된 자막을 Console 창에 출력해보았고, 위 사진처럼 잘 작동하였다.
그러나 문제는 자막의 탐지를 변화하지 못하는 것도 있었고 생각보다 반응성이 좋지 않았다.
Tampermonkey는 자막 탐지만을 위한 extension이 아니기 때문에 좀 더 무거워서 그런 것 같다.
 
그래서 그냥 나의 chrome extension을 만들려고 한다.
앞으로 Udemy의 영어 강의들을 원할하게 듣기 위해서 하루 정도는 투자할만한 프로젝트라고 생각한다.
 
 

Chrome Extension 만들기


구글에서 만든 기본적인 가이드가 있어서 참고하였다.
 
extension은 기본적으로 javascript 코드랑 별 다를게 없었다.
다만, extension의 설정 파일인 manifest.json이 꼭 필요했다.
기본적인 프로젝트 구조는 다음과 같았다.
notion image
 
각 파일별로 어떤 기능을 하는지 알아보자.

manifest.json

아래 코드는 실제로 내가 구현하기 위해 만든 파일이다.
{ "manifest_version": 3, "name": "실시간 자막 번역기", "version": "1.0", "description": "영어 자막을 실시간으로 한국어로 번역하여 표시합니다.", "permissions": ["storage", "activeTab", "scripting"], "host_permissions": ["https://*/*", "http://*/*"], "content_scripts": [ { "matches": ["<all_urls>"], "js": ["content.js"], "css": ["styles.css"], "run_at": "document_end" } ], "background": { "service_worker": "background.js" }, "action": { "default_popup": "popup.html", "default_title": "자막 번역기" }, "web_accessible_resources": [ { "resources": ["translator.js"], "matches": ["<all_urls>"] } ] }
manifest_version , name , version 은 필수적인 기본 메타데이터이다.
배포하기 위해서는 icon 데이터도 필요하지만 지금 당장 난 배포할 용도는 아니기에 넘어가겠다.
 
permissionsactiveTab 은 사용자가 포커스가 지정된 탭에서 확장 프로그램을 실행하도록 의도적으로 선택할 수 있으며, 이렇게 하면 사용자의 개인 정보가 보호된다. 또한 Chrome은 서비스 워커가 필요하지 않으면 종료한다. storage 는 API를 사용하여 서비스 워커 세션 간에 상태를 유지한다. 마지막으로 scripting 은 웹페이지에 코드를 주입할 수 있도록 하는 권한이다.
 
host_permissions 의 경우 외부 도메인으로 네트워크 요청 권한 부여한다. 내가 번역을 하기 위해서 외부 api를 이용하므로 필요하다.
 
content_scripts 는 페이지의 콘텐츠를 읽고 수정하기 위한 스크립트에 대한 정보이다.
matches 를 통해 콘텐츠 스크립트를 삽입할 사이트를 식별할 수 있다.
 
background 에는 확장 프로그램의 service worker 를 사용하여 백그라운드에서 브라우저 이벤트를 모니터링할 수 있다. service worker 란, 이벤트를 처리하고 필요하지 않을 때 종료되는 특수한 JS 환경이라고 한다.
예를 들어, 서비스 워커가 수신 대기하는 첫 번째 이벤트는 chrome.runtime.onInstalled() 이다. 이 메서드를 사용하면 확장 프로그램이 초기 상태를 설정하거나 설치 시 일부 작업을 완료할 수 있다.
 
action은 브라우저 툴바에 표시되는 확장자 아이콘과 관련된 설정이다.
extension의 동작방식을 이미지로 간단히 잘 표현한 것이 있어서 첨부해보았다.
notion image
 

어떻게 구현하였나?


먼저 Cursor로 구현에 도움을 받았지만, 코드를 한줄 한줄 분석해보니 불필요한 코드들도 너무 많았다.
그래서 실제로 모든 코드를 내가 직접 설명할 수 있을 정도로 Cursor의 코드를 리뷰하고,
내가 원하는 최적의 기능을 담긴 코드만 남겨두었다.
 
일단 전체 내 코드는 상단 github 링크에서 보면 좋을 것 같다.
여러 파일 중 가장 중요하게 봐야할 파일은 content.jsbackground.js이다.
content.js가 DOM의 변경을 탐지하는 코드라면,
background.js는 세팅 및 외부 api를 이용하여 번역을 해오는 코드이다.
 
content.js 에서 번역 api를 불러오면 안될까?
브라우저의 content script(웹페이지 컨텍스트)에서 외부 API로 직접 fetch 요청을 보내면,
Google 서버가 Access-Control-Allow-Origin 헤더를 보내지 않기 때문에 브라우저가 요청을 차단한다.
즉, CORS 에러가 발생하는 것이다.
 
반면 background.js는 브라우저 확장자 권한으로 동작하므로 background.js(서비스 워커)는 CORS 제한이 없다. 그래서 background.js를 사용하는 것이다.
 
번역 api를 이용하기 위해 조사해본 결과, 추천 목록은 다음과 같았다.
  1. google cloud translate api
  1. libreTranslate
  1. MyMemory
근데 아쉽게도 Google과 libreTranslate는 유료 서비스였다..
그래서 MyMemory api를 사용했다. 하지만.. MyMemory도 사실 전부 무료는 아니어서 디버깅을 하다가 Time Limit에 걸렸고, 이러다가 내가 번역 api도 만들어야 하나 싶은 생각도 들었다ㅠ
번역 api를 만드는 것이 아닌 Docker를 활용하여 내 로컬 서버에서 번역 서버를 실행시키도록 했다.
→ 이 내용도 아래 기술적 챌린지에서 다루겠다.

디버깅 에러 모음


사실 코드 구현 전에 더 중요하다고 생각했던 부분이 디버깅&테스트 였다.
어떻게 디버깅을 해야할까 걱정이 되었지만, 생각보다 간단했다.
Chrome의 Extension 주소로 들어간 후, 개발자 모드를 켜서 내 코드를 불러오기만 하면 끝이었다.
코드를 수정하고 extension을 새로고침만 해주면 자동으로 코드가 반영이 되었다.
 
Service Worker registration failed. Status code: 15 → 이 에러는 경로 문제가 아니라, background.js에서 아래의 오류가 나서 Service Worker를 불러오지 못해 생긴 경고다.
 
Uncaught TypeError: Cannot read properties of undefined (reading ‘create’) → contextMenus를 사용하는데 권한이 없어서 생긴 에러였다.
→ manifest.json에 "permissions": ["contextMenus"]를 추가하여 해결하였다!
 
Error: Extension context invalidated. → 내가 extension을 실행하고 디버깅을 위해 탭을 바꾸면 서비스워커가 자동으로 꺼지면서 이런 에러가 생겼다.
→ 해결방법은 기술적 챌린지에서 설명하겠다.
 

기술적 챌린지

서비스워커 유지하기

위에 오류 모음에서 말했듯이, 탭을 바꾸면 extension의 서비스 워커가 바로 종료되는 현상이 나타났다.
서비스 워커는 이벤트가 없으면 자동으로 종료된다는 말을 보아서 아래와 같은 코드를 추가해보았다.
chrome.alarms.create('keepAlive', { periodInMinutes: 1 }); chrome.alarms.onAlarm.addListener((alarm) => { if (alarm.name === 'keepAlive') { // console.log('Service worker keep-alive ping'); } })
위와 같이 1분마다 주기적으로 알람을 주는 이벤트를 생성해주었다.
서비스워커는 결국 1분이라는 타이머를 계산하고 있는 것이다.
물론 크롬 정책상 영구적으로 살아있을 수는 없겠지만, 그래도 잠깐의 탭 전환 정도는 충분히 가능하게 되었다!
 

번역 API의 한계…

번역 API를 이용하면 결국 하루에 제한이 걸려있었다. 그렇다고 유료 결제를 하고 싶지는 않아서 다른 방법을 찾아보았다. 그렇게 하나 알게 된 것이 자체 LibreTranslate를 Docker로 내 로컬로 실행하는 것이었다.
즉, LibreTranslate를 Docker 컨테이너로 실행해서 당신의 로컬에서 번역 API 서버를 구동시키는 것이다.
애초에 LibreTranslate는 오픈소스인데 왜 유료일까 사기당했다라고 생각했는데 내가 바보였던 것이다ㅋㅋ
docker run -d --name libretranslate -p 5000:5000 libretranslate/libretranslate --load-only en,ko
 

자막 삽입

처음에는 번역된 텍스트를 원래 자막 속에 새로운 div로 감싸서 추가해주었다.
그러나 이 방식 때문에 번역 텍스트가 추가 되면 다음 자막은 observer가 변화를 탐지 못했다.
원본 자막의 div에 영향을 주기 때문이라고 생각을 했고,
번역된 텍스트를 원문의 자식 요소에 추가 하던 방식을 형제 요소에 추가하여 해결하였다.
 

정리하며,


결과는 매우 흡족했다.
사실 한 문장 한 문장 그대로 번역을 시키는 것이기에 문맥상 이어지지 않는 말도 있었지만, 이런 부분까지 고려해서 만들기에는 시간적으로 과투자가 될 것 같다.
어쨌든 지금 이 정도도 내가 영어 강의를 듣는 데에는 어려움이 없어서 만족했고
무엇보다 내가 겪은 어려움을 기술로 해결하여 뿌듯했다.
또한 Chrome Extension을 만들기 전에 다른 방법으로 이 문제를 해결할 수 있지 않을까 연구해보며 답을 찾아가는 과정이 개발자스러웠던 것 같다.