
안녕하세요 단테입니다.
오늘은 TC39 Stage 3에 등재되어있는 import attribute에 대해 알아보겠습니다.
Stage3은 기능 구현은 완성되었고 대부분의 주요한 최신 브라우저와 노드버전에서 작동이 가능한 단계를 의미합니다.
다만 stage 4가 되지 않았기 때문에 가까운 미래에 문법이 수정될 수 있습니다.
아래와 같은 json 파일이 있다고 할때 키값을 조회하기 위해서는 각 모듈 시스템의 환경에 따라 다르게 코드를 작성해야 합니다.
{
"key1": "value1",
"key2": "value2"
}
모던 브라우저에서는 대부분 동적 임포트를 지원하므로
Chrome: Supported since version 63.
Firefox: Supported since version 67.
Safari: Supported since version 11.1.
Edge: Supported since version 79 (the Chromium-based version).
Opera: Supported since version 50.
import 구문을 사용해 동적으로 json 파일을 받아오거나 AJAX를 이용해 서버 데이터를 받아옵니다.
import('./data.json').then(jsonData => {
console.log(jsonData.key1) // value
})
fetch('./data.json')
.then(response => response.json())
.then(jsonData => {
console.log(jsonData.key1);
});
다만 별도 번들러를 활용하지 않고 바닐라 자바스크립트로만 사용할 경우 스크립트 파일을 불러오는
index.html 에서 모듈 타입을 설정해줘야 하므로 공공기관에서 사용하는 펜티엄(?) 컴퓨터에서는 동작하지 않을 수 있습니다.
<script type="module" src="./index.js"></script>
아니면 webpack, rollup, vitejs등의 번들러를 사용합니다.
Node.js의 fs 모듈을 사용해 파일을 읽을 수 있거나 require 구문을 사용합니다.
const fs = require('fs');
const jsonData = require('./data.json');
console.log(jsonData.key1); // Outputs: value1
console.log(jsonData.key2); // Outputs: value2
fs.readFile('./data.json', 'utf8', (err, jsonString) => {
if (err) {
console.error('Error reading the file', err);
return;
}
const jsonData = JSON.parse(jsonString);
console.log(jsonData.key1);
});
AMD 환경에서는 requirejs 플러그인을 사용해 json 데이터를 가져옵니다.
require(['json!data.json'], function(jsonData) {
console.log(jsonData.key1);
});
이미지를 JS 내부에서 참조하는게 여의치 않으면 css 클래스로 미리 만들어두고 각 템플릿에서 참조해 사용하는 걸로 우회할 수 있습니다.
.icon {
background-image: url("./assets/icon.svg")
}
<template>
<div class="icon text-blind">icon</div>
</template>
브라우저에서 이미를 사용하기 위해 이미지 경로를 불러와 엘리먼트를 생성해 속성값으로 주입합니다.
const imgElement = document.createElement("img");
imgElement.src = "./path/to/image.jpg";
document.body.appendChild(imgElement);
fetch를 사용해 불러온 wasm파일을 WebAssembly 인터페이스로 실행시킵니다.
async function loadWasm() {
const response = await fetch('./module.wasm');
const bytes = await response.arrayBuffer();
const results = await WebAssembly.instantiate(bytes);
// Use the WebAssembly module
const exports = results.instance.exports;
}
loadWasm();
서버 입장에서는 Node.js 환경에서 image를 정적 파일로 제공합니다.
const path = require('path');
const express = require('express');
const app = express();
app.use('/static', express.static(path.join(__dirname, 'public')));
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
// fetch http://localhost:3000/static/image.jpg
Node.js에서 WASM 모듈을 사용하는 방법입니다.
const fs = require('fs').promises;
const path = require('path');
async function loadWasm() {
const bytes = await fs.readFile(path.join(__dirname, 'module.wasm'));
const { instance } = await WebAssembly.instantiate(bytes);
// Use the WebAssembly module
instance.exports.someFunction();
}
loadWasm();
ESM과 동일하게 AMD 환경에서도 엘리먼트 속성으로 주입해주거나 WebAssemby api를 사용합니다.
define([], function() {
const imgElement = document.createElement('img');
imgElement.src = './path/to/image.jpg';
document.body.appendChild(imgElement);
});
define([], function() {
async function loadWasm() {
const response = await fetch('./module.wasm');
const bytes = await response.arrayBuffer();
const results = await WebAssembly.instantiate(bytes);
// Use the WebAssembly module
const exports = results.instance.exports;
}
loadWasm();
});
ESM에서 asset을 불러오기 위해 각 번들러의 플러그인을 사용할경우
Vite.js에서는 별도 설정없이 json과 wasm 파일을 불러와 사용할 수 있습니다.
import wasmURL, { instantiate } from './module.wasm';
import jsonData from './data.json';
instantiate(new Uint8Array(wasmURL)).then(instance => {
console.log(instance.exports.myFunction());
});
각 정적파일을 사용하기 위해 별도 플러그인을 사용해야 합니다.
import { wasm } from '@rollup/plugin-wasm';
export default {
input: 'src/index.js',
output: {
dir: 'output',
format: 'cjs'
},
plugins: [wasm()]
};
import init, { main } from '../build/sample.js';
import sample from '../build/sample_bg.wasm';
sample()
.then({ instance } => init(instance))
.then(() => main());
// or using top-level await
await init(await sample());
main();
각 정적파일을 불러오기 위해 loader를 설정해야 합니다.
image를 위해서는 file-loader 나 url-loader, wasm은 @wasm-tool/wasm-pack-plugin, json은 별도설정 없이 지원됩니다.
// webpack.config.js
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
module.exports = {
entry: './src/index.js',
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.js'
},
plugins: [
new HtmlWebpackPlugin(),
new WasmPackPlugin({
crateDirectory: path.resolve(__dirname, "./rust-crate")
})
],
mode: 'development',
rules: [
{
test: /\.(png|jpe?g|gif|svg)$/i,
use: [
{
loader: 'file-loader',
},
],
},
],
};
import init, * as wasm from "../bundle/lib";
init();
addEventListener("DOMContentLoaded", () => {
const $submit = document.getElementById("submit");
const $name = document.getElementById("name");
$submit.addEventListener("click", () => {
alert(wasm.openPopup($name.value));
});
});
프론트엔드에서는 대부분 ESM 문법을 사용합니다. ViteJS는 별도 플러그인을 사용하지 않으면 모던 브라우저에서만 사용한 type="module"에서만 사용가능한 번들링 파일을 만들어주고 모든 최신 기능들은 ESM 환경에서 사용하는 것을 기반으로 많은 기술들이 만들어지고 있습니다.
asset파일을 사용하기 위해 비동기적으로 asset파일을 불러올 수 있게 동적 임포팅을 사용하거나 개발환경에 번들러를 적용시켜야 코드 내부에서 json, image, wasm등의 정적파일을 편하게 불러올 수 있습니다.
번들러 사용은 javascript를 사용하는 여러 개발환경에서 asset 참조시 일관성있는 설정과 방법을 보장하지 못합니다.
A 프로젝트에서 사용했던 webpack.config.js를 그대로 복사해서 B프로젝트에 사용할 수 있으면 일관성이 보장될텐데 왜 이런 문제가 나올까요?
import jsonObject from "./data.json" assert { type: "json" };
import attribute는 assert 구문을 사용해 모듈의 타입을 명확하게 정의하여 자바스크립트가 동작하는 host environment에게 모듈의 정보를 전달할 수 있는 기능입니다.
host environment의 예시로는 Chrome, Firefox, Safari, Node.js, Bun가 있습니다.
모듈의 해석을 도와줄 수 있게 메타데이터를 제공하는 것입니다.
웹 어셈블리는 바이너리 파일이기 때문에 host environment가 자바스크립트 모듈을 다루는 것과는 다르게 동작해야 합니다.
import myHTML from "./component.html" assert { type: "html" };
import myStyles from "./styles.css" assert { type: "css" };
type 속성을 명시해 브라우저가 임포트된 모듈을 어떻게 다룰지 알 수 있는 component.html은 text/html로, styles.css는 text/css로 다룰 수 있습니다.
import data from './data.json' assert { type: "json" };
위에서 각 개발환경별로 json파일을 다르게 불러오기 위해 추가설정을 한 것을 봤듯이 import data from "./data.json"와 같이 임포트할 경우 json 파일은 javascript가 아니기 때문에 브라우저에서는 사용할 수 없지만
assert { type: "json" }; 를 붙임으로 브라우저야, JSON 파일로 이 모듈을 해석해줘라고 명확하게 올바른 파싱과 해석방법을 전달할 수있습니다.
3
import template from './template.html' assert { type: "html" };
html 모듈을 임포트할 때 host environment가 HTML module이라는 정보를 알기 떄문에 자동으로 sanitization을 해서 악의적인 스크립트 실행을 방지할 수 있게 도와줍니다.
as is
<div> Welcome to my website! <script>alert('This is a potential malicious script!');</script> </div>
to be
<div> Welcome to my website! </div>
정교한 sanitization은 단순히 `<script>` 태그를 없애는게 아닙니다. 기능에 유의미한 핸들러는 놔두고 쿠키를 강탈당할 수 있는 스크립트나 img 태그 내부에 있는 악의적인 src 속성 값을 없애줘야 하기 때문에 보다 어려운 문제입니다.
## Module assertion. type: js
js 속성으로 임포트된 js 모듈은 임포트되면서 코드가 실행됩니다. `import x from "./modules.js"` 로만 작성하더라도 자바스크립트로 host environment가 해석할 것이기 때문에 별도로 작성해야 할 필요는 없습니다.
다만 명시적으로 임포트를 할 경우 `type: js`가 아닌 모듈들은 Non-Javascript 콘텐츠로 처리되기 때문에 실행되지 않고 각 모듈에 적합한 형태로 내부적으로 파싱되는 것입니다.