빌드 시스템 없이 프론트엔드 자바스크립트 라이브러리 불러오기
저는 빌드 시스템 없이 자바스크립트를 쓰는 걸 좋아합니다. 그런데 어제 또, 정말 셀 수 없이 겪어 온 일이지만 빌드 시스템 없이 코드에서 자바스크립트 라이브러리를 import해야 하는 상황에 부딪혔고, 라이브러리의 설정 안내가 빌드 시스템을 사용한다는 전제로 되어 있어서 가져오는 방법을 알아내는 데 정말 한참이 걸렸습니다.
다행히 이제는 이런 상황을 헤쳐 나가는 법을 어느 정도 익혀서 라이브러리를 성공적으로 사용하거나, 너무 어렵다고 판단되면 다른 라이브러리로 갈아타곤 합니다. 그래서 몇 년 전에 있었더라면 좋았을 자바스크립트 라이브러리 가져오기 가이드를 여기에 정리해 봅니다.
이 글에서는 프론트엔드에서 자바스크립트 라이브러리를 사용하는 방법만, 그것도 빌드 시스템이 없는 환경에서 사용하는 방법만 다룹니다.
이 글에서 다룰 내용은 다음과 같습니다.
- 라이브러리가 제공할 수 있는 자바스크립트 파일의 세 가지 주요 유형(ES 모듈, “클래식한” 전역 변수 방식, 그리고 CommonJS)
- 자바스크립트 라이브러리가 빌드에 포함하는 파일 유형을 알아내는 방법
- 각 유형의 파일을 코드에서 가져오는 방법
자바스크립트 파일의 세 가지 종류
라이브러리가 제공할 수 있는 자바스크립트 파일에는 기본적으로 세 가지 유형이 있습니다.
- 전역 변수를 정의하는 “클래식한” 유형의 파일입니다. 그냥
<script src>로 불러오면 바로 동작하는 파일입니다. 구할 수 있다면 가장 편하지만 항상 제공되는 건 아닙니다. - ES 모듈(다른 파일에 의존할 수도 있고 아닐 수도 있는데, 이 부분은 뒤에서 설명합니다)
- “CommonJS” 모듈입니다. Node용으로, 빌드 시스템 없이는 브라우저에서 전혀 사용할 수 없습니다.
“클래식한” 유형에 더 좋은 이름이 있는지는 잘 모르겠지만 그냥 “클래식”이라고 부르겠습니다. “AMD”라는 유형도 있지만 2024년 기준으로 얼마나 의미가 있는지는 잘 모르겠습니다.
이제 세 가지 유형을 알았으니, 라이브러리가 실제로 어떤 유형을 제공하는지 알아보는 방법을 이야기해 보겠습니다.
파일은 어디에 있을까: NPM 빌드
모든 자바스크립트 라이브러리는 NPM에 업로드하는 빌드를 가지고 있습니다. “Julia! 핵심은 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 빌드 안의 파일들을 직접 둘러보면, 라이브러리가 빌드에서 실제로 제공하는 모든 것을 빠짐없이 보고 있다는 확신이 들고, 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
이 라이브러리에는 기본적으로 세 가지 옵션이 있는 것 같습니다.
옵션 1: chart.cjs. .cjs 확장자를 보면 이 파일이 CommonJS 파일, 즉 Node에서 쓰기 위한 파일이라는 걸 알 수 있습니다. 따라서 어떤 빌드 단계 없이는 브라우저에서 직접 사용할 수 없습니다.
옵션 2:chart.js. .js 확장자만으로는 어떤 종류의 파일인지 알 수 없지만, 파일을 열어 보면 import '@kurkle/color';라는 구문이 보입니다. 이 import ... 구문은 ES 모듈 문법이므로 이 파일이 ES 모듈이라는 걸 바로 알 수 있습니다.
옵션 3: chart.umd.js. “UMD”는 “Universal Module Definition”의 약자로, 이 파일을 기본적인 <script src>나 CommonJS, 혹은 제가 잘 모르는 AMD라는 세 번째 방식으로도 사용할 수 있다는 뜻이라고 이해하고 있습니다.
UMD 파일을 사용하는 방법
Chart.js를 사용할 때 저는 옵션 3을 골랐습니다. 코드에 아래 한 줄만 추가하면 됐습니다.
<script src="./chart.umd.js"> </script>
그러면 전역 Chart 변수로 라이브러리를 사용할 수 있었습니다. 더없이 간단했습니다. NPM이나 CDN 장애를 걱정하지 않아도 되도록 chart.umd.js 파일을 그대로 제 Git 저장소에 복사해 두었습니다.
빌드 파일이 항상 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는 브라우저에서 Bluesky에 OAuth로 로그인하기 위한 라이브러리입니다.
이 라이브러리가 빌드에서 어떤 종류의 자바스크립트 파일을 제공하는지 살펴보겠습니다.
$ 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 모듈이라는 뜻입니다. 즉, 빌드 단계 없이도 브라우저에서 사용할 수 있다는 의미입니다. 어떻게 사용하는지 살펴보겠습니다.
importmap으로 ES 모듈 사용하는 방법
ES 모듈을 사용하는 건 단순히 <script src="whatever.js">를 추가하는 것만큼 쉽지 않습니다. 대신 ES 모듈에 의존성이 있다면(예: @atcute/oauth-browser-client처럼) 단계는 다음과 같습니다.
- HTML에 import map을 설정합니다.
- JS 코드에
import { configureOAuth } from '@atcute/oauth-browser-client';같은 import 문을 넣습니다. - HTML에 JS 코드를
<script type="module" src="YOURSCRIPT.js"></script>형태로 포함합니다.
단순히 import { BrowserOAuthClient } from "./oauth-client-browser.js" 같은 방식으로 하지 않고 import map이 필요한 이유는, 모듈 내부에 import {something} from @atcute/client 같은 import 문이 더 있고, 브라우저에게 @atcute/client와 그 외 모든 의존성의 코드를 어디서 가져와야 하는지 알려줘야 하기 때문입니다.
제가 @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을 이용해 importmap을 자동으로 생성하는 스크립트를 작성하는 것도 분명 가능하지만, 아직 해 보지 않았고 어쩌면 더 좋은 방법이 있을지도 모르겠습니다.
어제 github.com/jvns/bsky-oauth-example을 동작하게 하려고 importmap을 설정했는데, 해당 저장소에 예제 코드가 있습니다.
또 Simon Willison이 만든 download-esm이라는 도구를 추천받았는데, ES 모듈을 다운로드한 뒤 import 경로를 JS 파일을 직접 가리키도록 바꿔 주어서 importmap이 필요 없게 해 줍니다. 아직 직접 써 보지는 않았지만 아주 좋은 아이디어 같습니다.
importmap의 문제: 파일이 너무 많습니다
다만 브라우저에서 importmap을 사용하면서 몇 가지 문제를 겪었습니다. 사이트를 로드하는 데 수십 개의 자바스크립트 파일을 다운로드해야 했고, 개발 환경의 웹서버가 어쩐지 이를 감당하지 못했습니다. 파일 로드가 무작위로 실패하는 일이 계속 생겼고, 그때마다 페이지를 새로고침해서 이번엔 성공하길 바라야 했습니다.
사이트를 프로덕션에 배포하고 나서는 더 이상 문제가 없었으니, 아마 로컬 개발 환경의 문제였던 것 같습니다.
또 ES 모듈 전반에 대해 약간 불편한 점 하나는, 사용하려면 웹서버를 실행해야 한다는 것입니다. 분명 타당한 이유가 있겠지만, 웹서버를 켜지 않고 그냥 index.html 파일을 열 수 있을 때가 더 편한 건 사실입니다.
“파일이 너무 많다”는 문제 때문에, 이런 식으로 importmap과 함께 ES 모듈을 실제로 사용하는 것은 제게는 그다지 매력적으로 느껴지지 않지만, 가능하다는 걸 알아두는 것 자체는 좋습니다.
importmap 없이 ES 모듈 사용하는 방법
ES 모듈에 의존성이 없다면 훨씬 더 쉽습니다. importmap이 필요 없습니다! 그냥 다음처럼 하면 됩니다.
- HTML에
<script type="module" src="YOURCODE.js"></script>를 넣습니다.type="module"이 중요합니다. YOURCODE.js안에import {whatever} from "https://example.com/whatever.js"를 넣습니다.
대안: esbuild 사용하기
importmap을 사용하고 싶지 않다면 esbuild 같은 빌드 시스템을 사용할 수도 있습니다. 그 방법에 대해서는 Some notes on using esbuild에서 설명한 적이 있습니다. 하지만 이 글은 빌드 시스템을 완전히 피하는 방법에 대한 글이므로 여기서는 그 옵션을 다루지 않겠습니다. 저는 여전히 esbuild를 좋아하고 이 경우에도 좋은 선택지라고 생각합니다.
importmap의 브라우저 지원은 어떤가요?
CanIUse에 따르면 importmap은 “Baseline 2023: 주요 브라우저에서 새롭게 사용 가능” 상태라고 합니다. 그래서 제 느낌으로는 2024년 기준으로는 아직 조금 이른 감이 있습니다. 저라면 저와 열두 명 정도만 쓰는 재미있는 실험용 코드에는 importmap을 쓰겠지만, 코드를 더 널리 쓰이게 하고 싶다면 대신 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/[email protected]"
그냥 동작하는 것 같아서 신기합니다! 물론 이것도 여전히 일종의 빌드 시스템을 사용하는 셈입니다. 다만 제가 아니라 esm.sh가 빌드를 실행해 주는 것뿐입니다. 이 접근 방식에 대해 제가 우려하는 점은 다음과 같습니다.
- CDN이 영원히 잘 동작할 거라고 믿기 어렵습니다. 저는 보통 의존성을 저장소에 복사해 두어 나중에 어떤 이유로든 사라지는 일이 없도록 하는 걸 좋아합니다.
- CDN이 보안 침해를 당했다는 이야기를 들은 적이 있어서 그 점이 걱정됩니다.
- esm.sh가 정확히 무엇을 하는지 잘 모르겠습니다.
esbuild로도 CommonJS 모듈을 ES 모듈로 변환할 수 있습니다
또 esbuild를 사용해서도 CommonJS 모듈을 ES 모듈로 변환할 수 있다는 걸 알게 되었는데, 몇 가지 제약이 있습니다. import { BrowserOAuthClient } from 구문이 동작하지 않습니다. 이에 대한 GitHub 이슈가 있습니다.
저에게는 esbuild 방식이 esm.sh 방식보다 조금 더 끌리는데, 이미 제 컴퓨터에 있는 도구라 더 신뢰가 가기 때문입니다. 아직 이 부분을 많이 실험해 보지는 않았습니다.
세 가지 파일 유형 정리
지금까지 마주칠 수 있는 세 가지 JS 파일 유형과 각 파일을 사용하는 방법, 그리고 식별하는 방법을 정리해 보겠습니다.
도움이 안 되게도 .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"로 가져옵니다.- 또는 importmap이 필요 없도록 download-esm을 사용합니다.
- esbuild나 다른 ES 모듈 번들러를 사용합니다.
- 의존성이 없으면 코드에서 바로
- 식별 방법:
import나export구문을 찾습니다. (module.exports = ...는 CommonJS입니다.).mjs확장자package.json의"type": "module"(다만 정확히 어떤 파일을 가리키는지는 명확하지 않습니다)
- 사용 방법:
- CommonJS 모듈
- 사용 방법:
- https://esm.sh을 사용해 ES 모듈로 변환합니다. 예:
https://esm.sh/@atproto/[email protected] - 어떤 식으로든 빌드를 사용합니다(??)
- https://esm.sh을 사용해 ES 모듈로 변환합니다. 예:
- 식별 방법:
- 코드에서
require()나module.exports = ...를 찾습니다. .cjs확장자package.json의"type": "commonjs"(다만 정확히 어떤 파일을 가리키는지는 명확하지 않습니다)
- 코드에서
- 사용 방법:
ES 모듈이 표준으로 정해진 건 정말 좋습니다
제 관점에서 CommonJS 모듈과 ES 모듈의 가장 큰 차이는 ES 모듈이 실제로 표준이라는 점입니다. 덕분에 훨씬 더 안심하고 사용할 수 있습니다. 브라우저는 웹 표준에 대해 영원히 하위 호환성을 보장하기로 약속하기 때문에, 오늘 ES 모듈을 사용해 코드를 작성하면 15년 뒤에도 똑같이 동작할 거라고 확신할 수 있습니다.
또 esbuild 같은 도구를 사용하는 것도 마음이 놓입니다. esbuild 프로젝트가 중단되더라도 표준을 구현한 것이기 때문에 나중에 그 자리를 대신할 비슷한 도구가 또 나올 가능성이 높다고 느껴지기 때문입니다.
자바스크립트 커뮤니티는 정말 멋진 도구를 많이 만들어 두었습니다
이런 이야기를 할 때마다 “자바스크립트 진짜 싫어!!! 최악이야!!!” 같은 반응을 자주 듣습니다. 하지만 제 경험상 자바스크립트에는 훌륭한 도구가 정말 많습니다(어제 알게 된 https://esm.sh도 정말 좋아 보입니다! esbuild도 정말 좋아합니다!). 그리고 어떻게 동작하는지 배울 시간을 들이면, 그런 도구들을 활용해 제 삶을 훨씬 더 편하게 만들 수 있습니다.
그래서 이 글의 목적은 결코 자바스크립트에 대해 불평하려는 것이 아니라, 전체 지형을 이해해서 도구를 제게 잘 맞는 방식으로 활용하려는 것입니다.
아직 남아 있는 질문들
아직 남아 있는 질문들은 다음과 같습니다. 답을 알게 되면 글에 추가하겠습니다.
- 로컬에 설정해 둔 ES 모듈에 대해 importmap을 자동으로 생성해 주는 도구가 있을까요? (있다고 합니다: jspm)
- https://esm.sh이 하는 것처럼 CommonJS 모듈을 제 컴퓨터에서 ES 모듈로 변환하려면 어떻게 해야 할까요? (esbuild로 어느 정도 할 수 있다고는 하는데, named export가 동작하지 않습니다)
- 사람들이 보통 CommonJS 모듈을 일반 JS 코드로 빌드할 때, 어떤 코드가 그 일을 하고 있는 걸까요? webpack, rollup, esbuild 같은 도구가 있는 건 당연하지만, 그 도구들은 모두 자체적으로 JS 파서/정적 분석을 구현한 걸까요? 세상에 JS 파서는 대체 몇 개나 있는 걸까요?
- ES 모듈을 단일 파일(예:
atcute-client.js)로 번들하면서도, 브라우저에서 여전히 그 파일로부터 여러 다른 경로(예:@atcute/client/lexicons와@atcute/client둘 다)를 import할 수 있는 방법이 있을까요?
등장한 모든 도구
이 글에서 이야기한 모든 도구를 정리하면 다음과 같습니다.
- Simon Willison의 download-esm — ES 모듈을 다운로드하고 import를 JS 파일을 직접 가리키도록 변환해서 importmap이 필요 없게 해 줍니다.
- https://esm.sh/와 skypack.dev
- esbuild
- JSPM — importmap을 생성할 수 있습니다.
이 글을 쓰면서 든 생각은, 보통은 프로젝트를 업데이트할 때마다 실행하는 빌드를 갖고 싶지 않지만, 프로젝트를 설정할 때 딱 한 번만 실행하고 의존성 버전을 업데이트할 때를 제외하고는 다시는 실행하지 않는 빌드 단계(download-esm 같은 걸 활용한)라면 기꺼이 감수할 수 있겠다는 것입니다.
이게 전부입니다!
Marco Rogers에게 감사드립니다. 이 글에 담긴 많은 내용을 그분에게 배웠습니다. 이 글에는 아마 몇 가지 틀린 부분이 있을 텐데, 어떤 부분인지 알려 주시면 정말 감사하겠습니다. Bluesky나 Mastodon으로 알려 주세요!
글을 무작위로 읽기