On a couch

[강의리뷰] FECONF 2022: 내 import 문이 그렇게 이상했나요? 본문

프론트엔드 공부/컨퍼런스 리뷰 스터디 (24.03.11 - 05.05)

[강의리뷰] FECONF 2022: 내 import 문이 그렇게 이상했나요?

couch 2024. 3. 16. 19:34

* 컨퍼런스 영상 시청 및 리뷰 스터디를 위한 인증글입니다. 

 

강의 영상 : https://www.youtube.com/watch?v=mee1QbvaO10

강의 노트

CommonJS 모듈 시스템의 등장

javascript에 ‘모듈’이라는 개념이 없었을 때 라이브러리를 import 하려면, 스크립트 태그를 이용해서 해당 라이브러리의 전역을 참조해야 했다.

<script src="라이브러리 주소"></script>

‘commonjs’라는 모듈 시스템이 등장해, script 사용으로 인해 발생하는 비효율을 해결하고자 했다.

  • 함수 호출 한 번으로 모듈을 가져올 수 있게 되어 함수를 가져오거나 외부에 노출하는 동작이 쉬워짐.
// export
exports.add = function (){ ... }
// improt
const {add} = require('./add.js')
  • 거대한 파일 1개를 가져와 필요한 것을 수정해 쓰던 방식 ⇒ 수백 /수천 개의 작은 파일 단위로 개발하는 방식 보편화
  • 손쉬운 라이브러리 함수 재사용이 가능

CommonJS의 문제 : 언어 표준이 아니다

  • 처음으로 성공한 javascript 모듈이었지만, 표준은 아니기에 NodeJs처럼 이 시스템을 지원하는 환경이 아니면 사용할 수 없다.
  • 정적 분석이 어렵다
    • require는 ‘함수’이지 ‘키워드’가 아니기 때문에, 조건절에 따라 실제 결과가 바뀌는 일이 발생한다. 따라서 컴파일 타임에만 확인이 가능하다.
  • 조용한 require 함수 재정의
    • 함수이기 때문에 동작 재정의가 가능하다.
    • 프레임워크/라이브러리 중에는 require를 조용히 재정의해 실행시키는 것들이 있다. (e.g. Jest)
  • 비동기 모듈 정의 불가능
    • 동기로 동작하기 때문에, DB에 읽고 쓰는 명령처럼 ‘연결’이 필요한 비동기 동작을 컨트롤하기 어렵다.
    • Javascript의 특장점인 비동기 처리와의 궁합이 좋지 않음

Ecmascript Modules(ESM)의 등장

언어 표준!

  • import - export 키워드 사용
    • 임의로 재정이 불가
  • 쉬운 정적 분석
    • 조건절 사용 불가
    • 어떤 파일을 참조하는지 쉽게 알 수 있으므로, 용량 관리가 중요한 브라우저 환경에서도 효율적으로 사용할 수 있음
  • 쉬운 비동기 모듈 (feat. Top-level await)

Node.js 생태게는 ESM으로 가고 있다

생태계 내의 주요 라이브러리들이 ESM의 장점을 누리기 위해 ESM 방식을 채택하고 있으나, 그것이 여러 에러메시지의 원인이 되기도 한다.

  • 원인은 동기 함수에서 비동기 함수를 사용할 수 없기 때문 (동기인 기존 CommonJS 프로젝트 → 비동기인 ESM 프로젝트)
  • 에러를 해결할 유일한 방법은 내 프로젝트를 ESM으로 옮기는 것

NodeJs에서의 ESM 규칙

  • package.json 에서 type: module을 명기해 패키지 유형을 변경한다.
    • 해당 패키지 하위의 모든 js 파일은 esm 방식을 따른다.
    • js 파일은 가장 가까운 패키지 방식을 따른다. (내 프로젝트의 package.json < 가까운 react 라이브러리 package.json)
    • .cjs는 항상 CommonJs, mjs는 항상 ESM이다. 패키지 방을 무시하고 파일 단위로 컨트롤이 가능하다.

ESM으로 옮기기 어려운 두 가지 이유

1. 우리가 사용하는 가짜 ESM

  • TSC/Babel이 트랜스파일링을 하기 때문에 import문을 사용해도 실제로는 require문으로 변환 후 사용하게 됨 ⇒ 우리의 improt-export는 가짜였다!
  • NodeJs는 확장자를 명시하지 않아도 알아서 폴더를 순회하며 대상을 찾아다닌다 VS 진짜 ESM은 import하는 파일의 확장자를 반드시 명시해야 한다

2. 미성숙한 생태계

  • Typescript의 ESM 지원 문제 : 다소 혼란스러운 방식으로 지원
    • TS 디자인 상 ‘타입만 지워도 잘 돌아가야 하는데 확장자의 rewrite는 코드 내용을 변경하는 것이기’ 때문에, 실제로는 .ts 파일만 있더라도 .js로 import해야 한다. ⇒ webpack 등의 다른 도구과 궁합이 좋지 않음
    • .cjs, .mjs ⇒ .cts, .mts 등 별개의 확장자가 추가됨
  • 라이브러리 지원 시 subpath import 불가한 문제
    • Next.js 패키지에서 일부 모듈을 불러올 때, await import(’next/app’)으로는 에러가 나고, await import(’next/app**.js**’) 라고 명시해야 참조가 가능하다.
    • 그러나 라이브러리 개발자들은 전체 중 ‘app 부분’을 가져오라는 의미로 만들었지, ‘app.js’라는 특정 파일을 가져오라는 의미로 만들지 않았을 것.
    • 이를 해결하기 위해 package.json에 “exports”라는 필드를 지원하기 시작함. ⇒ ‘./app’이 ‘./app.js’를 의미하는 것이라고 지정할 수 있음.
  • require의 동작을 바꾸는 라이브러리들(jest, ts-node, yarn 등은)은 근본적으로 commonjs에 의존성을 가지고 있다.

ESM으로 옮기는 방법

생태계가 옮겨가는 과도기에서 생기는 이런 문제를 해결할 유일한 방법은 내 프로젝트를 ESM으로 옮기는 것

  • 파일 확장자 추가
  • require() 호출 삭제 ⇒ import - export 문으로 교체
  • esm에서 삭제된 node module을 새롭게 정의하거나 대체 api로 교체
  • if문 안에서 require를 해야 하는 등이 있다면 module에서 지원하는 ‘createRequire()’ 함수를 사용

리뷰

일을 시작한 이래로 CommonJs 방식을 사용한 경험이 거의 없어, 이를 사용하는 데 어떤 어려움이 있는지 알지 못했다.

  • CommonJS의 장/단점, EMS가 CJS에 비해 가지는 장점, 생태계의 전환 과정에 대해 알았다.
  • CommonJs를 사용하는 프로젝트의 코드를 발견했을 때, 내가 사용하는 ESM 방식 프로젝트에 어떻게 변환해 적어야 할 지 알게 되었다.
  • 동적 로딩이 필요할 때, (아직 nodejs만을 사용한 적은 없지만) nextjs에서 서버사이드 코드를 작성할 때에도 디버깅에 유용할 지식이다.

더 알아볼 것

  • [ ] Typescript의 EMS 지원은 24년 현재 어느 단계일까?
    경험상 typescript에서도 확장자 명시 없이 사용했고 문서를 찾아봤을 때도 예시에 확장자가 없는데, 이것이 ‘가짜 import’이기 때문인지 지원하는 구조가 확장되어서인지 궁금하다. 문서의 설명을 정독하는 것을 별개의 일정/목표로 잡아야겠다.