JS module 그리고 CJS와 ESM의 차이

2023.10.26

Table of Contents

각종 프로젝트를 진행하며 CommonJS와 ESM에 대해 알고 있지만 require, import 와 같은 키워드들을 혼용하며 의문을 가지게 되었다. config와 같은 파일들은 CJS를 활용하는 반면 컴포넌트를 위한 파일들은 ESM을 통해 동작한다. 이를 하나로 통합하지 못하는 이유와 차이점, 그리고 내가 겪은 각종 오류들에 대해 작성해보고자 한다.

모듈(Module)

우선 CJS와 ESM이 생겨나게된 근간을 간단하게 살펴볼 이유가 있다. 애플리케이션의 규모가 작을 때는 괜찮지만 크기가 커지면 유지보수와 재사용을 위해 여러 파일로 분리하는 것이 효율적이다. 이 분리된 파일을 모듈이라고 부른다.
그리고 이러한 모듈을 효과적으로 불러오고 모듈 단위로 구성해주는 AMD, UMD, CommonJS와 같은 라이브러리들이 등장하게 된다. 이 중 CommonJS는 Node.js 서버를 위해 만들어진 모듈 시스템으로 엄밀히 말하자면 표준 시스템은 아니다.
ES6 이후에 ES Modules이 등장하고 표준으로 등록되며 익숙한 import, export 키워드를 사용해 모듈을 효과적으로 사용할 수 있게 되었다. 결과적으로 모듈의 개발로 SPA를 고통받지 않고 구현할 수 있게 되었다.

모듈의 일반적 특징

  • 파일 스코프(모듈 레벨 스코프)를 가진다.
  • 선택적 공개가 가능하다(캡슐화) - export
  • 공개된 자산 중 일부 혹은 전체를 자신의 스코프 내로 불러들여 재사용이 가능하다. - import
자바스크립트로 코딩을 하는 것은 변수를 관리하는 것과 같다는 말과 잘 어울리는 효율적인 변수 관리 방법이라고 할 수 있다.

CJS와 ESM의 차이점

CJS에서는 requiremodule.exports를 ESM에서는 importexport를 사용하고 이들 모두 방식은 다르지만 default export를 할 수 있다. 이제 CJS와 ESM의 간단한 사용법과 차이점 확인해보자.

CommonJS (CJS)

// util.js module.exports.sum = (x, y) => x + y; // main.js const {sum} = require('./util.js'); console.log(sum(2, 4)); // 6
  • 동기적 로드
    • 모듈이 필요한 시점에 즉시 로드되고 해당 모듈의 코드가 실행될 때까지 다음 진행이 차단된다. 브라우저 환경에서 차단은 성능 혹은 동작에 문제가 발생할 수 있다.
  • 서버 사이드 혹은 런타임에서 사용
  • Tree-shaking이 어려움
    • 이는 번들의 크기에 영향을 미칠 수 있다.
  • 캐싱
    • 같은 모듈이 여러번 로드되어도 한번만 실행된다. 이는 무한 루프를 방지하고 성능을 향상시킨다.

ES Modules (ESM)

// util.js export const sum = (x, y) => x + y; // main.js import { sum } from './util.js'; console.log(sum(2, 4)); // 6
CJS와는 다른 구성, 인스턴스화, 평가 단계를 거치며 다양한 특징들을 가지게 된다.
  • 비동기적 로드
    • Top-Level Await를 지원하기 때문에 가능하다.
  • 정적 분석
    • 빌드 타임에 모듈 의존성을 파악할 수 있어 불필요한 모듈을 불러오지 않고 최적화할 수 있다.
    • Tree-shaking을 쉽게 할 수 있다.
  • dynamic import
    • 리액트의 lazy, Next.JS의 dynamic처럼 번들러를 활용해 코드 스플리팅과 모듈 로딩 성능을 최적화할 수 있다.
 
또한 CJS는 기본값으로 세팅되어 있다. 두 모듈 시스템은 또한 다른 파일 확장자(.cjs, .mjs)로 구분하고 package.json“type” 을 설정하여 프로젝트 단위의 값을 설정할 수 있다. 이러한 설정 이후에는 호환되지 않으며 ESM에서 CJS를 import하는 것만 가능하다. 너무 많은 차이점을 가지고 있기 때문이다.

경험한 오류들

학습과 프로젝트를 진행하면서 여러 오류들을 만났고 약간의 삽질 과정에서 여러 글들을 읽으며 위의 내용을 정리하게 되었다. 어떤 오류들이 있었는지 공유해보려고 한다.

Config 파일에서 CJS를 사용하는 이유

서론에 이야기했던 config 파일들에서는 대부분 require, module.exports를 활용해야 제대로 동작하는 것과 추가로 TS와 Lint는 오류를 발생시킨다. 여기서 궁금증이 생겨서 ESM으로 config를 변경하기 위해 package.json"type": "module"을 설정하거나 파일명을 .mjs 로 바꾸어 빌드와 동작까지 확인할 수 있었으나 어떤 라이브러리는 제대로 동작하지 않았다. 이 과정에서 CJS를 활용하는 몇가지 이유를 느끼게 되었다.
  • Node.js 환경 호환성
    • 프론트엔드의 프레임워크 혹은 라이브러리의 config 파일은 보통 Node.js 환경에서 실행된다. 앞서 설명한 바와 같이 Node.js는 CJS를 모듈 시스템을 기본값으로 활용하기 때문에 별다른 설정을 해줄 필요가 없어 용이한 점이 있다.
  • CJS와 ESM 동작의 차이
    • require는 동기로 동작이 이루어지고 ESM은 import와 export 구문을 바로 실행하지 않고 찾아 비동기 환경에서 동작하도록 한다. 이는 정확한 동작 순서 예측과 어떤 곳에서 문제가 발생하는지 찾기 어렵게 만든다.

Axios module import 에러 (feat. Jest)

Jest, React Testing Library를 활용한 리액트 테스트 강의를 들으며 발생했던 Axios 에러가 있다.
원인으로는 Jest가 Node.js 환경에서 동작하고 ESM을 지원하지 못하여 CJS로 트랜스파일링하는 과정이 필요하다. 반면 Axios가 업데이트를 진행하며 ESM으로 수정하였고 여기서 Jest가 Axios를 트랜스파일링하지 않고 넘어가 문제가 발생했다.
해결하기 위해 package.json에 jest 설정을 추가함으로 오류를 해결할 수 있었다. Axios의 오류라고 생각했지만 Jest가 ESM을 지원하지 못하기 때문이 더 큰 이유였다.
{ "dependencies": { ... "jest": { "transformIgnorePatterns": [ "node_modules/(?!axios)/" ] }, ... }
Axios는 v0.x에서 CJS만 사용하고 v1.x로 넘어오며 exports 를 설정하며 ESM을 지원하기 시작했다. 이제는 CJS, ESM을 둘 다 지원하는 라이브러리는 거의 필수로 보인다. 물론 라이브러리 자체 사이즈가 더 커지는 것에 대한 문제가 발생할 수 있다. 하지만 여러 라이브러리들과 유기적으로 동작하는 과정에서 유연하게 동작하기 위해서 그리고 의존성을 낮추기 위해 둘 다 지원해야 할 수 밖에 없는 점이 있다.

번외, src 폴더 내 이미지 파일 접근에 타입 오류

따로 준비한 image를 src 폴더 내에 저장하고 이를 활용하고 싶었으나 아래와 같은 오류가 발생했다.
Cannot find module '이미지 파일 경로' or its corresponding type declarations.
require를 활용해서 우선 해결되어 CJS, ESM의 차이로 인한 문제라고 생각했으나 타입과 관련된 문제였다. import하는 과정에 타입이 설정되어야 하는데 이를 추론하지 못해 발생한 오류라고 생각된다.
해결하기 위해 다른 분들은 tsconfig.jsontyperoot를 따로 설정하는 것 같았으나 Craco로 세팅한 본인과 같은 경우는 따로 설정하지 않아도 괜찮았다. 타입 선언을 위해서 아래와 같은 코드를 types 폴더에 생성했다.
//images.d.ts declare module '*.png'; declare module '*.jpg'; declare module '*.jpeg';

마치며

모듈 시스템을 공부하며 Node.js에 조금 더 초점이 맞춰질 수 있는 부분이지만 프론트엔드에서 다양한 라이브러리를 접하기 때문에 오류들을 조금은 더 빠르게 이해하고 수정할 수 있는 능력을 가지게 되었다. 이후에 나만의 라이브러리를 만들게 될 때 주의해야할 점과 생각보다 복잡한 이 둘의 차이점을 이해하고 더 효율적인 시스템을 구축할 수 있을 것이라고 생각된다.
그리고 5~6개 정도의 글을 읽으며 최대한 간략하게 정리하였다. 더 깊은 내용은 좋은 개발자 분들이 이미 정리해두었으니 이를 참조하면 좋을 것 같다.

참조


Prev
블로그 스크롤 스파이(TOC) 구현기
Next
Scroll method와 리액트에서 주의할 점