자바스크립트가 아닌 리소스 번들링

자바스크립트에서 다양한 유형의 애셋을 가져와 번들로 묶는 방법을 알아보세요.

잉바르 스테파니안
잉바르 스테파니안

웹 앱을 작업한다고 가정해 보겠습니다. 이 경우 JavaScript 모듈뿐만 아니라 웹 워커 (자바스크립트이지만 일반 모듈 그래프의 일부는 아님), 이미지, 스타일시트, 글꼴, WebAssembly 모듈 등 모든 종류의 리소스도 처리해야 할 수 있습니다.

이러한 리소스 중 일부에 대한 참조를 HTML에 직접 포함할 수 있지만, 재사용 가능한 구성요소에 논리적으로 결합되는 경우가 많습니다. 자바스크립트 부분에 연결된 맞춤 드롭다운의 스타일시트, 툴바 구성요소에 연결된 아이콘 이미지 또는 자바스크립트 글루에 연결된 WebAssembly 모듈을 예로 들 수 있습니다. 이 경우 자바스크립트 모듈에서 리소스를 직접 참조하고, 해당 구성요소가 로드될 때 (또는 해당하는 경우) 리소스를 동적으로 로드하는 것이 더 편리합니다.

JS로 가져온 다양한 유형의 애셋을 시각화하는 그래프

그러나 대부분의 대규모 프로젝트에는 추가 최적화 및 콘텐츠 재구성(예: 번들링 및 축소)을 수행하는 빌드 시스템이 있습니다. 코드를 실행하고 실행 결과를 예측할 수 없으며 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 스키마를 지원합니다.

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 도구 모음에서 이미 작동하는 가장 유망한 솔루션입니다.