Javascript 모듈 시스템 파헤치기 (CommonJS, ESModule)

dodog·2024년 9월 1일

원래 바로 Vite에 관한 포스팅을 작성하려고 했지만,
Vite를 제대로 이해하고 사용하기 위해서는
CommonJS와 ESModule에 대한 깊은 이해가 전제되어야 한다는 것을 깨달았다.

그래서 먼저 CommonJS와 ESModule에 대해 깊게 탐구해보는 시간을 가지려고 한다.

모듈을 제대로 공부해보기 전에는 너무나 당연한 듯 사용하고 있었기 때문에
모듈 시스템이 왜 필요하고, 또 어떤 역할을 제공하는지 전혀 몰랐다.

우리가 의식하고 있진 않지만, 공기가 없으면 소리를 들을 수 없는 것처럼,
아무 의식 없이 사용하던 모듈 시스템이 없다면 어떤 문제가 발생하게 될까?

JS에 모듈 시스템이 없다면 어떻게 될까?

  1. 전역 스코프 오염
  2. 의존성 관리의 어려움
  3. 순서 의존성

먼저 가장 잘 알려진 문제로 전역 스코프 오염이 발생한다.
a.js, b.js, c.js로 나뉘어진 js파일을 전부 html에서 script로 불러오게 되면
각 파일에서 선언한 변수와 함수는 window 객체의 전역변수가 되기 때문에
서로 선언이 중복되게 되면 마지막으로 불러온 파일의 선언으로 덮어씌이게 된다.
그렇기 때문에 순서 의존성 문제도 같이 발생하게 되는 것이다.

2번도 중요한 문제인데, 예를 들어서 auth.js 파일의 logout함수를
다른 여러 파일에서 사용할 때, 이 함수가 어떤 파일에 있는지 직관적으로 알기 어렵다.
원래는 import문을 따라가면 되지만, 파일 이름 검색을 통해서 적당한 걸 찾아 맞춰야한다.
그리고, logout함수를 사용하는 파일은 반드시 auth.js보다 뒤에 로드되어야만 하므로
순서 의존성 문제가 여기서도 발생하게 된다.
심지어 프로젝트가 복잡해지면 다이어그램을 그려서 각각의 파일이 서로 어떻게 의존하고 있는지 반드시 그림으로 그려야만 관리할 수 있는 순간이 찾아오게 될 것이다.

모듈 시스템이 어떤 역할을 제공하는가? 를 공부해보기 위해서
반대로 모듈이 사라지면 어떤 문제가 발생할지 생각해보니
새삼 모듈이 없었으면 파일 관리가 너무 힘들어 복잡한 프로젝트를 만들기가 버거워서
JS가 지금처럼 발전하기는 엄청 어려웠을 것이라는 생각이 들었다.

CommonJS의 등장

CommonJS는 Node.js가 출시되면서 JS를 브라우저 밖에서도 사용할 수 있게되어
서버, 데스크탑 앱 개발 등 복잡한 프로덕트를 만드려는 사람들이 많아졌는데,
그러기 위해서는 모듈이 반드시 필요해져서 급하게 만든 모듈 시스템이다.

그래서 원래 CommonJS는 브라우저에선 동작하지 않고 Node.js환경에서만 동작한다.
때문에 브라우저에서 동작하는 모듈인 AMD도 등장하게 되었지만,
webpack이 등장하면서 CommonJS 모듈로 작성된 코드도 번들링 과정에서
IIFE 등을 활용해서 CommonJS의 모듈처럼 동작하도록 코드를 바꿔주다보니
webpack 번들링을 거치면 굳이 AMD 필요없이 CommonJS만으로 충분해진 것이다.

CommonJS가 있는데 ESModule은 왜?

결론부터 얘기하자면, CommonJS는 어디까지나 Node.js에서 제공하는 것이지,
JS 자체적으로 지원하는 모듈 시스템이 아니였기 때문에 결국 완전한 해결책이 되지 못했다.

컴파일 시점에서의 정적 분석이 매우 어려움

ESModule의 import, export는 최상위 스코프에서만 가능해서
파일의 모든 코드를 살펴보지 않더라도 정적 분석을 쉽게 할 수 있다.

반면 CommonJS는 아래와 같은 동작도 가능하다.

// 1. 조건문 안에서도 require 함수 호출 가능
let condition = process.env.NODE_ENV === 'production';

if (condition) {
  const moduleA = require('./moduleA');
  moduleA.doSomething();
} else {
  const moduleB = require('./moduleB');
  moduleB.doSomethingElse();
}
// 2. require의 인자로 변수 등 동적으로 생성되는 값도 넣을 수 있음
const moduleName = 'moduleA';
const myModule = require('./' + moduleName);

require 함수가 이렇게 어디서든지 호출될 수 있어서 컴파일 단계에서는
moduleA를 불러올지 moduleB를 불러올지 절대로 알 수가 없고
require 함수의 인자로 변수까지 넣을 수 있기 때문에 정적 분석이 더욱 어렵다.

심지어 require로 불러온 모듈을 아래처럼 불러와서 수정까지 할 수 있다.

// moduleA.js
module.exports = {
  someFunction: () => console.log('Hello from A')
};

// main.js
const moduleA = require('./moduleA');
moduleA.someFunction = () => console.log('Modified A');

이렇듯 CommonJS는 태생적으로 정적 분석이 어렵다는 큰 한계가 존재한다.

ESLint로 저렇게 쓰지 못하게 규칙을 걸면 되지 않나?

라고 claude에게 끊임없이 태클을 걸고 꼬리질문을 던지며 괴롭혀보았는데

  1. JS에서 강제하지 않고 ESLint에 의존해야 하므로 강제성이 부족하다.
  2. require를 함수 호출로 인식하므로 결국 전체 코드를 읽을 수 밖에 없다.
  3. 정적 분석이 어려워서 런타임 단계에서야 모듈 관계를 제대로 파악할 수 있기 때문에 구조적으로 순환 의존성 문제를 완전히 해결하기 어렵다.

CommonJS는 JS 자체적으로 지원하는 네이티브 모듈이 아니라는
태생적 한계 때문에 발생하는 여러가지 문제를 완전히 해결할 수 없어서
늘 네이티브 모듈에 대한 수요가 존재해왔던 것이다.

고대하던 ESModule의 등장

CommonJS의 태생적인 한계 때문에 ES6에서 드디어 네이티브 모듈인 ESModule이 등장했다.
그럼 ESModule은 CommonJS가 해결해주지 못하는 어떤 문제를 해결해주었을까?

정적 구조

CommonJS에 대비되는 ESModule의 가장 큰 특징은 정적 구조다.
import와 export 문은 반드시 모듈의 최상위 레벨에서만 사용할 수 있어서
조건문이나 함수 내부에서 동적으로 모듈을 불러올 수 없다.
그렇기 때문에 컴파일 시점에서 모듈 의존성을 명확히 파악할 수 있게 되어
트리 쉐이킹, 코드 스플리팅 등의 최적화 기법을 더 잘 처리할 수 있게 되었다.

비동기적 로딩

동기적 로딩이 기본인 CommonJS와 달리 ESModule은 비동기적 로딩을 지원한다.
CommonJS는 기본적으로 동기적으로 동작하기 때문에 모듈을 한번에 다 불러와서
구조적으로 모듈이 나중에 필요할때 불러오도록 구현하기가 어렵고
그래서 코드 스플리팅을 적용하기가 까다롭다는 단점이 있다.

하지만 ESModule은 비동기 로딩을 지원하기 때문에
모듈이 필요할 때만 비동기로 불러와서 코드 스플리팅을 쉽게 적용할 수 있다.

통일성

ESModule은 네이티브 모듈이기 때문에 브라우저와 Node 양쪽에서 동일한 모듈을 사용할 수 있다.
아이폰은 라이트닝 케이블, 나머지 폰은 C타입을 사용해야하는게
얼마나 큰 불편함을 줬는지 생각해보면 이런 통일성은 정말 많은 이점을 가져다준다.

JS 모듈의 미래는 어떻게 될까

CommonJS와 ESModule이 각각 등장하게 된 배경과 해결한 문제들에 대해 알아보았다.
지금까지 작성한 내용만 보면 마치 CommonJS는 문제투성이고
ESModule이 유일한 구원인 것처럼 작성했다는 아쉬움이 남는데,
사실 ESModule도 '아직은' 완벽한 해결책이 되지 못했다.
기존의 모듈은 CommonJS가 너무 보편적으로 사용되었다보니
아직도 CommonJS만 지원하는 패키지들이 많이 남아있기 때문에
ESModule에게는 CommonJS와의 호환성 문제를 해결해야하는 과제가 남아있다.

Vite와 ESModule?

드디어 모듈에 대한 깊은 탐구를 마치게 되어서 글의 도입부에 얘기한 것처럼
왜 Vite를 이해하기 위해서는 모듈에 대한 이해가 필요한지 설명할 수 있게 되었다.

Vite는 ESModule을 기반으로 하는 차세대 프론트엔드 빌드 도구로
ESM의 정적 분석의 유리함, 코드 스플리팅 등에 대한 이점을 최대한 활용하여
webpack 같은 기존 CommonJS 기반 빌드 도구의 비효율성을 해결하기 위해 탄생하게 되었다.

그렇기 때문에 Vite에 대해서 깊게 파보기 위해서는 반드시 CJS, ESM 등의
모듈 시스템에 대한 이해가 기본적으로 전제되어 있어야하는 것이다.

마무리

이렇게 CJS, ESM에 대해 제대로 살펴보는 시간을 가져보고 나서야
어렵게만 들리던 CJS, ESM이라는 개념을 정말로 이해하고 가까워지게 된 것 같다.
지난번에는 webpack 을 깊게 공부해보긴 했지만 모듈에 대한 깊은 이해는 부족했는데
이렇게 모듈에 대해 깊게 알아보니 webpack의 중요성을 더욱 크게 느끼게 되었다.

다음에는 정말로 Vite에 대해 제대로 탐구해보는 글을 작성해볼 생각이다.

profile
심리학, 사회문제해결에 관심이 많은 프론트엔드 개발자

0개의 댓글