ESM Import attribute은 무엇인가

dante Yoon·2023년 10월 21일
2

js/ts

목록 보기
13/14
post-thumbnail

Import attrbute

안녕하세요 단테입니다.
오늘은 TC39 Stage 3에 등재되어있는 import attribute에 대해 알아보겠습니다.

영상으로 보기

TC39 stage 3

Stage3은 기능 구현은 완성되었고 대부분의 주요한 최신 브라우저와 노드버전에서 작동이 가능한 단계를 의미합니다.
다만 stage 4가 되지 않았기 때문에 가까운 미래에 문법이 수정될 수 있습니다.

ESM, CJS, AMD

JSON

아래와 같은 json 파일이 있다고 할때 키값을 조회하기 위해서는 각 모듈 시스템의 환경에 따라 다르게 코드를 작성해야 합니다.

{
  "key1": "value1",
  "key2": "value2"
}

ESM Dynamic import & AJAX/Fetch Bundler

모던 브라우저에서는 대부분 동적 임포트를 지원하므로

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등의 번들러를 사용합니다.

CJS fs module, require

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

AMD 환경에서는 requirejs 플러그인을 사용해 json 데이터를 가져옵니다.

require(['json!data.json'], function(jsonData) {
    console.log(jsonData.key1);
});

Image, WASM

브라우저 CSS

이미지를 JS 내부에서 참조하는게 여의치 않으면 css 클래스로 미리 만들어두고 각 템플릿에서 참조해 사용하는 걸로 우회할 수 있습니다.

.icon {
  background-image: url("./assets/icon.svg")
}
<template>
  <div class="icon text-blind">icon</div>
</template> 

ESM Dynamic import & AJAX/Fetch Bundler

브라우저에서 이미를 사용하기 위해 이미지 경로를 불러와 엘리먼트를 생성해 속성값으로 주입합니다.

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();

CJS fs module, require

서버 입장에서는 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();

AMD RequireJS

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();
});

Bundler webpack, rollup, vitejs

ESM에서 asset을 불러오기 위해 각 번들러의 플러그인을 사용할경우
Vite.js에서는 별도 설정없이 json과 wasm 파일을 불러와 사용할 수 있습니다.

vite

import wasmURL, { instantiate } from './module.wasm';
import jsonData from './data.json';

instantiate(new Uint8Array(wasmURL)).then(instance => {
  console.log(instance.exports.myFunction());
});

rollup

각 정적파일을 사용하기 위해 별도 플러그인을 사용해야 합니다.

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();

webpack

각 정적파일을 불러오기 위해 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));
  });
});

Import attribute

프론트엔드에서는 대부분 ESM 문법을 사용합니다. ViteJS는 별도 플러그인을 사용하지 않으면 모던 브라우저에서만 사용한 type="module"에서만 사용가능한 번들링 파일을 만들어주고 모든 최신 기능들은 ESM 환경에서 사용하는 것을 기반으로 많은 기술들이 만들어지고 있습니다.

asset파일을 사용하기 위해 비동기적으로 asset파일을 불러올 수 있게 동적 임포팅을 사용하거나 개발환경에 번들러를 적용시켜야 코드 내부에서 json, image, wasm등의 정적파일을 편하게 불러올 수 있습니다.
번들러 사용은 javascript를 사용하는 여러 개발환경에서 asset 참조시 일관성있는 설정과 방법을 보장하지 못합니다.

일관성 있는 설정과 사용방법

A 프로젝트에서 사용했던 webpack.config.js를 그대로 복사해서 B프로젝트에 사용할 수 있으면 일관성이 보장될텐데 왜 이런 문제가 나올까요?

웹팩이 사용하는, 그리고 웹팩을 사용하는 환경의 디펜던시가 진화하며 시간이 지남에 따라 A,B 프로젝트에서 일관성있게 보장되지 않을 확률이 큽니다.

일관성을 위해 프로젝트에 스펙을 추가하지 못하는 일을 감수해야 합니다.

문법 및 사용 방법

import jsonObject from "./data.json" assert { type: "json" };

import attribute는 assert 구문을 사용해 모듈의 타입을 명확하게 정의하여 자바스크립트가 동작하는 host environment에게 모듈의 정보를 전달할 수 있는 기능입니다.
host environment의 예시로는 Chrome, Firefox, Safari, Node.js, Bun가 있습니다.

host environment에게 정보를 전달한다?

모듈의 해석을 도와줄 수 있게 메타데이터를 제공하는 것입니다.

웹 어셈블리는 바이너리 파일이기 때문에 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 파일로 이 모듈을 해석해줘라고 명확하게 올바른 파싱과 해석방법을 전달할 수있습니다.

  1. 웹 어셈블리 파일은 바이너리 형식으로 작성되어있기 때문에 자바스크립트 엔진에서 실행시킬 수 없고 WebAssembly virtual machine이라는 JS엔진과 별도의 execution context에서 동작해야 합니다.

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


profile
성장을 향한 작은 몸부림의 흔적들

0개의 댓글