자바스크립트에서 다양한 유형의 애셋을 가져와 번들로 묶는 방법을 알아보세요.
웹 앱을 작업한다고 가정해 보겠습니다. 이 경우 JavaScript 모듈뿐만 아니라 웹 워커 (자바스크립트이지만 일반 모듈 그래프의 일부는 아님), 이미지, 스타일시트, 글꼴, WebAssembly 모듈 등 모든 종류의 리소스도 처리해야 할 수 있습니다.
이러한 리소스 중 일부에 대한 참조를 HTML에 직접 포함할 수 있지만, 재사용 가능한 구성요소에 논리적으로 결합되는 경우가 많습니다. 자바스크립트 부분에 연결된 맞춤 드롭다운의 스타일시트, 툴바 구성요소에 연결된 아이콘 이미지 또는 자바스크립트 글루에 연결된 WebAssembly 모듈을 예로 들 수 있습니다. 이 경우 자바스크립트 모듈에서 리소스를 직접 참조하고, 해당 구성요소가 로드될 때 (또는 해당하는 경우) 리소스를 동적으로 로드하는 것이 더 편리합니다.
그러나 대부분의 대규모 프로젝트에는 추가 최적화 및 콘텐츠 재구성(예: 번들링 및 축소)을 수행하는 빌드 시스템이 있습니다. 코드를 실행하고 실행 결과를 예측할 수 없으며 JavaScript에서 가능한 모든 문자열 리터럴을 탐색하여 리소스 URL인지 추측할 수도 없습니다. 그렇다면 어떻게 해야 자바스크립트 구성요소에서 로드한 동적 애셋을 '보이게' 하여 빌드에 포함할 수 있을까요?
번들러의 맞춤 가져오기
한 가지 일반적인 방법은 정적 가져오기 구문을 재사용하는 것입니다. 일부 번들러에서는 파일 확장자에 따라 형식을 자동 감지할 수도 있고, 다른 번들러에서는 플러그인이 다음 예와 같이 커스텀 URL 스키마를 사용하도록 허용합니다.
// regular JavaScript import
import { loadImg } from './utils.js';
// special "URL imports" for assets
import imageUrl from 'asset-url:./image.png';
import wasmUrl from 'asset-url:./module.wasm';
import workerUrl from 'js-url:./worker.js';
loadImg(imageUrl);
WebAssembly.instantiateStreaming(fetch(wasmUrl));
new Worker(workerUrl);
번들러 플러그인은 인식하는 확장 프로그램이나 이러한 명시적인 커스텀 스키마 (위 예에서는 asset-url:
및 js-url:
)가 포함된 가져오기를 찾으면 참조된 애셋을 빌드 그래프에 추가하고 최종 대상에 복사하고 애셋 유형에 해당하는 최적화를 실행하고 런타임 중에 사용할 최종 URL을 반환합니다.
이 접근 방식의 장점은 JavaScript 가져오기 구문을 재사용하면 모든 URL이 정적이고 현재 파일에 상대적임을 보장하므로 빌드 시스템에서 이러한 종속 항목을 쉽게 찾을 수 있습니다.
그러나 한 가지 중요한 단점이 있습니다. 브라우저가 맞춤 가져오기 스키마 또는 확장 프로그램을 처리하는 방법을 모르기 때문에 이러한 코드는 브라우저에서 직접 작동할 수 없습니다. 어차피 모든 코드를 제어하고 번들러를 사용해 개발한다면 괜찮을 수도 있지만, 마찰을 줄이기 위해 적어도 개발 중에는 브라우저에서 직접 JavaScript 모듈을 사용하는 것이 점점 더 보편화되고 있습니다. 소규모 데모 작업을 하는 사람에게는 프로덕션 단계에서도 번들러가 전혀 필요하지 않을 수 있습니다.
브라우저 및 번들러용 범용 패턴
재사용 가능한 구성요소를 작업 중인 경우 브라우저에서 직접 사용하든 더 큰 앱의 일부로 사전 빌드하든 상관없이 어느 환경에서나 작동하도록 할 수 있습니다. 대부분의 최신 번들러는 JavaScript 모듈에서 다음 패턴을 수락하여 이를 허용합니다.
new URL('./relative-path', import.meta.url)
이 패턴은 거의 특수 구문인 것처럼 도구에서 정적으로 감지할 수 있지만, 브라우저에서 직접 작동하는 유효한 JavaScript 표현식이기도 합니다.
이 패턴을 사용하면 위의 예를 다음과 같이 다시 작성할 수 있습니다.
// regular JavaScript import
import { loadImg } from './utils.js';
loadImg(new URL('./image.png', import.meta.url));
WebAssembly.instantiateStreaming(
fetch(new URL('./module.wasm', import.meta.url)),
{ /* … */ }
);
new Worker(new URL('./worker.js', import.meta.url));
기본 원리 나누어 봅시다. new URL(...)
생성자는 상대 URL을 첫 번째 인수로 사용하고 두 번째 인수로 제공된 절대 URL을 기준으로 이를 확인합니다. 여기서 두 번째 인수는 현재 자바스크립트 모듈의 URL을 제공하는 import.meta.url
이므로 첫 번째 인수는 이에 상대적인 경로가 될 수 있습니다.
동적 가져오기와 비슷한 장단점이 있습니다. import(someUrl)
와 같은 임의의 표현식과 함께 import(...)
를 사용할 수도 있지만 번들러는 컴파일 시간에 알려진 종속 항목을 사전 처리하면서도 동적으로 로드되는 자체 청크로 분할하는 방법으로 정적 URL import('./some-static-url.js')
가 있는 패턴을 특별하게 처리합니다.
마찬가지로 new URL(relativeUrl, customAbsoluteBase)
와 같은 임의의 표현식과 함께 new URL(...)
를 사용할 수 있지만 new URL('...', import.meta.url)
패턴은 번들러가 기본 JavaScript와 함께 종속 항목을 사전 처리하고 포함해야 한다는 명확한 신호입니다.
모호한 상대 URL
번들러가 다른 일반적인 패턴(예: new URL
래퍼가 없는 fetch('./module.wasm')
)을 감지할 수 없는 이유는 무엇일까요?
그 이유는 import 문과 달리 모든 동적 요청은 현재 JavaScript 파일이 아니라 문서 자체를 기준으로 확인되기 때문입니다. 구조가 다음과 같다고 가정해 보겠습니다.
index.html
:
html <script src="src/main.js" type="module"></script>
src/
main.js
module.wasm
main.js
에서 module.wasm
를 로드하려면 fetch('./module.wasm')
와 같은 상대 경로를 사용하고 싶을 수 있습니다.
그러나 fetch
는 실행되는 JavaScript 파일의 URL을 알지 못하며 대신 문서와 관련된 URL을 확인합니다. 그 결과 fetch('./module.wasm')
에서 의도한 http://example.com/src/module.wasm
대신 http://example.com/module.wasm
를 로드하려고 시도하여 실패합니다 (또는 의도했던 것과 다른 리소스를 자동으로 로드함).
상대 URL을 new URL('...', import.meta.url)
로 래핑하면 이 문제를 피할 수 있으며 제공된 URL이 로더에 전달되기 전에 현재 JavaScript 모듈의 URL (import.meta.url
)을 기준으로 확인되도록 할 수 있습니다.
fetch('./module.wasm')
를 fetch(new URL('./module.wasm', import.meta.url))
로 대체하면 예상 WebAssembly 모듈이 성공적으로 로드되고 번들러에 빌드 시간 동안 이러한 상대 경로를 찾을 수 있는 방법이 제공됩니다.
도구 지원
번들러
다음 번들러는 이미 new URL
스키마를 지원합니다.
- Webpack v5
- Rollup (플러그인을 통해 획득: 일반 애셋의 경우 @web/rollup-plugin-import-meta-assets, 작업자 전용의 경우 @surma/rollup-plugin-off-main-thread)
- Parcel v2 (베타)
- Vite
WebAssembly
WebAssembly로 작업할 때는 일반적으로 Wasm 모듈을 수동으로 로드하지 않고 대신 도구 모음에서 내보낸 JavaScript 글루를 가져옵니다. 다음 도구 모음은 설명된 new URL(...)
패턴을 자동으로 내보낼 수 있습니다.
Emscripten을 통한 C/C++
Emscripten을 사용할 때는 다음 옵션 중 하나를 통해 일반 스크립트 대신 JavaScript 글루를 ES6 모듈로 내보내도록 요청할 수 있습니다.
$ emcc input.cpp -o output.mjs
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6
이 옵션을 사용하면 번들러가 연결된 Wasm 파일을 자동으로 찾을 수 있도록 내부적으로 new URL(..., import.meta.url)
패턴이 출력됩니다.
-pthread
플래그를 추가하여 WebAssembly 스레드에 이 옵션을 사용할 수도 있습니다.
$ emcc input.cpp -o output.mjs -pthread
## or, if you don't want to use .mjs extension
$ emcc input.cpp -o output.js -s EXPORT_ES6 -pthread
이 경우 생성된 Web Worker는 동일한 방식으로 포함되며 번들러와 브라우저에서도 검색할 수 있습니다.
wasm-pack / wasm-bindgen을 통한 Rust
Wasm-pack(WebAssembly용 기본 Rust 도구 모음)에도 여러 출력 모드가 있습니다.
기본적으로 WebAssembly ESM 통합 제안을 사용하는 JavaScript 모듈을 내보냅니다. 이 제안서는 현재 실험 단계에 있으며 Webpack과 번들로 제공되어야만 출력됩니다.
대신 --target web
를 통해 브라우저 호환 ES6 모듈을 내보내도록 wasm-pack에 요청할 수 있습니다.
$ wasm-pack build --target web
출력에 설명된 new URL(..., import.meta.url)
패턴이 사용되며 Wasm 파일도 번들러에 의해 자동으로 검색됩니다.
Rust와 함께 WebAssembly 스레드를 사용하려는 경우 조금 더 복잡합니다. 자세한 내용은 가이드의 해당 섹션을 참조하세요.
짧은 버전은 임의 스레드 API를 사용할 수 없다는 것입니다. 하지만 Rayon을 사용하는 경우 Wasm-bindgen-rayon 어댑터와 결합하여 웹에서 작업자를 생성할 수 있습니다. wasm-bindgen-rayon에서 사용하는 JavaScript 접착제는 내부적으로 new URL(...)
패턴도 포함하므로 번들러에서도 작업자를 검색하고 포함할 수 있습니다.
출시 예정 기능
import.meta.resolve
전용 import.meta.resolve(...)
호출은 향후 개선이 될 수 있습니다. 추가 매개변수 없이 보다 간단한 방식으로 현재 모듈과 비교하여 지정자를 확인할 수 있습니다.
new URL('...', import.meta.url)
await import.meta.resolve('...')
또한 import
와 동일한 모듈 해상도 시스템을 거치므로 가져오기 맵 및 맞춤 리졸버와 더 잘 통합됩니다. 번들러는 URL
와 같은 런타임 API에 종속되지 않는 정적 구문이므로 번들러에 대해 더 강력한 신호가 될 수 있습니다.
import.meta.resolve
는 이미 Node.js에서 실험으로 구현되어 있지만 웹에서 어떻게 작동하는지에 대한 해결되지 않은 질문이 아직 남아 있습니다.
어설션 가져오기
가져오기 어설션은 ECMAScript 모듈 이외의 유형을 가져올 수 있도록 하는 새로운 기능입니다. 현재 JSON으로 제한됩니다.
foo.json:
{ "answer": 42 }
main.mjs:
import json from './foo.json' assert { type: 'json' };
console.log(json.answer); // 42
또한 번들러에서 사용하고 현재 new URL
패턴으로 다루는 사용 사례를 대체할 수도 있지만 가져오기 어설션의 유형은 사례별로 추가됩니다. 지금은 JSON만 다루고 CSS 모듈은 곧 지원될 예정입니다. 하지만 다른 유형의 애셋에는 여전히 더 일반적인 솔루션이 필요합니다.
이 기능에 대해 자세히 알아보려면 v8.dev 기능 설명을 참조하세요.
결론
아시다시피, 비 자바스크립트 리소스를 웹에 포함하는 방법에는 여러 가지가 있지만 여러 가지 단점이 있으며 다양한 도구 모음에서 작동하지 않습니다. 향후 제안서에서는 특수 문법으로 이러한 애셋을 가져올 수 있지만, 아직은 그렇게 하지 못했습니다.
그때까지 new URL(..., import.meta.url)
패턴은 오늘날 브라우저, 다양한 번들러, WebAssembly 도구 모음에서 이미 작동하는 가장 유망한 솔루션입니다.