안녕하세요 단테입니다.
오늘은 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 콘텐츠로 처리되기 때문에 실행되지 않고 각 모듈에 적합한 형태로 내부적으로 파싱되는 것입니다.