JS module 2- Static module structure

Sal Jeong·2022년 11월 9일
0

yarn v2 사용해보기

목록 보기
2/2

https://exploringjs.com/es6/ch_modules.html#static-module-structure

ESM ES6의 모듈 체계 정리를 위해 위 내용을 정리함.

ES6의 ESM 체계 원리

  1. Default exports를 권장함
  2. Static module 구조를 가짐
  3. Synchronous와 Asynchronous로딩 둘 다를 지원함
  4. Cyclic dependencies를 지원함.

1. Default exports

ESM은 두 가지 exports/imports 모듈을 제공한다.

  1. named exports
// imports
// ex. importing a single named export
import { MyComponent } from "./MyComponent";
// ex. importing multiple named exports
import { MyComponent, MyComponent2 } from "./MyComponent";
// ex. giving a named import a different name by using "as":
import { MyComponent2 as MyNewComponent } from "./MyComponent";

// import * as MyComponents from "./MyComponent";

// exports from ./MyComponent.js file
export const MyComponent = () => {}
export const MyComponent2 = () => {}
  1. default exports
import * as MainComponents from "./MyComponent";
// use MainComponents.MyComponent and MainComponents.MyComponent2
here

이 중 default exports 기법을 권장하는데, 그 이유는

ECMAScript 개발자의 발언에서 찾을 수 있다.

https://esdiscuss.org/topic/moduleimport#content-0

ECMAScript 6 favors the single/default export style, and gives the sweetest syntax to importing the default. Importing named exports can and even should be slightly less concise.

named exports역시 가능하지만, 조금이라도 헷갈릴 가능성이 있기 때문에 라고 한다.

2. Static Module Structure

es6는 기존 commonJS의 require.js와 다르게, static하게 module을 불러온다.

기존(es5까지의) 자바스크립트는 dynamic import 구조를 가지고 있었다.
런타임에서 export/import를 결정하였고, 이것을 바꾼 이유는, 몇가지가 있다고 한다.

먼저 static 구조라는 것을 먼저 설명한다면 export/import가 컴파일 시 일어난다는 것으로 ES6의 문법적으로 지원되는 것이다. 이를 위해서 export/import는 top level에서 지정해야 하며(require 처럼 if statements에 넣으면 동작하지 않는다.)

// 기존 es5까지의 dynamuic한 commonJS 모듈 import 예시.
// runtime 에서 dynamic 하게 module를 가져온다.

var my_lib;
if (Math.random()) {
    my_lib = require('foo');
} else {
    my_lib = require('bar');
}

if (Math.random()) {
    exports.baz = ···;
}

es6에서는 이러한 방식이 아니라 더 정적이지만 그에 맞는 이점을 얻을 수가 있다.

3. Static Module의 장점 1

프론트엔드 개발에서 modules은 이런 특징을 가지게 된다.

  1. 개발 시 코드는 작은 많은 파일(modules)로 구성됨
  2. deploy 시 작성된 코드들이 몇개의 큰 코드 파일(bundle)로 만들어짐

이러한 이유는 바로

  1. 모듈 파일 하나하나씩을 다운받는 것보다 큰 몇 개의 파일을 받는 것이 효율적이기 떄문.
  2. 번들된 파일 몇 개를 압축하는 것이 여러 작은 파일을 압축하는 것보다 효율적임.
  3. 번들링하는 동안 사용되지 않는 export를 제거해 용량을 더욱 줄일 수 있다.

1.은 http/1 프로토콜에서 특히 유용하다. 현재는 http/2 프로토콜로써 그렇게 중요하지 않을 수 있다.

하지만 여전히 3.의 이유가 가장 중요하다고 한다. 이것은 이 Static Structure로만 얻을 수 있는 이점이다.

3-2. 장점 2 번들링된 파일 줄이기

https://github.com/rollup/rollup

모듈 번들러 중 하나인 rollup 가장 먼저 es6 모듈을 효율적으로 합치는 방법을 고안해 내었음.

rollup에서는 모든 모듈을 single scope로 만들고 변수명을 renaming하게 됨. 여기서 두 가지 es6 모듈 시스템의 특징을 사용함.

  1. static한 structure이므로 로딩 시 조건문을 사용하지 않는다.
  2. 읽기 전용의 import 구문 시 export되는 module이 copy되지 않음.
// 예시로 dev 환경에서 이러한 코드가 있다면,
// lib.js
export function foo() {}
export function bar() {}

// main.js
import {foo} from './lib.js';
console.log(foo());
// rollup에서는 이것을 하나의 모듈로 만들고(여기서 사용되지 않는 bar의 export는 삭제됨)
function foo() {}

console.log(foo());

또한 이것은 rollup에서 custom하게 제작한 모듈 시스템이 아니며, es6의 기본 사양을 사용하고 있는 것이 중요하다.

3-2. 장점 3 빠른 imports 조회

CommonJS에서 require 구문을 사용하게 되면,

var lib = require('lib');
lib.someFunc(); // property lookup

이러한 식으로 object 식으로 조회하게 된다.
이러한 방법은 import -> object property 조회로 이어지므로 역시 속도 면에서 느리다.

es6에서는 이것을 직접 import 하기 때문에 조회 속도가 더 빠르다.

import * as lib from 'lib';
lib.someFunc(); // statically resolved

3-3. 변수 체크

static한 코드이므로, module 안에서의 변수를 쉽게 알 수 있음.

  1. 글로벌 변수: 현재 시점에서 글로벌 변수는 언어 자체에서 제공하는 것 밖에 없다고 할 수 있음. 다른 것들은 모두 모듈 내부의 변수임.(library와 browser 변수까지 모두 포함해서) 따라서 글로벌 변수에 대해서는 크게 신경 쓰지 않아도 되며, static하게 위치를 모두 알 수 있다.
  2. Module Imports : import를 직접 해오니까 역시 위치를 알 수 있음.
  3. Module Local 변수들: 이것은 module 코드를 보면서 확인할 수 있음.

이 부분은 개발 시스템적으로도 아주 중요한데, jsLint 나 jsHint와 같이 개발 툴에서 빠르게 변수 관련 에러를 잡아낼 수 있기 때문임.

3-4. Macro의 활용

https://www.sweetjs.org/

Macro는 아직 JS의 로드맵에 포함된 기능이다. Sweet.js는 experimental한 macro를 지원하는데, 이런식으로 사용한다.

// 너무 옛날 예시라 실제로 사용하는지는 모르겠음.

// Define the macro
macro class {
    rule {
        $className {
                constructor $cparams $cbody
                $($mname $mparams $mbody) ...
        }
    } => {
        function $className $cparams $cbody
        $($className.prototype.$mname
            = function $mname $mparams $mbody; ) ...
    }
}

// Use the macro
class Person {
    constructor(name) {
        this.name = name;
    }
    say(msg) {
        console.log(this.name + " says: " + msg);
    }
}
var bob = new Person("Bob");
bob.say("Macros are sweet!");

매크로를 사용하는데 있어 자바스크립트 새 엔진이 이 부분을 전처리하여 compile 하여, static structure로 만들어 준다.

3-5. 타입의 활용

Static type checking이 가능하도록 한다. 이것 역시 macro와 비슷한 원리로 실행된다.
(typescript에서 type 지정 시 vs code에서 참조되는 모든 부분의 코드를 실행된 것처럼 체킹하여 알려주는 것처럼,)

static structure에 포함된 module에서 사용된다.

http://lljs.org/

이러한 static하게 type이 명시된 코드를 사용함으로써 퍼포먼스를 크게 향상시킬수 있다.
그 중 하나가 위의 Low-Level JavaScript(LLJS) 이다.

3-6. 타 언어지원

다른 언어의 경우에도 컴파일링을 통해 위 3-5, 3-6의 모듈을 자바스크립트에서 일읽을 수 있게 해준다.

http://calculist.org/blog/2012/06/29/static-module-resolution/

4. Static structure 추가 사항

4-1. 동기, 비동기 로딩 지원 관련

ESM은 독립적인 모듈 시스템을 원칙으로 한다.
서버에서는 동기적으로, 클라이언트에서는 비동기적으로 로드되지만 위 원칙은 그대로 사용된다.

예를 들어서 static하게 모든 import를 선언하면 이것을 module의 코드가 실행되기 전에 import한 것으로 취급할 수 있는 것이다.(이것은 AMD와 유사한 부분이 있다.)

4-2. 순환성 의존에 관해.

순환 의존성을 지원하는 것이 ESM 시스템의 중요 목표이다. 그 이유는

순환성 의존이 나쁜것만은 아니다. 특히 오브젝트에서, 이러한 관계를 사용하는 경우가 있다.
예시를 들면

DOM Tree 구조에서 부모가 자식을, 자식이 부모를 referencing 하는 것이 그것이다.
하지만 일반적으로 코드를 작성할때 이러한 의존 구조를 피하려는 것이 좋다. 그러나
시스템의 크기가 커질 수록 이러한 상황이 일어날 수 있게 된다. 특히나 리팩토링 작업을 진행하는 도중, 코드를 수정하며 이런 상황이 일어날 수 있으므로, 이 과정 중 생긴 순환성 의존을 작업이 끝날 때까지 에러를 뱉지 않게 하는 것이 목표이다.

5. FAQ

  1. 모듈 import 시 변수에 담을 수 있나요?

import는 static함.

이 경우 loader API로 특정할 수 있음.

const moduleSpecifier = 'module_' + Math.random();
System.import(moduleSpecifier)
.then(the_module => {
    // Use the_module
})
  1. import 시 조건문을 사용할 수 있나요?

import는 모듈의 맨 상위에 위치해여ㅑ 함. 만약 조건문을 사용하거나 dynamic하게 사용하기 위해선

1.의 loader API를 사용해야 함.

if (Math.random()) {
    System.import('some_module')
    .then(some_module => {
        // Use some_module
    })
}
  1. import시 변수로 모듈을 불러올 수 있나요?

안됨.

  1. import 시 destructuring을 사용할 수 있나요?

불가능함.

// 불가능
import { foo: { bar } } from 'some_module';
  1. named exports가 꼭 필요한가요??
import {moduleA} from ./modules;

default-export와 named exports를 혼용하는 이유는 object export 시 얻을 수 있는 이점 때문임.

3-2. 를 참고.

  1. 모듈에서 eval()을 사용할 수 있나요?

모듈은 eval()보다 상위 레벨의 construct()를 사용하므로 불가능함.

이렇게 새로운 esm에 대한 모듈 시스템을 ebook 내용을 보며

다시 정리했고,

다음으로 yarn v2가 제공하는 기능, 모듈 관련 내용을 정리하면 되겠다.

profile
행복하기 위해 코딩합니다. JS 합니다.

0개의 댓글