CommonJS와 ESM을 모두 지원하는 React 라이브러리 개발
모듈이란 무엇인가
프론트엔드 개발을 하다 보면 모듈(module)이라는 개념을 마주하게 된다. script 태그에 type="module"을 입력하기도 하고, React 프로젝트에서 갑자기 발생한 에러를 해결하기 위해 package.json에 module을 설정해주기도 한다. 그렇다면 모듈이 무엇일까? 프론트엔드 개발에서 모듈이란 무엇이고, 이걸 어떻게 이용할 수 있을까?
이번 글에서는 자바스크립트의 모듈 시스템인 CommonJS와 ESModule을 알아보고, 실제로 이 개념이 적용된 라이브러리를 배포해보려고 한다. 그럼 시작! 😎
1. 모듈(module)이란 무엇일까?
우선 기본으로 돌아가서, 모듈(module)이라는 개념을 알아보자.
모듈(module)의 의미
모듈(module)이란 애플리케이션을 구성하는 개별적 요소로서, 재사용 가능한 코드 조각을 말한다. 따라서, 모듈은 일반적으로 기능 단위로 분리하여 파일로 저장한다. 이때 모듈 속에 포함된 변수, 함수, 객체 등은 기본적으로 비공개 상태이기 때문에 다른 모듈에서 접근할 수 없다. 즉, 모듈은 개별적 존재로서 애플리케이션과 분리되어 존재한다.
그렇다면 이러한 각각의 모듈 속에 있는 변수, 함수, 객체를 이용하려면 어떻게 해야할까? 모듈은 기본적으로 비공개 상태이지만, 외부로 내보내고 싶은 것을 선택적으로 공개할 수 있다. 이를 export라고 한다.
자바스크립트와 모듈
하지만 자바스크립트는 다른 언어와 달리 모듈을 지원하지 않는다. C언어는 #include, 자바는 import를 이용하여 모듈을 불러올 수 있지만 자바스크립트는 이러한 기능이 없다. 자바스크립트에서 script 태그를 이용하여 파일을 나눈다고 하더라도 모든 자바스크립트 파일은 하나의 전역을 공유하기 때문에 모듈을 구현할 수 없다. 이런 상황을 해결하기 위해 나타난 것이 바로 CommonJS와 AMD라는 모듈 시스템이다. CommonJS 또는 AMD를 이용하면 브라우저 환경에서 모듈을 이용할 수 있게 되었고, Node.js에서 CommonJS를 채택하게 되었다. Node.js 환경에서 개발을 진행한다면 CommonJS 형태의 모듈 시스템을 이용할 수 있게 된 것이다.
ESM의 등장
그러던 중, 자바스크립트에서 ES6부터 모듈 시스템을 지원하기 시작했다. 이를 ESM(ES Module)이라고 부른다. ESM을 이용하면 모듈을 구현할 수 있지만, CommonJS와 문법이 다르다. 따라서, 이제 자바스크립트를 이용하여 개발할 때는 CommonJS 방식의 모듈 시스템과 ESM 방식의 모듈 시스템을 모두 고려해야 할 필요성이 생겼다.
CommonJS(CJS)와 ESM의 문법 차이
CJS와 ESM은 자바스크립트에서 모듈을 사용할 수 있게 해주는 공통점이 있지만, 문법이 다르다. 그 차이를 살펴보면 다음과 같다.
CJS 방식
// calculator.js
module.exports.calculator = (x, y) => x + y;
// main.js
const { calculator } = require('./calculator');
calculator(1, 2);
ESM 방식
// calculator.js
export function calculator(x, y) {
return x + y
}
// main.js
import { calculator } from './calculator.js';
calculator(1, 2);
- CJS는 require / module.export를 사용하고, ESM은 import / export를 사용한다.
- CJS는 동기적으로 작동하고, ESM는 비동기적으로 작동한다.
- ESM에서 CJS를 import 할 수는 있지만, CJS에서 ESM을 require 하는 것은 불가능하다.
즉, 두 모듈 시스템은 서로 호환이 잘 되지 않는다.
2. 자바스크립트 개발 환경에서 CJS와 ESM을 모두 지원해야 하는 이유
그렇다면 호환되기 어려운 CJS와 ESM을 모두 고려하며 개발해야 하는 이유는 무엇일까? 한 가지 방식으로 통일하여 사용하면 당연히 편리할 수는 있겠지만, 개발 성능을 향상시키기 위해 두 가지 시스템을 모두 지원해야 한다.
브라우저의 렌더링 과정을 살펴보면 자바스크립트의 실행이 브라우저 렌더링에 영향을 미치는 것을 알 수 있다. 브라우저 렌더링 엔진은 HTML 문서를 파싱하던 중 자바스크립트를 만나면 HTML 파싱을 중단하고 자바스크립트 엔진에게 제어권을 넘기기 때문에, 자바스크립트로 인해 페이지 렌더링이 중단될 수 있다. (브라우저 렌더링 과정 자세히 알아보기: https://inthedev.tistory.com/16)
따라서 자바스크립트 번들의 사이즈를 줄여서 렌더링 중단을 최소화해야 하는데, 이를 위해 사용하지 않는 코드는 삭제해버리는 과정(=Tree-Shaking)이 필요하다. Tree-Shaking이 CJS보다 ESM에서 훨씬 쉽기 때문에 ESM을 지원해야 한다. 변수, 조건 등을 동적으로 작성할 수 있는 CJS에 비해 ESM은 정적인 구조를 하고 있고, 이 때문에 CJS는 빌드 타임에 정적 분석을 하기 어렵지만 ESM은 빌드 타임에도 정적 분석을 하여 모듈 사이의 의존성을 확인하며 Tree-Shaking을 쉽게 진행할 수 있는 것이다.
그러나 자바스크립트 개발에서 빼놓을 수 없는 Node.js가 CJS 방식을 사용하고 있기 때문에, 현재까지의 많은 프로젝트, 패키지들이 CJS 방식을 사용하고 있다. 이러한 기존 코드나 라이브러리들과의 호환성을 유지하기 위해서 CJS 방식 또한 지원하는 것이 좋다. 따라서, CJS와 ESM을 모두 지원하는 것이 좋다.
3. CJS와 ESM을 모두 지원하는 라이브러리를 개발해보자
모듈이 무엇인지 알게 되었으니 이제 CJS와 ESM을 모두 지원하는 라이브러리를 개발해보자😎 React와 TypeScript를 기준으로 작성되었고, 개발 과정은 다음과 같다. (패키지 매니저: NPM)
1. React 프로젝트에서 배포하고 싶은 컴포넌트 만들기
프로젝트 구조는 다음과 같다.
├── cjs
│ ├── package.json
│ └── tsconfig.json
├── esm
│ ├── package.json
│ └── tsconfig.json
├── public
│ └── index.html
├── src
│ ├── index.tsx
│ └── lib
│ ├── Example1.tsx
│ └── Example2.tsx
├── .gitignore
├── .npmignore
├── package-lock.json
├── package.json
├── README.md
└── tsconfig.json
프로젝트를 생성하고 배포하고 싶은 컴포넌트를 만든다. 이 프로젝트에서 최종적으로 배포될 컴포넌트는 lib 폴더 아래에 있는 Example1.tsx, Example2.tsx이다.
2. CJS, ESM 빌드를 위한 tsconfig, package.json 작성하기
2.1. cjs 폴더의 package.json과 tsconfig.json 작성
// ./cjs/package.json
{
"type": "commonjs"
}
// ./cjs/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "CommonJS",
"outDir": "./"
}
}
2.2. esm 폴더의 package.json과 tsconfig.json 작성
// ./esm/package.json
{
"type": "module"
}
// ./esm/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"module": "ES2020",
"moduleResolution": "Node",
"outDir": "./"
}
}
2.3. 루트의 package.json과 tsconfig.json 작성
CRA로 프로젝트를 생성했다면 기본적으로 package.json에 내용이 적혀있을텐데, 아래 내용을 추가해주면 된다.
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js"
}
},
"scripts": {
// 기존 스크립트에 아래 명령어 추가
"build": "mkdir dist && npm run build:cjs & npm run build:esm",
"build:cjs": "tsc --p ./cjs/tsconfig.json --outDir ./dist/cjs",
"build:esm": "tsc --p ./esm/tsconfig.json --outDir ./dist/esm"
},
스크립트 내용은 'cjs와 esm 형식으로 컴포넌트를 컴파일하고, 컴파일 결과를 각각 dist/cjs와 dist/esm에 생성한다'는 의미이다. 나중에 배포할 때 dist 폴더 안에 생성된 파일들을 배포하게 될 것이다.
마찬가지로 tsconfg.json에도 몇 가지를 더 추가해준다.
// tsconfig.json
{
"compilerOptions": {
"target": "es5",
"types": ["node", "jest", "@testing-library/jest-dom"],
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"noFallthroughCasesInSwitch": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": false, // 변경
"jsx": "react-jsx",
"declaration": true // 변경
},
"include": ["./src/lib/**/*.tsx", "./src/lib/**/*.ts"] // 변경
}
CRA로 생성된 tsconfig에서 noEmit, declation, include 값만 위와 같이 변경해주면 된다.
3. .npmignore 생성
루트에 .npmignore을 생성한다. 이곳에는 npm 패키지에 포함되지 않았으면 하는 것들을 작성한다. 이 프로젝트에서는 패키지에 dist 폴더, package.json, README만 포함해도 정상적으로 라이브러리가 작동하기 때문에, 이것들을 제외한 항목들을 npmignore에 작성해주었다.
// .npmignore
node_modules/
src/
public/
tsconfig.json
package-lock.json
cjs/
esm/
4. 배포하기
이제 명령어만 입력해주면 배포가 완료된다. 우선 터미널에서 npm에 로그인을 해준다.
npm login
로그인이 완료되면, 빌드를 해준다.
npm run build
그리고 배포하면 완료!
npm publish
npm에 들어가서 Code를 확인하면 다음과 같이 배포가 완료되어 있을 것이다👏
dist 폴더 속에는 cjs 모듈과 esm 모듈이 모두 포함되어 있으므로, 2가지 환경을 모두 지원하는 라이브러리가 완성되었다😎
이 라이브러리를 배포하면서 이론 공부가 실무에 얼마나 도움이 되는지 새삼 또 느꼈다. 모듈이라는 개념이 자바스크립트에서는 어떻게 적용되고 있는지, 브라우저의 렌더링 과정에서 어떤 영향을 주는지, 그리고 성능 향상을 위해서 이 개념들 실제로 어떻게 대입해야 하는지를 알 수 있었다.
예전에 공부했었던 브라우저 렌더링 과정도 다시 한 번 짚고 넘어가면서, 따로 놀던 각각의 개념들을 통합해서 생각할 수 있어서 좋았다. 역시 좋은 코드를 작성할 줄 아는 개발자가 되기 위해서는 이론 공부도 게을리 해서는 안 될 것 같다👏
참고한 글
책 '자바스크립트 Deep Dive 48장'
https://toss.tech/article/commonjs-esm-exports-field
https://velog.io/@kjyook/npm%EC%97%90-%EB%AA%A8%EB%93%88-%EB%B0%B0%ED%8F%AC%ED%95%98%EA%B8%B0
'개발 이야기 > 직접 해보기' 카테고리의 다른 글
[JavaScript] React에서 Pagination 라이브러리 사용해보기 (0) | 2024.01.17 |
---|---|
[CSS] 마우스를 올리면 움직이는 3D 책 만들기 (0) | 2023.11.20 |
[React] 반응형 디자인 직접 코드로 구현해보기 (0) | 2023.08.09 |
[React Query] useQuery로 검색 기능을 만들던 중 마주친 문제 (0) | 2023.05.15 |
[React] Pagination + 검색 기능 로직 직접 만들어보기 (0) | 2023.02.18 |