본문 바로가기
source-code/FrontEnd

[chrome extension] dynamic import시 Cannot find module 에러 해결하기

by mattew4483 2024. 7. 26.
728x90
반응형

Background

chrome extension의 content script에서, 다른 모듈을 동적으로 import하고자 했습니다.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/import

 

import() - JavaScript | MDN

The import() syntax, commonly called dynamic import, is a function-like expression that allows loading an ECMAScript module asynchronously and dynamically into a potentially non-module environment.

developer.mozilla.org

// boot.ts
console.log('loaded')

// content-script.ts
import('boot.ts') // content script가 실행될 때, loaded 콘솔이 뜨면 됩니다!

 

content-scripts에서 같은 경로 내 boot.ts를 동적 import하면 되는 상황이었고,

번들러는 Webpack을 사용하고 있었습니다.

 

content scripts

https://developer.chrome.com/docs/extensions/develop/concepts/content-scripts?hl=ko

 

콘텐츠 스크립트  |  Chrome Extensions  |  Chrome for Developers

콘텐츠 스크립트 및 Chrome 확장 프로그램에서 콘텐츠 스크립트를 사용하는 방법에 대한 설명입니다.

developer.chrome.com

Chrome Extension은 브라우저의 기능을 확장하고 웹 페이지와 상호작용할 수 있는 기능을 제공합니다.

이 중 content script는 특정 웹 페이지에서 실행되는 JavaScript 파일로, 웹 페이지의 DOM을 읽고 수정할 수 있습니다.

→ 웹 페이지와 동일한 환경에서 실행되기 때문에, 웹 페이지의 자바스크립트와 상호작용하거나 DOM 요소를 조작할 수 있는 것이죠!

 

content scripts에서 외부 파일 사용하기

content script에서 외부 파일을 불러오기 위해서는 몇 가지 설정이 필요합니다.

 

1. web_accessible_resources

conent script에서는 manifest.json의 web accessible resources에 정의된 asset들에만 접근할 수 있습니다.

// manifest.json
{
  "name": "My Extension",
  "version": "1.0",
  "manifest_version": 3,
  "permissions": ["activeTab"],
  "background": {
    "service_worker": "background.js"
  },
  "content_scripts": [
    {
      "matches": ["<all_urls>"],
      "js": ["content.js"]
    }
  ],
  "web_accessible_resources": [
    {
      "resources": ["boot.js"], // 추가!
      "matches": ["<all_urls>"]
    }
  ]
}

 

2. chrome.runtime.getURL

content script에서 extension의 asset을 포함하는 경우에도 (일반적인 경우와 마찬가지로) 자원의 전체 URL을 지정해야합니다.

→ chrome.runtime.getURL API를 사용해, 현재 chrome extension의 경로에서 파일을 탐색할 수 있습니다.

const scriptUrl = chrome.runtime.getURL("boot.js");

이를 통해 content script에서 동적으로 boot.js 파일을 불러올 수 있겠죠.

 

Problems

위 설정을 적용하면, content script를 아래와 같이 작성할 수 있습니다.

// boot.ts
console.log('loaded')

// content-script.ts
const dynamicModuleUrl = chrome.runtime.getURL("boot.js");
import(dynamicModuleUrl)

 

그런데, 막상 익스텐션을 실행해보면...

익스텐션 경로에서 파일을 찾을 수 없다는 에러가 발생합니다!

 

하지만 막상 위 링크로 접속하면

번들링된 boot.js 파일이 정상적으로 존재하는 것을 확인할 수 있습니다. 이게 어떻게 된 일일까요?

 

Caused

원인을 파악하기 위해 원격 모듈(boot.js)를 import하는, content-script.js 번들링 파일을 살펴봤습니다.

(() => {
  var r = {
      6910: (r) => {
        function e(r) {
          return Promise.resolve().then(() => {
            var e = new Error("Cannot find module '" + r + "'");
            throw ((e.code = "MODULE_NOT_FOUND"), e);
          });
        }
        (e.keys = () => []), (e.resolve = e), (e.id = 6910), (r.exports = e);
      },
    },
    e = {};
  function o(t) {
    var n = e[t];
    if (void 0 !== n) return n.exports;
    var s = (e[t] = { exports: {} });
    return r[t](s, s.exports, o), s.exports;
  }
  (o.o = (r, e) => Object.prototype.hasOwnProperty.call(r, e)),
    (() => {
      "use strict";
      var r = chrome.runtime.getURL("boot.js");
      o(6910)(r);
    })();
})();

 

이 코드에서 Webpack이 동적 import를 처리하려고 시도하는 부분은 다음과 같습니다:

var r = chrome.runtime.getURL("boot.js");
o(6910)(r);

 

하지만 막상 o(6910) 함수는

var r = {
  6910: (r) => {
    function e(r) {
      return Promise.resolve().then(() => {
        var e = new Error("Cannot find module '" + r + "'");
        throw ((e.code = "MODULE_NOT_FOUND"), e);
      });
    }
    (e.keys = () => []), (e.resolve = e), (e.id = 6910), (r.exports = e);
  },
};

Cannot find module Error만을 반환하고 있습니다!

 

아하...

즉 번들링 결과물에서부터 chrome.runtime.getURL("boot.js") 경로의 모듈은 존재하지 않기 때문에

런타임에서도 당연히 Cannot find module 에러가 발생했던 것입니다!

 

Webpack과 동적 파일 번들링

Webpack은 정적 분석 도구로서, 빌드 시점에 모든 모듈을 분석하고 번들링합니다.

그리고 이는 동적 import를 사용할 때도 예외는 아닙니다.

 

동적 import를 사용할 때, Webpack은 경로를 정적으로 분석하여 해당 모듈을 번들링 과정에 포함시키려고 시도합니다.

그러나 chrome.runtime.getURL을 통해 생성된 경로는 런타임에만 알 수 있기 때문에,

Webpack은 이 경로를 인식하지 못하고 위와 같은 에러를 발생시키고 마는 것이죠.

 

Solution

이를 해결하기 위해서는?

Webpack이 해당 import를 무시하고, 런타임 시점에 해당 경로로 모듈을 동적으로 로드할 수 있게 되겠죠!

→ webpackIgnore: true 플래그를 사용해 이를 구현할 수 있습니다.

// boot.ts
console.log('loaded')

// content-script.ts
const dynamicModuleUrl = chrome.runtime.getURL("boot.js");
import(
  /* webpackIgnore: true */ 
  dynamicModuleUrl
);

해당 플래그를 통해 Webpack에게 해당 import 구문을 무시하도록 지시할 수 있습니다.

 

(() => {
  "use strict";
  var t = chrome.runtime.getURL("boot.js");
  import(t);
})();

번들링 된 content-script.js 파일입니다.

 

이전과는 달리, Webpack이 해당 경로를 분석하고 번들링하지 않았음을 확인할 수 있습니다.

이를 통해 런타임 시점에 해당 경로로 모듈을 동적으로 로드할 수 있겠죠.

성공!

콘솔을 통해 동적 import가 정상적으로 이뤄졌음을 확인한 모습입니다.

728x90
반응형