[WebTranslator 개발기 #13] 3,000줄 단일 파일의 한계와 크롬 MV3 네이티브 ES 모듈 전환

3,000줄을 돌파한 단일 모놀리스 스크립트의 유지보수 한계를 분석하고, 번들러 없이 크롬 MV3 환경에서 boot.js 동적 부트로더를 통해 네이티브 ES 모듈 아키텍처를 구축한 과정을 정리합니다.
WebTranslator 3,000줄 단일 파일 분해 및 크롬 MV3 네이티브 ES 모듈 부트로더 아키텍처 다이어그램

다중 번역 엔진 연동, 듀얼 렌더러, 단어 사전, 큐 기반 재시도 로직까지 살을 붙여 나가다 보니 content.js와 background.js의 코드 길이가 각각 3,000줄을 돌파하는 상황을 겪게 되었습니다. 특히 AI 에이전트와 페어 프로그래밍을 진행할 때 거대한 단일 파일은 큰 걸림돌이 되었습니다. AI가 파일 전체의 문맥을 한 번에 다루다 보니 엉뚱한 줄의 코드를 건드리거나, 사소한 괄호 오타 하나로 확장 프로그램 전체가 먹통이 되는 디버깅 문제가 빈번하게 일어났습니다. 이번 글에서는 3,000줄 단일 파일을 분리하고, 번들러 없이 크롬 Manifest V3 환경에서 네이티브 ES 모듈(ESM) 구조로 전환한 과정을 정리하고자 합니다.

1. 거대 단일 파일의 한계와 번들러 없는 네이티브 모듈 선택

거대 단일 파일의 한계를 체감한 후 리팩토링 방식을 고민할 때, Webpack이나 Vite 같은 모던 번들러 도입과 번들러 없는 네이티브 ES 모듈(ESM) 방식을 비교 검토했습니다:

구분 번들러 도입 (Webpack / Vite) 노빌드 네이티브 ES 모듈 (ESM)
장점 여러 모듈을 단일 파일로 묶어 배포가 깔끔하고 구형 브라우저 호환성 처리가 수월함 별도의 빌드 과정 없이 코드 저장 후 브라우저 새로고침만으로 즉시 반영됨
단점 코드 한 줄을 고칠 때마다 매번 다시 빌드(npm run build)해야 해서 디버깅이 번거로움 모듈 파일이 늘어날수록 파일 간 의존성 연결과 매니페스트 권한을 직접 꼼꼼히 관리해야 함
개발 환경 복잡한 번들러 설정 및 외부 패키지 설치 필요 브라우저 표준 문법(import/export)만으로 가볍고 빠르게 동작

개발 과정에서 코드를 수정하자마자 브라우저에서 바로 확인하고 디버깅할 수 있는 빠른 피드백 루프를 갖추기 위해, 별도의 번들러를 쓰지 않고 브라우저 표준 네이티브 ES 모듈 방식을 선택했습니다.

2. 크롬 MV3 Content Script의 모듈 제약과 전면 마비

디렉토리를 src/api, src/background, src/content, src/options로 분리하고 코드를 쪼갠 뒤 확장 프로그램을 브라우저에 로드하자마자 전면 마비 현상이 발생했습니다:

⚠️ 발생한 오류: Uncaught SyntaxError: Cannot use import statement outside a module (at content.js:1)

원인은 크롬 Manifest V3의 스펙 차이에 있었습니다. 백그라운드 서비스 워커는 manifest.json에서 "type": "module"을 공식 지원하지만, 웹페이지 컨텍스트에 직접 주입되는 Content Script는 매니페스트 레벨에서 모듈 선언을 지원하지 않아 최상단 정적 import 구문을 문법 오류로 처리해버렸습니다.

3. boot.js 동적 부트로더와 리소스 권한 연동

이 제약을 해결하기 위해, 매니페스트에는 아주 가벼운 비모듈 진입점 파일인 src/content/boot.js만 등록하고, 내부에서 비동기 import() 함수를 실행하여 실제 Content Script 모듈 트리를 끌어오는 부트로더(Bootloader) 구조를 구현했습니다.

  1. 부트로더(boot.js) 작성: chrome.runtime.getURL()로 진입점 모듈의 확장 프로그램 절대 경로를 생성하고 비동기 동적 import 수행.
  2. 매니페스트 리소스 노출 선언: 웹페이지 컨텍스트에서 src/content/* 하위 JS 모듈에 접근할 수 있도록 web_accessible_resources에 등록.
  3. 백그라운드 서비스 워커 모듈화: manifest.json의 background 필드에 "type": "module" 지정.
// src/content/boot.js - Content Script 네이티브 ESM 동적 부트로더
(async () => {
  try {
    const src = chrome.runtime.getURL("src/content/index.js");
    await import(src);
    console.log("[WebTranslator] Content scripts loaded via boot.js");
  } catch (err) {
    console.error("[WebTranslator] Failed to load content scripts:", err);
  }
})();
// manifest.json - 네이티브 ESM 및 웹 접근 가능 리소스 구성
{
  "background": {
    "service_worker": "src/background/index.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["src/content/boot.js"],
      "run_at": "document_idle"
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["src/content/*"],
      "matches": ["<all_urls>"]
    }
  ]
}

4. 리팩토링 디버깅 경험: 롤백과 정면 돌파의 판단 기준

부트로더를 뚫어낸 직후에도 모듈 간 참조가 끊기며 ReferenceError가 발생하는 등 시행착오가 이어졌습니다. 대규모 리팩토링 과정에서 얻은 판단 기준은 다음과 같습니다:

  • 단순 오타 및 바인딩 누락: 에러 콘솔의 스택 트레이스를 따라 단방향 export/import 바인딩을 재연결하며 정면 돌파하는 것이 맞음.
  • 구조 설계 자체의 결함: 모듈 간에 순환 참조가 발생하거나 계층 구조에 대한 명확한 설계 없이 무작정 쪼갰을 때는 억지 땜질보다 즉시 롤백하고 단방향 데이터 흐름(api ➔ background, dom ➔ translation ➔ ui)을 재설계하는 것이 훨씬 안전함.

마무리하며

단일 3,000줄 스크립트를 200줄 내외의 독립적인 15개 모듈로 분리하면서, 번들러 없이도 크롬 브라우저에서 소스 파일별로 브레이크포인트를 걸고 즉각 디버깅할 수 있는 쾌적한 개발 환경을 갖추게 되었습니다.

혹시 크롬 확장 프로그램이나 프론트엔드 환경에서 노빌드 네이티브 ESM을 구성하면서 겪으신 의존성 관리 팁이 있다면 댓글로 공유해 주시기 바랍니다. 다음 글에서는 모듈화된 기반 위에서 단어 사전의 속도와 다국어 지원을 개선하기 위해 진행했던 외부 사전 연동 시행착오와 단일 LLM 고속 사전 파이프라인 전환 과정을 다루겠습니다.