Skip to content

ESM 제대로 쓰기(추후 분리할 예정)

daeseong9388 edited this page Nov 28, 2022 · 1 revision

문제 상황

Typescript 파일을 실행하는 방법에는 여러가지가 있다. 그런데 같은 파일을 실행을 해도 그 방법에 따라 에러가 발생할 수도 있다.

다음과 같이 ts파일을 실행시킬 수 있다. (scripts에 명령어로도 가능)

  1. npx ts-node index.ts or npx nodemon index.ts : tsconfig.json을 따르고 ts-node의 flag들을 tsconfig.json의 ts-node 객체로 저장할 수 있다.
  2. npx tsc & node/nodemon index.js : tsconfig.json이 정의되어 있으면 파일들을 알아서 컴파일 해준다.
  3. npx vite
  4. npx jest

JSON import

문제 상황을 좀 더 구체적으로 가정해보자

// index.ts
import packages from './package.json'
  • tsconfig.jsonresolveJsonModule 값을 true로 했으니 컴파일은 문제가 없다.

그러나,

  1. npx ts-node index.ts or npx nodemon index.ts :
  2. npx tsc & node/nodemon index.js
  3. npx jest

npx vite를 제외한 3가지 경우에 대해 실패하는 것을 알 수 있다. 그러면 어떻게 고쳐야할까??

문제점 1. ECMAScript 모듈 시스템(ESM, import와 export 키워드)은 기본적으로 자바스크립트 코드만 가져올 수 있다.

  • 오랫동안 JSON 가져오기는 commonjs 모듈 형식으로 지원되었다. 다음과 같이 작성할 경우 문제 없이 실행된다.
// index.cjs
const packages = require('./package.json');
console.log(packages);
  • JSON 모듈 제안의 일반 import 에서도 JSON을 import할 수 있도록 assert문을 추가한다.
// index.ts
import packages from './package.json' assert { type: "json" };
  • 다음과 같이 작성할 경우 1, 2번의 경우를 해결할 수 있다.
  • 그러나 여전히 npx jest에서는 에러가 발생한다.

문제점 2. **npx jest에서는 에러가 발생한다.**

import * as packages from './package.json';

console.log(packages);
  • 다음과 같이 작성하면 해결된다.. 왜??

요약

  1. tsc , ts-node , nodemonimport packages from './package.json' assert { type: "json" };
  2. jestimport * as packages from './package.json';
  3. viteimport packages from './package.json' 혹은 @babel/plugin-syntax-import-assertions 의 설치?

자존심 강한 천재들의 대결…

왜 이런 슬픈 현상이 일어나는 것일까??

모듈 로더

  • Javascript 모듈을 런타임에 로드할 수 있게 만드는 구현체!

node, 모듈 시스템이 없었던 Javascript

  • 전역변수, 네임스페이스 관리가 어렵다
  • 파일 덩치가 엄청나게 커진다.

CommonJS

// index.cjs
const packages = require('./package.json');
console.log(packages);

exports.add = function (a, b) {
	return a + b;
}

// app.js
const { add } = require('./add.js');
  • 파일 단위로 분리 가능!
  • 라이브러리 함수 재사용
  • npm의 등장

한계

  • CommonJS는 언어 표준이 아니다!

  • 정적 분석이 어렵고 함수를 재정의할 수 있다. require 는 그냥 함수이기 때문에 조건에 따라 동적으로 인자를 넣을 수도 있고 다른 시그니쳐로도 사용이 가능하다. 따라서 코드가 무엇을 참조하는지 컴파일 타임에 분석하는 것이 어려워지고 실제로 사용하는 코드만 뽑아내는 tree-shaking 같은 작업도 어려워지게 된다.

    if (...) {
    	add = require('./add.js');
    }
    
    require( ... ? 'foo' : 'bar')
    
    const anotherRequire = global.require;
  • 비동기 모듈 정의가 불가능하다. 따라서 연결을 위한 초기화 작업(e.g., flag 변수를 통해 연결이 되었는지를 확인)과 매번 연결을 확인하는 과정이 필요하다.

    • 모든 의존성이 로컬 디스크에 존재해 필요한 모듈을 바로 사용할 수 있는 환경을 전제로한다.. → 동기적 호출
    • 게다가 브라우저에서는 매번 js 파일을 불러와야 했기 때문에 동기적 모듈도 비동기적으로 로딩이 되기 때문에 사용하기 어려웠다. ⇒ 이 문제는 번들러로 해결했다!

ECMAScript Modules(ESM)

import foo from 'foo'

export { foo };
export default foo;
  • 동기/비동기 로드 지원
  • 실제 객체/함수를 바인딩 → 수월한 순환 참조 관리
  • 언어 표준 & 쉬운 문법 → 최상위 수준에서만 허용!
  • 정적 분석 → 트리 쉐이킹

번들러

  • 런타임에 모듈을 가져오기 위한 목적 → 모듈 로더
  • 빌드 시 모듈을 묶고 런타임에서 추가적인 로드를 안 함 → 모듈 번들러
    • 종속성 관계 및 순서 관리
    • asset 로딩
    • 브라우저의 모듈 시스템 지원 부족
    • 프로덕트 개발 과정에서 필요한 일련의 과정까지 자동화(개발 → 빌드 → 최적화)
      • 컨벤션 → eslint
      • 컴파일, 빌드, 전처리 → Sass, Typescript
      • 소스 코드를 축소 및 번들링(최적화)
  • Webpack : 번들 + 개발에 필요한 여러가지 도구들 제공
    • asset을 javascript 코드로 변환, 분석 후 번들
    • 개발 서버
      • 라이브 리로딩
      • 핫 모듈 교체: 앱을 종료하지 않고 갱신된 파일만을 교체하는 방식
        • 변경된 모듈을 핫 모듈이라고 한다
    • 단점
      • JavaScript 모듈의 개수도 극적으로 증가 → build 및 위의 추가 기능까지 병목 현상 발생
        • 소스 코드를 업데이트 하게 되면 번들링 과정을 다시 거쳐야함. 이를 우회하기 위한 핫 모듈 교체지만 선형적으로 갱신에 필요한 시간이 증가함
      • 트리 쉐이킹을 위해 CommonJS 방식으로 모듈을 로드한 부분을 ES6로 교체해야함, 그러나 ES6 모듈 형태로 빌드 결과물을 출력할 수 없음
      • 복잡함

Vite

image

기존 번들러의 개발 서버의 단점 → Vite의 해결법

  1. 빌드 속도 자체가 느리다 → esbuild의 사용
  2. 빌드를 끝마치면 또 다시 번들을 해야한다 → 번들을 하지 말고 변경 사항이 있는 모듈만 빌드 후 업데이트
  3. 번들링을 하고 다시 웹 페이지에서 불러오기 → 핫 모듈 교체, 그러나 핫 모듈 역시 번들링을 한다 → ESM을 이용해, 브라우저가 요청을 할 때 교체된 모듈을 전달!

요약하자면, 모듈 별 빌드 + 브라우저 표준 모듈(ESM) 활용, 일종의 브라우저가 번들러의 역할을 수행을 통해 기존 번들러의 개발 과정에서의 단점을 해소했다!

그러나, 여전히 배포때에는 번들링을 수행한다!

  • 프로덕션에서 번들 되지 않은 ESM을 가져오는 것은 중첩된 import로 인한 추가 네트워크 통신으로 인해 여전히 비효율적
  • 번들링에 필수적으로 요구되는 기능인 코드 분할(Code-splitting) 및 CSS와 관련된 처리가 아직 미비

중간 요약

ESM은 CommonJS의 문제점들을 해결하는 언어 표준의 모듈 시스템이고 Vite는 ESBuild와 ESM을 이용해 Webpack 개발 서버의 문제점을 해결해 생산성을 높였다.

의문

  1. Vite는 ESM의 어떤 점을 이용해 번들링을 하지 않는 것일까?
  2. 그러면 제일 처음의 문제 상황은 어떤 원인때문에 발생한 것일까?

Vite는 ESM의 어떤 점을 이용해 번들링을 하지 않는 것일까?

링크 에서 좋은 설명을 볼 수 있지만 생각보다 깊다..

image

  • ESM 스펙은 일종의 모듈(파일)들을 파싱, 인스턴스화, 평가를 할 것인지를 정해놓은 것이라면, 스펙은 어떻게 모듈(파일)을 로딩할 것인가에 대한 이야기이다.

image

  • Vite는 브라우저가 어떻게 로딩을 하는 것인가를 이용해 번들링을 하지 않는 것이라고 말할 수 있다.

Vite는 그저 브라우저의 판단 아래 특정 모듈에 대한 소스 코드를 요청하면 이를 전달할 뿐입니다. 따라서 조건부 동적 import 이후의 코드는 현재 화면에서 실제로 사용이 되어야만 처리가 됩니다.

  • 로딩 과정에서 모듈맵을 작성한다!
  • 더 복잡한 동작 원리는 추후에 공부해봐야겠다.

문제 상황의 원인

그래서 왜? import 하는 부분이 다 다를까???

  • 동기(CommonJS) - 비동기(ESM)
    • 아직도 너무 많은 CommonJS 모듈들이 존재
    • require 를 이용해 ESM 모듈을 import할 수 없다
      • 비동기에서 동기를 사용하기는 쉽지만 동기에서 비동기를 사용하기는 어려운 것처럼

Node.js에서 ESM 사용하기

  • package.json 에서 type: module 명시하기
    • package.json 에 아래에 있는 모든 파일은 ESM으로 동작한다.
    • .js 파일은 가장 가까운 package.json 설정을 따른다.
  • .cjs, .mjs 로 명시하면 원하는 모듈 로딩 방식을 사용할 수 있다.

가짜 ESM

  1. 확장자를 명시해야한다.
    1. require 는 명시하지 않아도 확장자를 붙여주면서 찾아준다 → Redundant하게 파일 시스템에 추가적으로 접근해야한다. import foo from './foo'import foo from './foo.js'
    2. TS 컴파일/Babel 을 통해 compile / transform 될 때에도 이를 반영한다.
  2. Typescript의 ESM 지원?
    1. 여전히 .js 로 import 해야한다?
      1. import foo from './foo.ts' 는 틀린 코드이다
      2. import foo from './foo.js' 가 맞는 코드이다
      3. webpack이나 ts-node 같은 도구들과 궁합이 맞지 않는다.
        1. 따로 컴파일을 하지는 않으니까? 아직 이유는 불분명
      4. subpath import를 할 때에도 확장자를 명시해야한다…
    2. .cjs, .mjs.cts, .mts 를 지원한다!
  3. Jest, ts-node
    1. 공통점 : 모두 require의 동작을 바꾼다!!!

      1. Jest: Mocking function때문에 require의 동작 방식을 고침
      2. ts-node: require('./foo.ts') , ts 파일을 require할 수 있도록 고침

      image

결론

  1. ts-node, tsc, nodemon 은 타입스크립트 컴파일에 관한 것이고 컴파일 되었을 때 ESM을 잘 따르도록 규칙에 맞게 작성해야한다. ex) 확장자 명시, assert문
  2. Jest는 명확하지는 않지만 자체 import, require의 동작을 정의했다?
  3. Vite는 1번과 거의 동일하겠지만 @vitejs/plugin-react 때문에 babel이 관여한다. 따라서 어떻게 transform되는지 확인해봐야 한다.

참조

💊 비타500

📌 프로젝트

🐾 개발 일지

🥑 그룹활동

🌴 멘토링
🥕 데일리 스크럼
🍒 데일리 개인 회고
🐥 주간 회고
👯 발표 자료
Clone this wiki locally