(번역) 자바스크립트 Import Map에 대해 알아야 할 모든 것

Chanhee Kim·2022년 8월 14일
11

FE 글 번역

목록 보기
5/21
post-thumbnail

원문: https://www.honeybadger.io/blog/import-maps/

Import Map은 웹 페이지에서 자바스크립트 import를 제어하는 새로운 방법으로, 빌드 시스템을 벗어날 수 있는 가능성을 열어줍니다. 이 글에서 Ayooluwa Isaiah와 함께 Import Map의 스펙을 깊이 살펴봅시다.

ECMAScript 2015에서 자바스크립트 모듈 시스템을 표준화하기 위한 방법으로 ES 모듈이 처음 도입되었을 때 import 문에서 상대 또는 절대 경로를 의무적으로 사용하도록 하는 스펙으로 구현되었습니다.

import dayjs from 'https://cdn.skypack.dev/dayjs@1.10.7'; // ES Modules

console.log(dayjs('2019-01-25').format('YYYY-MM-DDTHH:mm:ssZ[Z]'));

이 방식은 CommonJS와 같은 다른 일반적인 모듈 시스템에서 모듈이 동작하는 방식이나 더 간단한 구문을 사용하는 Webpack 같은 모듈 번들러를 사용했을 때와는 조금 다릅니다.

const dayjs = require('dayjs'); // CommonJS

import dayjs from 'dayjs'; // Webpack

이러한 시스템에서 import 지정자는 Node.js 런타임 또는 빌드 도구를 통해 버전이 지정된 특정 파일에 매핑됩니다. 사용자는 import 문에서 일반적으로 패키지의 이름인 베어(bare) 모듈 지정자만 적용하면 되며 모듈 해석은 자동으로 처리됩니다.

개발자들은 이런 방식으로 npm 패키지를 가져오는 것에 익숙했기 때문에 이런 방식으로 작성된 코드가 브라우저에서 실행될 수 있도록 빌드 과정이 필요했습니다. 이러한 문제는 Import Map에 의해 해결됐습니다. 기본적으로 Import Map은 import 지정자를 절대 또는 상대 경로에 매핑할 수 있기 때문에 빌드 과정을 거치지 않고도 모듈 해석을 제어할 수 있습니다.

Import Map의 작동 방식

<script type="importmap">
  {
    "imports": {
      "dayjs": "https://cdn.skypack.dev/dayjs@1.10.7"
    }
  }
</script>
<script type="module">
  import dayjs from 'dayjs';

  console.log(dayjs('2019-01-25').format('YYYY-MM-DDTHH:mm:ssZ[Z]'));
</script>

Import Map은 HTML 문서의 <script type="importmap"> 태그를 통해 명시됩니다. 이 스크립트 태그는 모듈 해석을 수행하기 전 구문 분석될 수 있도록 문서의 첫 번째 <script type="module"> 태그 앞에 위치해야 합니다.(가급적 \에) 또한 Import Map은 현재 문서당 하나만 허용되지만, 향후 이 제한을 제거할 계획에 있습니다.

스크립트 태그 내 JSON 객체는 문서의 스크립트에서 필요한 모듈에 대해 요구되는 모든 매핑을 지정하는데 사용됩니다. Import Map의 구조는 일반적으로 다음과 같습니다.

<script type="importmap">
  {
    "imports": {
      "react": "https://cdn.skypack.dev/react@17.0.1",
      "react-dom": "https://cdn.skypack.dev/react-dom",
      "square": "./modules/square.js",
      "lodash": "/node_modules/lodash-es/lodash.js"
    }
  }
</script>

위의 imports 객체에서 각 속성은 매핑에 해당합니다. 매핑의 왼쪽은 import 지정자의 이름이고 오른쪽은 지정자에 매핑되어야 하는 상대 또는 절대 URL입니다. 매핑에서 상대 URL을 지정할 때는 항상 /, ../ 또는 ./로 시작해야 합니다. Import Map에 패키지가 있더라도 반드시 브라우저에 의해 로드되는 것은 아닙니다. 페이지의 스크립트에서 사용되지 않는 모듈은 Import Map에 있더라도 로드되지 않습니다.

<script type="importmap" src="importmap.json"></script>

외부 파일에서 매핑을 지정하고 src 속성을 사용해 파일에 연결할 수도 있습니다(위에서 볼 수 있듯). 이 방법을 사용하는 경우 Content-Type헤더가 application/importmap+json으로 설정되어 파일이 전송되었는지 확인해야 합니다. 성능상의 이유로 인라인 방식이 권장되며, 이 글에서 앞으로 보여드릴 예제에서도 이 방식을 사용합니다.

매핑을 지정하고 나면 다음과 같이 import 문에서 import 지정자를 사용할 수 있습니다.

<script type="module">
  import { cloneDeep } from 'lodash';

  const objects = [{ a: 1 }, { b: 2 }];

  const deep = cloneDeep(objects);
  console.log(deep[0] === objects[0]);
</script>

Import Map의 매핑은 <script> 태그의 src 속성과 같은 곳에 위치한 URL에 영향을 주지 않습니다. 따라서 <script src="/app.js"> 와 같이 사용하는 경우 브라우저는 Import Map의 내용과 관계없이 해당 경로에서 문자 그대로 app.js 파일을 다운로드하려고 시도합니다.

지정자를 패키지 전체에 매핑하기

지정자를 모듈에 매핑하는 것뿐 아니라 여러 모듈이 포함된 패키지에 매핑할 수도 있습니다. 슬래시로 끝나는 지정자와 경로를 사용하면 됩니다.

<script type="importmap">
  {
    "imports": {
      "lodash/": "/node_modules/lodash-es/"
    }
  }
</script>

이 방법을 사용하면 브라우저가 모든 구성 요소 모듈을 다운로드하도록 하는 메인 모듈 전체를 가져오는 대신 지정된 경로의 모듈을 가져올 수 있습니다.

<script type="module">
  import toUpper from 'lodash/toUpper.js';
  import toLower from 'lodash/toLower.js';

  console.log(toUpper('hello'));
  console.log(toLower('HELLO'));
</script>

동적으로 Import Map 구성하기

매핑은 임의의 조건에 따라 동적으로 구성될 수 있으며, 이를 이용해 기능 탐지 결과에 따라 모듈을 import 할 수 있습니다. 다음 예제에서는 IntersectionObserver API의 지원 여부에 따라 lazyload 지정자를 통해 가져올 올바른 파일을 선택합니다.

<script>
  const importMap = {
    imports: {
      lazyload: 'IntersectionObserver' in window ? './lazyload.js' : './lazyload-fallback.js',
    },
  };

  const im = document.createElement('script');
  im.type = 'importmap';
  im.textContent = JSON.stringify(importMap);
  document.currentScript.after(im);
</script>

이미 존재하는 Import Map을 수정하는 것은 효과가 없기 때문에 이 방법을 사용하고자 한다면 Import Map 스크립트 태그를 만들고 삽입하기 전에 사용해야 합니다.

해시를 매핑해 스크립트 캐시빌리티(Cacheability) 향상시키기

정적 파일을 장기간 캐시 하기 위한 일반적인 방법은 파일 이름에 파일 내용의 해시를 사용해 파일 내용이 변경될 때까지 브라우저 캐시에 파일이 남아있도록 하는 것입니다. 이 경우 업데이트시 최신 업데이트가 즉시 앱에 반영될 수 있도록 파일에 새 이름을 부여합니다.

종래의 스크립트 번들 방식에서는 여러 모듈이 의존하는 종속성 패키지를 업데이트했을 때 문제가 될 수 있습니다. 코드가 단 한글자만 변경된 경우에도 해당 종속성 패키지에 의존하는 모든 파일이 업데이트되어 브라우저에서 파일을 새로 다운로드해야 합니다.

Import Map은 재매핑(remapping) 기술을 통해 각 종속성을 별도로 업데이트할 수 있도록 해 이 문제를 해결합니다. 만약 post.bundle.8cb615d12a121f6693aa.js라는 파일에서 메서드를 가져와야 하는 경우 다음과 같은 Import Map을 사용할 수 있습니다.

<script type="importmap">
  {
    "imports": {
      "post.js": "./static/dist/post.bundle.8cb615d12a121f6693aa.js"
    }
  }
</script>

다음과 같은 import 문을

import { something } from './static/dist/post.bundle.8cb615d12a121f6693aa.js';

아래와 같이 작성할 수 있습니다.

import { something } from 'post.js';

파일을 업데이트하게 되면 Import Map만 업데이트하면 됩니다. export에 대한 참조는 변경되지 않으므로 업데이트된 해시로 인해 업데이트된 스크립트가 다시 다운로드되는 동안 브라우저에 캐시 된 상태로 유지됩니다.

<script type="importmap">
  {
    "imports": {
      "post.js": "./static/dist/post.bundle.6e2bf7368547b6a85160.js"
    }
  }
</script>

동일한 모듈을 여러 버전으로 사용하기

Import Map을 사용하면 손쉽게 동일한 패키지를 여러 버전으로 사용할 수 있습니다. 다음과 같이 매핑에서 다른 import 지정자를 사용하기만 하면 됩니다.

<script type="importmap">
  {
    "imports": {
      "lodash@3/": "https://unpkg.com/lodash-es@3.10.1/",
      "lodash@4/": "https://unpkg.com/lodash-es@4.17.21/"
    }
  }
</script>

스코프를 지정해 동일한 패키지의 다른 버전을 참조하도록 할 수도 있습니다. 이를 통해 주어진 스코프 내에서 import 지정자에 매핑된 경로를 변경할 수 있습니다.

<script type="importmap">
  {
    "imports": {
      "lodash/": "https://unpkg.com/lodash-es@4.17.21/"
    },
    "scopes": {
      "/static/js": {
        "lodash/": "https://unpkg.com/lodash-es@3.10.1/"
      }
    }
  }
</script>

위 매핑을 사용하면 /static/js 경로의 모든 모듈은 import에서 lodash/ 지정자를 참조할 때 https://unpkg.com/lodash-es@3.10.1/ URL을 사용하는 반면 다른 모듈들은 https://unpkg.com/lodash-es@4.17.21/를 사용합니다.

Import Map에서 NPM 패키지 사용하기

이 글에서 설명했듯 ES 모듈을 사용하는 모든 NPM 패키지의 프로덕션 버전을 ESM, Unpkg 또는 Skypack과 같은 CDN을 통해 Import Map에서 사용할 수 있습니다. NPM 패키지가 ES 모듈 시스템 및 브라우저의 기본 import 방식을 위해 설계되지 않았더라도 Skypack, ESM과 같은 서비스는 Import Map에서 사용할 수 있도록 변환해 줄 수 있습니다. Skypack 홈페이지의 검색창을 이용하면 빌드 단계를 거치지 않고 바로 사용할 수 있도록 브라우저에 최적화된 NPM 패키지를 찾을 수 있습니다.

프로그래밍을 통해 Import Map 지원 여부 파악하기

HTMLScriptElement.supports() 메서드가 지원되는 한 브라우저의 Import Map 지원 여부 파악이 가능합니다. 지원 여부 파악을 위해 다음 스니펫을 사용할 수 있습니다.

if (HTMLScriptElement.supports && HTMLScriptElement.supports('importmap')) {
  // Import Map 지원됨
}

오래된 브라우저 지원

Import Map을 사용하면 자바스크립트 생태계에 널리 퍼져 있는 복잡한 빌드 시스템에 의존하지 않고 베어(bare) 모듈 지정자를 사용 가능하지만 현재 대부분의 브라우저에서 지원되지 않고 있습니다. 글 작성 시점을 기준으로 89 버전 이상의 Chrome 및 Edge 브라우저는 완벽히 지원하지만, Firefox, Safari 및 일부 모바일 브라우저는 이 기술을 지원하지 않습니다. 이러한 브라우저들에서 Import Map을 사용하려면 적절한 폴리필을 사용해야 합니다.

사용 가능한 폴리필로는 ES 모듈을 지원하는 모든 브라우저에 Import Map 을 포함한 기타 새로운 모듈 기능에 대한 지원을 추가해 주는 ES Module Shims 폴리필이 있습니다. HTML 파일의 Import Map 스크립트의 앞에 es-module-shim 스크립트를 추가하면 됩니다.

<script async src="https://unpkg.com/es-module-shims@1.3.0/dist/es-module-shims.js"></script>

폴리필을 추가한 이후에도 콘솔에 자바스크립트 TypeError가 표시될 수 있습니다. 이 오류는 사용자에게 아무런 영향을 미치지 않으므로 무시해도 좋습니다.

Uncaught TypeError: Error resolving module specifier “lodash/toUpper.js”. Relative module specifiers must start with “./”, “../” or “/”.

Import Map에 관한 다른 폴리필이나 툴들은 Github 저장소에서 찾아볼 수 있습니다.

결론

Import Map은 상대 또는 절대 URL을 통한 import에 제한되지 않고 브라우저에서 ES 모듈을 사용할 수 있는 더 현명한 방법을 제공합니다. 이는 import 문을 변경할 필요 없이 코드를 쉽게 이동할 수 있으며 개별 모듈의 업데이트는 해당 모듈에 의존하는 스크립트의 캐시빌리티에 영향을 주지 않고 보다 원활하게 수행됩니다. 전반적으로 Import Map은 서버와 브라우저에서 ES 모듈을 활용하는 방식을 동일하게 만듭니다.

현재 사용 중인 빌드 시스템을 대체하거나 보완하기 위해 Import Map을 사용하실 건가요? 트위터를 통해 어떤 이유로 사용하기로 혹은 사용하지 않기로 결정했는지 알려주세요.

읽어주셔서 감사합니다. 즐거운 코딩 되시길!

🚀 한국어로 된 프런트엔드 아티클을 빠르게 받아보고 싶다면 Korean FE Article(https://kofearticle.substack.com/)을 구독해주세요!

profile
FE 개발을 하고 있어요🌱

2개의 댓글

comment-user-thumbnail
2023년 3월 10일

좋은 글 감사합니다!!!

답글 달기
comment-user-thumbnail
2023년 11월 29일

Thank you so much for this wonderful Immaculate grid essay!!!

답글 달기