(번역) 빌드 시스템 없이 프런트엔드 자바스크립트 라이브러리 불러오기
원문: https://jvns.ca/blog/2024/11/18/how-to-import-a-javascript-library/
저는 빌드 시스템 없이 자바스크립트를 작성하다는 것을 선호하는데, 어제 어김없이 코드에서 자바스크립트 라이브러리를 불러오는 방법을 알아내야 하는 문제에 부딪혔습니다. 그런데 빌드 시스템을 사용하지 않다 보니 그 방법을 알아내는 데 엄청나게 오랜 시간이 걸렸습니다. 대부분의 라이브러리 설정 가이드가 빌드 시스템을 사용하는 것을 전제로 작성되어 있기 때문입니다.
다행히 이제는 이런 상황을 어느 정도 해결하는 방법을 익혀서, 라이브러리를 성공적으로 사용하거나 너무 어렵다면 다른 라이브러리로 전환할 수 있게 되었습니다. 그래서 몇 년 전에 이런 가이드를 가지고 있었으면 좋았을 법한 내용을 여기 공유해보려고 합니다.
이 글에서는 프런트엔드에서 자바스크립트 라이브러리를 사용하는 방법, 특히 빌드 시스템 없이 사용하는 방법에 대해서만 다룰 예정입니다.
이 포스트에서 다룰 내용은 아래와 같습니다.
- 라이브러리가 제공할 수 있는 세 가지 주요 자바스크립트 파일 유형 (ES 모듈, “클래식” 전역 변수 유형, CommonJS)
- 자바스크립트 라이브러리의 빌드에 어떤 유형의 파일이 포함되어 있는지 파악하는 방법
- 각 유형의 파일을 코드에서 불러오는 방법
세 가지 종류의 자바스크립트 파일
라이브러리가 제공할 수 있는 3가지 기본 자바스크립트 파일 유형이 있습니다.
- 가장 먼저 전역 변수를 정의하는 “클래식” 유형의 파일입니다.
<script src>
를 추가해 바로 사용할 수 있는 종류의 파일을 의미합니다. 가장 간단하고 편리하지만, 항상 사용할 수 있지는 않습니다. - ES 모듈 (다른 파일에 의존할 수도 있고 아닐 수도 있습니다. 이에 대해서는 뒤에서 다루겠습니다.)
- “CommonJS” 모듈입니다. 이는 Node용이며, 빌드 시스템 없이는 브라우저에서 전혀 사용할 수 없습니다.
“클래식” 유형에 대한 더 좋은 이름이 있는지 모르겠지만 그냥 “클래식”이라고 부르겠습니다. “AMD”라는 유형도 있지만 2024년에 얼마나 관련이 있는지는 잘 모르겠습니다.
이제 3가지 유형을 알았으니, 라이브러리가 실제로 이 중 어떤 것을 제공하는지 알아내는 방법에 관해 이야기해 보겠습니다!
파일을 찾을 수 있는 곳: NPM 빌드
모든 자바스크립트 라이브러리는 빌드를 생성하여 NPM에 업로드합니다. 아마도 (제가 원래 생각했던 것처럼) — “잠깐만요! Node로 빌드하지 않는 게 핵심인데, 왜 갑자기 NPM 얘기가 나오는 거죠?”라고 생각하실 수 있습니다.
하지만 https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.4.1/chart.umd.min.js
와 같은 CDN 링크를 사용하더라도 여전히 NPM 빌드를 사용하고 있는 것을 의미합니다! CDN의 모든 파일은 원래 NPM에서 가져온 것입니다.
이 때문에 라이브러리를 빌드하는 데 Node를 전혀 사용할 계획이 없더라도 때때로 라이브러리를 npm install
하는 것을 좋아합니다. 새로운 임시 폴더를 만들고, 거기에 npm install
을 한 다음, 작업이 끝나면 삭제합니다. 저는 NPM 빌드의 파일을 파일 시스템에서 살펴볼 수 있는 것을 좋아합니다. 왜냐하면 라이브러리가 빌드에서 제공하는 모든 것을 100% 확실하게 볼 수 있고 CDN이 무언가를 숨기고 있지 않다는 것을 알 수 있기 때문입니다.
그럼 몇 가지 라이브러리를 npm install
하고 빌드에서 어떤 유형의 자바스크립트 파일을 제공하는지 알아보겠습니다!
예시 라이브러리 1: chart.js
먼저 플로팅 라이브러리인 Chart.js 내부를 살펴보겠습니다.
$ cd /tmp/whatever
$ npm install chart.js
$ cd node_modules/chart.js/dist
$ ls *.*js
chart.cjs chart.js chart.umd.js helpers.cjs helpers.js
이 라이브러리는 3가지 기본 옵션을 가지고 있는 것으로 보입니다.
옵션 1: chart.cjs
: .cjs
접미사는 이 파일이 Node에서 사용하는 CommonJS 파일이라는 것을 알려줍니다. 즉, 어떤 종류의 빌드 단계 없이는 브라우저에서 직접 사용하는 것이 불가능합니다.
옵션 2: chart.js
: .js
접미사만으로는 어떤 종류의 파일인지 알 수 없지만, 열어보면 import '@kurkle/color';
가 보이는데 이는 ES 모듈이라는 것을 나타내는 명확한 신호입니다. import ...
는 ES 모듈 구문이기 때문입니다.
옵션 3: chart.umd.js
: "UMD"는 "Universal Module Definition"의 약자로, 기본적인 <script src>
, CommonJS, 또는 제가 이해하지 못하는 AMD 방식에서 모두 이 파일을 사용할 수 있다는 의미입니다.
UMD 파일 사용 방법
Chart.js를 사용할 때 저는 옵션 3을 선택했습니다. 코드에 이것만 추가하면 됩니다.
<script src="./chart.umd.js"></script>
그런 다음 전역 Chart
환경 변수로 라이브러리를 사용할 수 있었습니다. 이보다 더 쉬울 수 없습니다. chart.umd.js
를 제 깃 저장소에 복사해 두기만 하면 NPM이나 CDN이 다운되는 것에 대해 걱정할 필요가 없었습니다.
빌드 파일이 항상 dist
디렉터리에 있는 것은 아닙니다
많은 라이브러리가 빌드를 dist
디렉터리에 넣지만, 항상 그런 것은 아닙니다! 빌드 파일의 위치는 라이브러리의 package.json
에 명시되어 있습니다.
예를 들어 Chart.js의 package.json에서 발췌한 내용입니다.
"jsdelivr": "./dist/chart.umd.js",
"unpkg": "./dist/chart.umd.js",
"main": "./dist/chart.cjs",
"module": "./dist/chart.js",
이는 ES 모듈(module
)을 사용하려면 dist/chart.js
를 사용해야 하지만, jsDelivr와 unpkg CDN은 ./dist/chart.umd.js
를 사용해야 한다는 것을 말하는 것 같습니다. main
은 Node용인 것 같습니다.
chart.js
의 package.json
에는 "type": "module"
도 있는데, 이 문서에 따르면 Node에게 파일을 기본적으로 ES 모듈로 취급하라고 알려줍니다. 정확히 어떤 파일이 ES 모듈이고 어떤 파일이 아닌지는 알려주지 않지만, 그 안에 ES 모듈이 있다는 것을 알려줍니다.
예시 라이브러리 2: @atcute/oauth-browser-client
@atcute/oauth-browser-client
는 브라우저에서 OAuth를 사용하여 Bluesky에 로그인하기 위한 라이브러리입니다.
빌드에서 어떤 종류의 자바스크립트 파일을 제공하는지 살펴보겠습니다!
$ npm install @atcute/oauth-browser-client
$ cd node_modules/@atcute/oauth-browser-client/dist
$ ls *js
constants.js dpop.js environment.js errors.js index.js resolvers.js
여기서 그럴듯한 루트 파일은 index.js
뿐인 것 같습니다. 어떻게 되어있는지 함께 살펴보죠.
export { configureOAuth } from "./environment.js";
export * from "./errors.js";
export * from "./resolvers.js";
이 export
구문은 ES 모듈이라는 것을 의미합니다. 즉, 빌드 단계 없이 브라우저에서 사용할 수 있습니다! 어떻게 하는지 살펴보겠습니다.
importmaps로 ES 모듈 사용하기
ES 모듈을 사용하는 것은 <script src="whatever.js">
를 추가하는 것만큼 쉽지 않습니다. 대신, ES 모듈에 의존성이 있는 경우(@atcute/oauth-browser-client
처럼) 다음 단계를 따라야 합니다.
- HTML에 import map 설정
- 자바스크립트 코드에
import { configureOAuth } from '@atcute/oauth-browser-client';
와 같은 import문 추가 - HTML에
<script type="module" src="YOURSCRIPT.js"></script>
와 같은 자바스크립트 코드 추가
단순히 import { BrowserOAuthClient } from "./oauth-client-browser.js"
와 같이 할 수 있지만, 내부적으로 모듈 내부에 import {something} from @atcute/client
와 같이 또 다른 import 문을 가지고 있다면, 브라우저에게 @atcute/client
와 그 외의 모든 의존성에 대한 코드를 어디서 가져올지 알려줘야 합니다. 이를 위해 import map이 필요합니다.
@atcute/oauth-browser-client
를 위해 제가 사용한 importmap은 다음과 같습니다.
<script type="importmap">
{
"imports": {
"nanoid": "./node_modules/nanoid/bin/dist/index.js",
"nanoid/non-secure": "./node_modules/nanoid/non-secure/index.js",
"nanoid/url-alphabet": "./node_modules/nanoid/url-alphabet/dist/index.js",
"@atcute/oauth-browser-client": "./node_modules/@atcute/oauth-browser-client/dist/index.js",
"@atcute/client": "./node_modules/@atcute/client/dist/index.js",
"@atcute/client/utils/did": "./node_modules/@atcute/client/dist/utils/did.js"
}
}
</script>
이러한 import map을 작동시키는 것은 꽤 까다롭습니다. 자동으로 생성하는 도구가 있을 것 같은데 아직 찾지 못했습니다. esbuild의 metafile을 사용하여 importmaps를 자동으로 생성하는 스크립트를 작성하는 것은 분명히 가능하지만, 아직 그렇게 하지 않았고 더 나은 방법이 있을 수 있습니다.
어제 github.com/jvns/bsky-oauth-example을 작동시키기 위해 importmaps를 설정하기로 결정했으므로, 해당 저장소에 예제 코드가 있습니다.
또한, 누군가 Simon Willison의 download-esm이라는 도구를 알려줬는데, 이 도구는 ES 모듈을 다운로드하고 imports 구문을 JS 파일 경로로 재작성해줘 importmaps이 필요 없게 해줍니다. 직접 사용해보지는 않았지만 정말 좋은 아이디어인 것 같습니다.
importmaps의 문제점: 너무 많은 파일
하지만 브라우저에서 importmaps를 사용할 때 몇 가지 문제가 발생했습니다. 사이트를 로드하기 위해 수십 개의 자바스크립트 파일을 다운로드해야 했고, 개발 환경의 웹서버가 어떤 이유에서인지 따라가지 못했습니다. 파일들이 무작위로 로드에 계속 실패했고, 페이지를 다시 로드하고 성공하기를 바라야 했습니다.
프로덕션에 배포했을 때는 더 이상 문제가 발생하지 않았기 때문에, 제 로컬 개발 환경의 문제였던 것 같습니다.
또한 ES 모듈의 약간 귀찮은 점은 웹서버를 실행해야 사용할 수 있다는 것입니다. 이것은 분명 좋은 이유가 있겠지만, 웹서버를 시작하지 않고 index.html
파일을 열 수 있을 때가 더 쉽습니다.
“너무 많은 파일” 이라는 문제 때문에, 이런 방식으로 importmap과 함께 ES 모듈을 사용하는 것이 사실 저에게는 크게 매력적이지는 않습니다. 하지만 가능하다는 것을 알게 되어 좋습니다.
importmaps 없이 ES 모듈 사용하기
ES 모듈에 의존성이 없다면 더 쉽습니다. importmaps가 필요 없습니다! 다음과 같이 할 수 있습니다.
- HTML에
<script type="module" src="YOURCODE.js"></script>
를 넣습니다.type="module"
이 중요합니다. YOURCODE.js
에import {whatever} from "https://example.com/whatever.js"
를 넣습니다.
대안: esbuild 사용하기
importmaps를 사용하고 싶지 않다면 esbuild와 같은 빌드 시스템을 사용할 수도 있습니다. Some notes on using esbuild에서 그 방법에 관해 이야기했지만, 이 글은 빌드 시스템을 완전히 배제하는 방법을 다루고 있으므로 여기서는 그 옵션에 관해 이야기하지 않겠습니다. 하지만 저는 여전히 esbuild를 좋아하고 이 경우에 좋은 옵션이라고 생각합니다.
importmaps의 브라우저 지원은 어떻습니까?
CanIUse에 따르면 importmaps는 “2023년 기준: 주요 브라우저에서 모두 지원됨”이니 2024년에는 여전히 조금 새로운 것일 수 있습니다. 저와 12명 정도만 사용하기를 원하는 재미있는 실험적 코드에는 importmaps를 사용하겠지만, 코드를 더 널리 사용하고 싶다면 대신 esbuild를 사용할 것 같습니다.
예시 라이브러리 3: @atproto/oauth-client-browser
마지막 예시 라이브러리를 살펴보겠습니다! 이 라이브러리는 @atcute/oauth-browser-client
와는 다른 Bluesky 인증 라이브러리입니다.
$ npm install @atproto/oauth-client-browser
$ cd node_modules/@atproto/oauth-client-browser/dist
$ ls *js
browser-oauth-client.js browser-oauth-database.js browser-runtime-implementation.js errors.js index.js indexed-db-store.js util.js
여기서도 가능한 루트 파일은 index.js
뿐인 것 같습니다. 하지만 이전 예시 라이브러리와는 다른 상황입니다! index.js
를 살펴보겠습니다.
index.js
에는 이런 내용들이 들어 있습니다.
__exportStar(require("@atproto/oauth-client"), exports);
__exportStar(require("./browser-oauth-client.js"), exports);
__exportStar(require("./errors.js"), exports);
var util_js_1 = require("./util.js");
이 require()
구문은 CommonJS 구문이며, 이는 브라우저에서 이 파일을 전혀 사용할 수 없고 어떤 종류의 빌드 단계가 필요하다는 것을 의미하며, ESBuild도 작동하지 않습니다.
또한 이 라이브러리의 package.json에는 "type": "commonjs"
라고 되어 있어 이것이 CommonJS라는 것을 알 수 있습니다.
esm.sh로 CommonJS 모듈 사용하기
처음에는 빌드 시스템을 배우지 않고는 CommonJS 모듈을 사용하는 것이 불가능하다고 생각했지만, 그때 Bluesky에서 누군가 esm.sh에 대해 알려주었습니다! 이 도구는 모든 것을 ES 모듈로 변환하는 CDN입니다. skypack.dev도 비슷한 일을 하는데, 차이점이 무엇인지는 잘 모르지만 한 사람이 하나가 작동하지 않으면 때때로 다른 것을 시도한다고 언급했습니다.
@atproto/oauth-client-browser
의 경우 사용하는 것이 꽤 간단해 보입니다. HTML에 다음과 같이 넣기만 하면 됩니다.
<script type="module" src="script.js"></script>
그리고 script.js
에 아래 코드를 넣습니다.
import { BrowserOAuthClient } from "https://esm.sh/@atproto/oauth-client-browser@0.3.0";
잘 작동하는 것 같습니다. 좋네요! 물론 이것도 일종의 빌드 시스템을 사용하는 것입니다. 단지 esm.sh가 제 대신 빌드를 실행하는 것뿐입니다. 하지만, 이 접근 방식에 아래와 같은 몇가지 우려사항이 있습니다.
- CDN이 영원히 작동할 것이라고 믿지 않습니다. 보통 미래에 어떤 이유로 사라지지 않도록 의존성을 제 저장소에 복사해 두는 것을 좋아합니다.
- CDN이 보안 문제를 겪은 사례를 들은 적이 있어서 걱정됩니다.
- esm.sh가 무엇을 하는지 잘 이해하지 못합니다.
- esbuild도 CommonJS 모듈을 ES 모듈로 변환할 수 있습니다.
또한 esbuild를 사용하여 CommonJS 모듈을 ES 모듈로 변환할 수 있다는 것도 알게 되었습니다. 하지만 몇 가지 제한사항이 있습니다. import { BrowserOAuthClient } from
구문은 작동하지 않습니다. 여기 관련 github 이슈에 관련된 내용이 있습니다.
제 생각에는 esbuild
접근 방식이 esm.sh
접근 방식보다 더 매력적일 것 같습니다. 이미 제 컴퓨터에 있는 도구이기 때문에 더 신뢰할 수 있기 때문입니다. 하지만 아직 이것에 대해 많이 실험해보지는 않았습니다.
세 가지 유형의 파일 요약
이제 라이브러리가 제공할 수 있는 세 가지 유형의 자바스크립트 파일과 그 사용 방법, 식별 방법을 요약해 보겠습니다.
도움이 되지 않게도 .js
나 .min.js
파일 확장자는 이 세 가지 옵션 중 어느 것이나 될 수 있으므로, 파일이 something.js
인 경우 무엇을 다루고 있는지 더 자세히 조사해야 합니다.
“클래식” JS 파일
사용 방법: <script src="whatever.js"></script>
✔️ 식별 방법:
- 웹사이트에 “CDN으로 사용하세요!”와 같은 큰 친절한 배너가 있는 경우
.umd.js
확장자<script src=...
태그에 넣어보고 작동하는지 확인
ES 모듈
✔️ 사용 방법:
- 의존성이 없는 경우, 코드에서 직접
import {whatever} from "./my-module.js"
사용 - 의존성이 있는 경우, importmap을 만들고
import {whatever} from "my-module"
사용 - 또는 download-esm을 사용하여 importmap 필요성 제거
- esbuild나 다른 ES 모듈 번들러 사용
✔️ 식별 방법:
import
나export
문 찾기 (module.exports = ...
가 아닌 경우, 그것은 CommonJS).mjs
확장자package.json
의"type": "module"
(정확히 어떤 파일을 가리키는지는 명확하지 않음)
CommonJS 모듈
✔️ 사용 방법:
- https://esm.sh를 사용하여 ES 모듈로 변환 (예:
https://esm.sh/@atproto/oauth-client-browser@0.3.0
) - 어떻게든 빌드 사용 (??)
✔️식별 방법:
- 코드에서
require()
나module.exports = ...
찾기 .cjs
확장자package.json
의"type": "commonjs"
(정확히 어떤 파일을 가리키는지는 명확하지 않음)
ES 모듈이 표준화된 것은 정말 좋습니다
제 관점에서 CommonJS 모듈과 ES 모듈의 주요 차이점은 ES 모듈이 실제로 표준이라는 것입니다. 이는 브라우저가 웹 표준에 대한 하위 호환성을 영원히 약속하기 때문에 더 자신있게 사용할 수 있게 해줍니다. 오늘 ES 모듈을 사용하여 코드를 작성하면 15년 후에도 동일한 방식으로 작동할 것이라고 확신할 수 있습니다.
또한, ES 모듈이 표준이기 때문에 esbuild
와 같은 도구를 사용할 때도 더 안심이 됩니다. 설령 esbuild 프로젝트가 중단되더라도, 표준을 구현하는 도구이기 때문에 미래에 이를 대체할 비슷한 도구가 나올 가능성이 높다고 느껴집니다.
JS 커뮤니티는 많은 매우 멋진 도구를 만들었습니다
이런 것들에 대해 이야기할 때마다 “자바스크립트가 싫어요!!! 최악이에요!!!”와 같은 반응을 많이 받습니다. 하지만 제 경험상 자바스크립트는 많은 훌륭한 도구가 있고(어제 알게 된 https://esm.sh도 훌륭해 보입니다! esbuild도 좋아합니다!), 어떻게 작동하는지 이해하는 데 시간을 투자하면 그런 도구들을 활용하여 제 삶을 훨씬 더 쉽게 만들 수 있습니다.
이 글의 목적은 절대 자바스크립트에 대해 불평하는 것이 아니라, 제가 좋아하는 방식으로 도구를 사용할 수 있도록 전반적인 상황을 이해하는 것입니다.
아직 남은 질문들
아직 가지고 있는 몇 가지 질문들입니다. 답을 알게 되면 포스트에 추가하겠습니다.
- 로컬에 설정한 ES 모듈에 대한 importmaps를 자동으로 생성하는 도구가 있나요? (답변: 네, jspm이 있습니다)
- esm.sh가 하는 것처럼 제 컴퓨터에서 CommonJS 모듈을 ES 모듈로 어떻게 변환할 수 있나요? (답변: esbuild가 어느 정도 할 수 있지만, named exports는 작동하지 않습니다
- CommonJS 모듈을 일반 자바스크립트 코드로 빌드할 때 실제로 어떤 코드가 그 작업을 수행하나요? webpack, rollup, esbuild 같은 도구들이 있지만, 이 도구들이 각자 자신만의 JS 파서/정적 분석기를 구현하나요? JS 파서는 실제로 얼마나 많이 존재하나요?
- ES 모듈을 단일 파일(예: atcute-client.js)로 번들링하면서도, 브라우저에서 여러 다른 경로(예: @atcute/client/lexicons와 @atcute/client 둘 다)에서 불러올 수 있는 방법이 있나요?
모든 도구들
이 포스트에서 언급한 모든 도구의 목록입니다.
- Simon Willison의 download-esm은 ES 모듈을 다운로드하고 importmap이 필요 없도록 imports를 JS 파일을 직접 가리키도록 변환합니다.
- https://esm.sh/ 와 skypack.dev
- esbuild
- importmaps를 생성할 수 있는 JSPM
이 글을 작성하면서, 보통은 프로젝트를 업데이트할 때마다 실행하는 빌드를 원하지 않지만, 프로젝트를 설정할 때 한 번만 실행하고 의존성 버전을 업데이트할 때만 다시 실행하는 빌드 단계(download-esm
이나 다른 것을 사용하는)는 괜찮을 것 같다고 생각하게 되었습니다.
이상입니다!
이 포스트에 있는 많은 것들을 가르쳐준 Marco Rogers님 감사드립니다. 이 포스트에서 몇 가지 실수를 했을 수 있으니 어떤 것들인지 알려주시면 감사하겠습니다. Bluesky나 Mastodon에서 알려주세요!