이전 게시글에서 TS는 내부 코드의 확장자를 변경하지 않는다는 것을 알 수 있었다.
그렇다면 tsconfig.json 두개로 cjs와 esm 모두 대응되게 만든다는 것은 어려운 일이라는 것을 알 수 있다.
하지만, 시작한 이상 여기서 포기하고 싶진 않았다. 그래서 번들러로 코드를 번들링한 결과를 npm에 배포하도록 했다.
왜 Rollup?
Webpack과 Vite, Rollup등 다양한 번들러가 있으나 Rollup을 택했다.
이유는
1. 라이브러리 번들링이기에 HMR이나 개발서버가 필요하지 않았다. (결과물 테스트는 Jest와 Storybook을 활용하여 확인한다)
2. Rollup은 ES6형태로 번들링 결과를 반환해서 ESM코드에 대해서 트리쉐이킹을 지원한다.
3. 사실 2번은 웹팩도 지원하지만, rollup이 빌드속도면에서 더 빠르기도하고, 써보고싶기도 했다.
Rollup 구성하기
일단 Rollup을 전역으로 깔아주었다.
npm install --global rollup
Rollup은 rollup.config.js를 통해 설정할 수 있다.
한가지 특징이라면, 한번 번들링을 할때 여러번의 번들링을 할 수 있다는 것이다.
rollup.config.js에서 배열 형태로 객체를 구성하여 가능하다.
ESM, CJS 번들링 구성
export default [
{
input: './src/index.ts',
output: {
file: './dist/esm/index.js', //결과파일 경로
format: 'es', //ESM으로 번들링
},
},
{
input: './src/index.ts',
output: {
file: './dist/cjs/index.cjs', //결과파일 경로
format: 'cjs', //CJS로 번들링
},
},
];
위와 같이 번들링 구성을 해주었다. 각 객체마다 한번씩 번들링이되어서 결과물이 dist/esm, dist/cjs 두개로 나오게 되는 것이다.
그런데 이걸 실행(rollup --config rollup.config.js)시키면 오류가 발생한다.
index.ts를 읽는중에 ./useDragIndexCarousel/useDragIndexCarousel을 가져오는걸 실패했기때문이다.
이게 왜 실패했는가? ts파일이라 확장자가 생략되어서 그렇다.
일단 확장자를 해결하기 위해서는 @rollup/plugin-node-resolve을 사용할 수 있다.
npm i @rollup/plugin-node-resolve -D
rollup.config.js
import resolve from '@rollup/plugin-node-resolve';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default [
{
input: './src/index.ts',
output: {
file: './dist/esm/index.js',
format: 'es',
},
plugins: [
resolve({
extensions, //모듈 확장자를 읽어오고, 추가로 외부 모듈도 읽어올 수 있게 된다.
}),
],
},
...
];
그리고 다시 rollup을 실행시키면... 다시 오류가 발생한다!
이건 진짜 ts문법을 그대로 읽으려고 했기때문이다.
그렇다면 ts를 읽어줄 필요가 있다.
이때 선택지가 몇 가지 존재한다.
- rollup에서 typescript을 읽을 수 있도록하기
- 다른 트랜스파일러를 통해 typescript를 읽고, 그걸 rollup이 번들링하도록 하기
- tsc한 뒤에 rollup하기
3번은 rollup에서 plugin을 제공하기때문에 굳이 그렇게 구성할 필요가 없다.
따라서 처음에는 1번을 택해서 @rollup/plugin-typescript를 사용해서 구성했었다.
import typescript from '@rollup/plugin-typescript';
export default [
{
input: './src/index.ts',
output: {
file: './dist/esm/index.js', //결과파일 경로
format: 'es', //ESM으로 번들링
plugins: [typescript()] //ts 플러그인 적용
},
},
{
input: './src/index.ts',
output: {
file: './dist/cjs/index.cjs', //결과파일 경로
format: 'cjs', //CJS로 번들링
plugins: [typescript()] //ts 플러그인 적용
},
},
];
하지만, 이왕 ESM과 CJS를 고려하는거, 다양한 브라우저도 다 고려하여 구성하기로 마음먹게되었다.
그래서 babel을 통해 ts파일을 js로 트랜스파일링 하도록 처리해주었다. 어떤 상황에서든 라이브러리를 사용할 수 있었으면 하는 마음이다.
따라서 babel설정을 해주었다.
npm i @babel/core @babel/preset-env @babel/preset-react @babel/preset-typescript @rollup/plugin-babel
plugin설정을 통해 babel을 설정해주었다.
import babel from '@rollup/plugin-babel';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default [{
input: './src/index.ts',
output: {
file: './dist/esm/index.js',
format: 'es',
},
plugins: [
babel({
babelHelpers: 'bundled',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
extensions,
}),
],
},
...
]
이러면 babel이 ts를 js로 트랜스파일링한 후에, rollup이 번들링을 하게된다.
d.ts도 각 모듈 시스템에 대해 만들어 주기위해 dts plugin을 설치해주었다.
npm i rollup-plugin-dts -D
또한 peerDependencies를 빌드에 포함시키지 않기위해 peerDepsExternal도 설치해주었다.
npm i rollup-plugin-peer-deps-external -D
rollup.config.js
이런식으로 구성해주었다.
순서대로 esm, esm에 대한 index.d.ts, cjs, cjs에 대한 index.d.cts이다.
import babel from '@rollup/plugin-babel';
import { dts } from 'rollup-plugin-dts';
import resolve from '@rollup/plugin-node-resolve';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default [
{
input: './src/index.ts',
output: {
file: './dist/esm/index.js',
format: 'es',
},
plugins: [
peerDepsExternal(),
babel({
babelHelpers: 'bundled',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
extensions,
}),
resolve({
extensions,
}),
],
},
{
input: './src/index.ts',
output: [{ file: 'dist/esm/index.d.ts', format: 'es' }],
plugins: [dts()],
},
{
input: './src/index.ts',
output: {
file: './dist/cjs/index.cjs',
format: 'cjs',
},
plugins: [
peerDepsExternal(),
babel({
babelHelpers: 'bundled',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
extensions,
}),
resolve({
extensions,
}),
],
},
{
input: './src/index.ts',
output: [{ file: 'dist/cjs/index.d.cts', format: 'cjs' }],
plugins: [dts()],
},
];
압축으로 용량줄이기
번들 사이즈를 최대한 줄이기 위해 압축도 해주었다.
uglify vs babel-minify vs terser
압축도 여러가지 선택지가 있었는데 uglify의 후속버전이 terser이고, rollup과 함께 사용하기때문에 추가적인 설정이 필요하지 않은 terser를 선택하게되었다. (babel-minify는 babel설정 파일이 또 필요한 것 같아 보인다.)
npm i @rollup/plugin-terser -D
최종 rollup.config.js
import babel from '@rollup/plugin-babel';
import { dts } from 'rollup-plugin-dts';
import resolve from '@rollup/plugin-node-resolve';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import terser from '@rollup/plugin-terser';
const extensions = ['.js', '.jsx', '.ts', '.tsx'];
export default [
{
input: './src/index.ts',
output: {
file: './dist/esm/index.js',
format: 'es',
},
plugins: [
peerDepsExternal(),
babel({
babelHelpers: 'bundled',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
extensions,
}),
resolve({
extensions,
}),
terser(),
],
},
{
input: './src/index.ts',
output: [{ file: 'dist/esm/index.d.ts', format: 'es' }],
plugins: [dts()],
},
{
input: './src/index.ts',
output: {
file: './dist/cjs/index.cjs',
format: 'cjs',
},
plugins: [
peerDepsExternal(),
babel({
babelHelpers: 'bundled',
presets: [
'@babel/preset-env',
'@babel/preset-react',
'@babel/preset-typescript',
],
extensions,
}),
resolve({
extensions,
}),
terser(),
],
},
{
input: './src/index.ts',
output: [{ file: 'dist/cjs/index.d.cts', format: 'cjs' }],
plugins: [dts()],
},
];
비교해보기
terser 적용전후
42.2kb에서 32.1kb로 압축된 것을 확인할 수 있다!
bundlephobia에서 비교한 것은 거의 차이가 나지 않는다.
아무래도 용량 자체가 작아서 효과가 미미한 것도 있는 것 같다.
후기
꽤나 쉽지는 않았지만, 그리고 React-hooks라 보통 번들러와 함께 사용될 것이기때문에 별로 중요하지는 않지만... 어쨌던 목표했던 CJS, ESM을 모두 지원하는 라이브러리를 만들어보았다.
당연하게 사용하는 것들이 전혀 당연하지 않다는 것을 다시 한 번 알게된 경험이었다.
과정에서 typescript의 모듈 시스템을 깊게 학습할 수 있었고, rollup의 구성과 다양한 플러그인, 번들사이즈와 트리쉐이킹 등 다양한 것들을 학습할 수 있어 뜻깊었다.