JS

ES-Module(ESM, ECMAScript Modules)

Hyeon_E 2024. 6. 11. 16:37

ES-Module(ESM)

ES Module은 ES6부터 도입된 모듈 시스템

export import문을 사용하여 분리되어 있는 자바스크립트 파일 간의 접근을 가능하게 만들어줌

 

▶ ES Module 등장 배경

  1. 기존의 웹은 자바스크립트의 비중이 크지 않았고, 따라서 큰 스크립트가 필요하지 않았음
  2. 웹의 발전에 따라 점점 자바스크립트의 중요도가 커지고, 여러 스크립트 파일을 쓰며 상호작용해야 했음
  3. 이를 처리하기 위해 JQuery 등으로 해결(각각의 script 파일을 전역 스코프처럼 사용)했지만 여러 문제점이 발생했음
    • script 파일들을 올바른 순서대로 정렬해야 하기 때문에 순서가 뒤틀리면 에러를 발생
    • 하위에 있는 script가 상위 script의 상태를 쉽게 변경시키는 '전역 오염'이 발생하기 쉬움
    • 모든 script 파일에서 전역 스코프에 있는 변수들에 접근할 수 있기 때문에 하나의 script가 어떤 script를 의존하고 있는지 파악하기 어려움
    • 궁극적으로 이와 같은 문제점들로 인해 유지보수가 어려움
  4. 이에 따라 각 파일 간의 상호작용을 위해 모듈화함

 

해결책 모듈화

모듈은 함수와 변수를 모듈 스코프에 넣고, 각 함수는 함수 스코프를 가짐

이 때, export문을 사용하면 해당 변수와 함수다른 모듈에서 import문을 사용하여 의존할 수 있도록 해줌

 

모듈화의 장점

  • export-import의 명시적인 의존성 관계로, 하나의 모듈이 제거되면 어떤 모듈이 손상되었는지를 파악하기 쉬움
  • 코드들을 각각 독립적으로 동작할 수 있는 단위로 나누기 용이
    이는 모듈을 재사용함으로써 다양한 종류의 어플리케이션을 만들 수 있도록 도와줌
  • export-import로 관계되어 있지 않은 모듈서로 오염을 일으키지 않음

한줄로 정리하면 코드를 의미 있는 단위로 분리해서 사용할 수 있기 때문에 유지 보수성도 향상되고, 코드를 재사용하기에도 좋음

 

이미 node.js에는 RequireJS와 같은 CommonJS를 제공하고 있었으며, 그 외에도 AMD 기반 모듈 시스템, Webpack, Babel 같은 모듈 기반 시스템과 같이 모듈 사용을 가능하게 만들어주는 자바스크립트 라이브러리와 프레임워크가 존재했음


 기존에는 위와 같은 라이브러리에 의존해야 했던 모듈 기능을 ECMAScript 6부터 네이티브 자바스크립트에서도 지원하기 시작했으며 여러 브라우저에서도 모듈 로딩을 최적화할 수 있도록 모듈 기능을 지원하고 있음

 

현재 브라우저별 호환성

 

▶ ES Module의 동작 방식

의존성 간의 연결import 문이 작성된 코드에서 발생

import 문은 브라우저 또는 Node가 어떤 코드를 불러와야 하는지 인식하는 데 사용

import 문에서 지정한 파일(일반적으로 url)의존성 그래프진입점(entry point)이 되고

연결되어 있는 import 문을 따라가면서 의존성 그래프가 그려짐

 

ES Module이 동작하기 위해서는 브라우저가 사용할 수 있도록 모듈 레코드(Module Record) 라고 하는 데이터 구조로 변환 작업 필요한데, 이러한 모듈화 작업 과정은 구성 → 인스턴스화 → 평가의 세 단계를 거침

 

ES 모듈의 진행 순서 3단계

  • 구성: 모든 파일을 찾아 다운로드하고 모듈 레코드로 구문분석
  • 인스턴스화: export 된 값을 모두 배치하기 위해 메모리에 있는 공간들을 찾음(아직 실제 값은 채우지 않음)
    그 다음 export와 import들이 이런 메모리 공간들을 가리키도록 함(이를 연결(linking(이라고 함)
  • 평가: 코드를 실행하여 상자의 값을 변수의 실제 값으로 채움

 

1. 구성(Construction)

 

  1. 구성 단계에서는 모듈이 들어 있는 파일을 어디서 다운로드 할 것인지 확인
  2. URL을 통하거나 파일 시스템을 이용해 파일을 가져옴
  3. 파일을 모듈 레코드로 구문분석

이 때 파일을 불러오는 역할을 하는 것이 로더(loader)인데

사용 중인 플랫폼에 따라 다른 로더를 가질 수도 있지만 브라우저의 경우 HTML 명세를 따름

로더는 스크립트 태그에서 진입점 파일을 찾을 수 있는 단서를 얻고

import문의 모듈 지정자(module specifier)를 통해 다음 모듈의 의존성을 파악

또한 모듈 맵을 이용하여 각 모듈의 캐시를 관리하기도 함

 

 

2. 인스턴스화(Instantiation)

 

자바스크립트 엔진은 먼저 모듈 환경 레코드를 생성한 후 이를 통해 모듈 레코드의 변수를 관리

생성된 모듈 환경 레코드는 각 export와 연관되어 있는 메모리 공간을 추적하는데

이 때 자바스크립트 엔진은 다른 것에 의존하지 않는 그래프의 최하단까지 조사한 후

export를 설정하고 모든 export를 연결

 

- 라이브 바인딩이란?
Node.js의 모듈 시스템인 CommonJS(CJS)는 위의 인스턴스 과정에서 export 객체가 메모리에 올라갈 때, 복제된 값이 올라감

따라서 인스턴스 과정이 끝나고, export 하는 모듈에서 값 수정이 일어나도 import 하는 모듈에선 알 수 있는 방법이 없음

이에 반해 ES 모듈은 라이브 바인딩을 사용

위 이미지처럼 export 하는 모듈과 import 하는 모듈이 같은 메모리 주소에 접근하기 때문에 모듈에서 발생하는 변경사항을 알수 있음 하지만 import하는 모듈에서는 가져온 값을 변경할 수는 없음(단, 모듈이 객체를 가져오는 경우에는 해당 객체에 있는 프로퍼티의 값을 변경 가능)

 

3. 평가 (Evaluation)

 

평가 단계에서는 코드를 실행하여 메모리 공간에 실제 값을 채움

자바스크립트 엔진은 함수 외부 코드인 최상위 레벨 코드를 실행하여 이를 수행

평가는 수행한 횟수에 따라 다른 결과를 가질 수 있기 때문에 한 번만 평가하도록 설계되어 있음(이를 처리하기 위해 모듈맵을 이용하여 깊이 우선 탐색을 통해 처리)

 

- 모듈맵

자료구조 map을 떠올리면 됨 모듈들을 URL을 통해 관리함

 

ES Module은 순환 의존성 지원

순환 의존성이란 두 개 이상의 모듈이 서로를 참조하는 경우를 말함

 

ES Module에서는 라이브 바인딩을 사용하기 때문에 순환 구조가 발생해도 빈 객체를 반환함

하지만 CommonJS는 라이브 바인딩이 아니고 사본을 메모리에 넣기 때문에 사이클이 생기면 처리할 수 없음

 

▶ ES Module 사용 방법

특징

  • 함수, var, let, const 키워드를 사용한 변수, 클래스를 export 하거나 import 할 수 있음
  • export문은 최상위 항목이어야 함(ex. 함수 안에서는 export문을 사용할 수 없음)
  • 내보내거나 가져올 때는 중괄호({})로 묶을 수 있음
  • 스크립트를 모듈로 선언하려면 <script> 요소에 type="module"을 포함시키면 됨

 

기본 사용법

각각 내보내기

export const name = 'square';

export function draw(ctx, length, x, y, color) {
  ctx.fillStyle = color;
  ctx.fillRect(x, y, length, length);

  return {
    length: length,
    x: x,
    y: y,
    color: color
  };
}

 

묶어서 내보내기

export { name, draw, reportArea, reportPerimeter };

 

묶어서 가져오기

import { name, draw, reportArea, reportPerimeter } from './modules/square.js';

 

Renaming

export문과 import문의 중괄호({}) 안에 as 키워드를 이용하여 식별 가능한 이름으로 변경하면 동일한 이름의 여러 함수를 하나의 모듈로 가져오려고 할 때 발생할 수 있는 충돌과 에러를 방지할 수 있음

 

export문 renaming

// inside module.js
export {
  function1 as newFunctionName,
  function2 as anotherNewFunctionName
};

// inside main.js
import { newFunctionName, anotherNewFunctionName } from './modules/module.js';

 

import문 renaming

// inside module.js
export { function1, function2 };

// inside main.js
import { function1 as newFunctionName,
         function2 as anotherNewFunctionName } from './modules/module.js';

 

Module Object

위와 같이 이름을 변경하는 것은 상황에 따라 코드가 길어지고 지저분해질 수 있음

이런 경우에는 각 모듈의 기능을 객체로 묶어 가져옴으로써 해결할 수 있음

 

Syntax

import * as Module from './modules/module.js';

Module.function1()
Module.function2()

 

Module 집합

 

모듈을 모아야 할 때 여러 서브 모듈을 하나의 부모 모듈로 결합하여 사용할 수 있음

// main.js
import { Square } from './modules/square.js';
import { Circle } from './modules/circle.js';
import { Triangle } from './modules/triangle.js';

 

세 모듈을 하나의 shape.js 라는 임의의 상위 모듈으로 집합시켜서 한 줄로 작성할 수 있음

 

structure

modules/
  shapes.js
  shapes/
    circle.js
    square.js
    triangle.js

 

example

// shape.js
export { Square } from './shapes/square.js';
export { Triangle } from './shapes/triangle.js';
export { Circle } from './shapes/circle.js';

 

// main.js
import { Square, Circle, Triangle } from './modules/shapes.js';

 

주의할 점은 shape.js에서 참조되고 있는 export들은 파일을 통해 리다이렉트되는 것일 뿐 실제로는 shape.js 안에 존재하는 게 아니기 때문에 같은 파일 안에서는 유용한 코드를 작성할 수 없음

 

동적 모듈 로딩

동적 모듈 로딩을 사용하면 모든 모듈들을 최상위에서 불러오는 것이 아닌, 필요할 때만 모듈을 동적으로 불러올 수 있음

 

import('/modules/myModule.js')
  .then((module) => {
    // Do something with the module.
  });


 

import() 를 함수로 호출하여 파라미터로 모듈 경로를 전달하고, 모듈 객체를 사용하여 promise를 반환하면 해당 모듈 객체가 가지고 있는 export에 접근할 수 있음

 

▶ Default export

위에 전부는 export는 내보내지는 함수, 변수, 클래스 등의 항목이 이름으로 참조되는 named export

named export는 해당 모듈들을 import 할 때에도 이 이름을 참조

named export 외에도 default export 라고 불리는 export도 존재하는데, 이는 모듈이 제공하는 기본 기능을 쉽게 만들 수 있도록 설계되었음 또한 모듈을 기존의 CommonJS와 AMD 모듈 시스템과 함께 사용하는 데에도 도움을 줌

 

default export하나의 모듈에 하나만 존재할 수 있기 때문에

import 할 때 해당 모듈이 default 값임을 알 수 있음(모듈에서 단일 값을 내보낼 때 사용)

사용할 때는 named export와 마찬가지로 선언과 분리할 수도 있고, 선언과 동시에 내보낼 수도 있음

 

default export 사용 유의점

  1. 중괄호({})가 없음
  2. 내보낸 모듈명을 as 문법없이 원하는 명칭으로 바로 사용
  3. 함수나 클래스와 달리 변수는 선언과 동시에 내보내기가 불가능하기 때문에 반드시 선언과 내보내기를 분리하여 작성

 

 Default export 사용 방법

사용 방법은 export default 키워드를 앞에 붙이는 것

 

// 선언과 내보내기 분리
export default randomSquare;

// 선언과 동시에 내보내기
export default function(ctx) {
  ...
}

 

// 기본형
import {default as randomSquare} from './modules/square.js';

// 단축형
import randomSquare from './modules/square.js';

 

ESM(ECMAScript Modules)와 CJS(CommonJS)

▶ 차이점

항목 ESM(ECMAScript Modules) CJS(CommonJS)
정의 ECMAScript (ES6 이상)에서 정의된 모듈 시스템 Node.js에서 사용하기 위해 만들어진 모듈 시스템
로딩
방식
정적(Static)
import와 export 구문이 소스 코드의 상단에 미리 정의
동적(Dynamic)
require() 함수를 통해 필요한 시점에 모듈 로드
분석
시점
컴파일 단계(코드가 번들링 되는 시점)
번들러는 이 정보를 사용하여 모듈 간의 의존성을
파악하고 번들링 단계에서 트리쉐이킹에 활용
런타임(코드가 실행되는 시점)
문법 import / export require() / module.exports
부분
로딩
쉬움(특정 모듈에서 파일단위로 default export된
일부 모듈만 가져오거나 하나의 모듈 내에서도
named export로 모듈을 부분로딩 할 수 있음)
어려움(모듈 전체를 가져와야 함)
트리
쉐이킹
효율적(사용하지 않는 코드 제거 가능) 비효율적(동적 특성 때문에 어떤것을
사용하지 않는지 파악하기 어려워 제거 불가능)

 

ESM은 번들링 단계에서 미리 모듈의 의존성을 파악할 수 있기 때문에, 불필요한 모듈들이 무엇인지 알 수 있음

따라서 번들링 단계에서 의존성이 없는 모듈들을 효과적으로 제거하여 번들링 크기를 크게 줄일 수 있음(번들링 크기가 작으면 페이지의 초기 렌더링 속도 단축)

 

- 모듈이 분석되고 시행되는 생명주기

1. 파싱 → 2. 분석 → 3. 번들링 → 4. 컴파일 →  5. 실행(런타임)

 

- 트리쉐이킹

모듈 번들러가 프로젝트에서 사용되지 않는 코드를 자동으로 제거하여 번들 크기를 최소화하는 프로세스

 

 

Reference 

[Javascript] ES Module

ES Module에 대해

JavaScript modules

CJS의 ESM 적용과 동작원리에 기반한 트리쉐이킹 효율성 이해하기