번들러 파헤치기 3 - 오픈소스 라이브러리 만들기 (rollup / react / typescript / babel)
Code/JavaScript

번들러 파헤치기 3 - 오픈소스 라이브러리 만들기 (rollup / react / typescript / babel)

반응형

Photo by Vasilis Caravitis on Unsplash

본 글은 번들러 파헤치기 시리즈의 마지막인 3부로, Rollup을 통해 리액트 컴포넌트 라이브러리를 직접 구성하고 배포하는 과정에 대해 설명하려고 합니다.


번들러 파헤치기

1부 - 모듈 시스템의 발전과 역사 (commonJS, AMD, UMD, ESM-esmodule)

2부 - 번들러의 발전과 역사 (HTTP/1.1, webpack, rollup, parcel, snowpack, exbuild, vite, turbopack)

3부 - 오픈소스 라이브러리 만들기 (rollup / react / typescript / babel)

 


3부는 구현에 집중하여 작성할 예정으로 구성과 관련된 기술적 배경에 대한 자세한 설명없이 진행할 예정으로, 본격적인 구성에 들어가기에 앞서 1부, 2부를 읽고오시는 것을 추천드립니다.


3부 리액트 오픈소스 라이브러리 만들기

 

서문

요즘 7년 만에 백수의 삶을 살고 있습니다.

운동 다음으로 제일 재밌는 게 코딩인데 고정 코딩 시간이 줄어드니 섭섭한 마음이 들길래 무얼 할까 생각하다가 번뜩 마음속에 남아있던 백로그가 하나 떠올랐습니다.

재직 당시 '추가되면 좋겠다' 이야기가 나왔던 기능이 있었는데 우선순위에 밀려 마음속에만 남아있던 태스크였습니다.

html의 textarea 태그를 사용하는 부분에 하이퍼링크를 자동으로 감지하여 인터렉션을 가능토록 하는 기능인데 기본 textarea 태그는 string 타입 말고는 html 요소를 값이나 자손으로 가질 수 없기 때문에 기존 요소에 없는 기능을 추가했어야 했습니다.

당시에도 서치를 해보았는데 마땅한 라이브러리가 없어 여유 있을 때 자체 구현을 해야겠다 생각하며 우선 넘어갔던 걸로 기억합니다.

이런 니즈를 혼자서만 겪을 것 같진 않아 확장성 높은 컴포넌트로 만들어 오픈소스를 만들면 좋겠다 생각이 들었습니다.
바로 작업을 시작했는데 이틀정도 걸려 react-link-textarea 라이브러리를 만들었네요.

사용 예제

별도로 번들러랑 빌드 설정을 진행해 본 경험이 크게 없다 보니 일주일 정도 설정 이슈와 싸우지 않을까 했는데 예상보다 빠르게 마무리가 되었습니다. 오히려 기능적인 부분에서 인터페이스와 호환성을 고려하는 데에 시간을 훨씬 훨씬 많이 썼던 것 같습니다.

저 또한 그랬지만 라이브러리, 오픈 소스 배포가 멀게만 느껴지던 분들이 계실 거라고 생각합니다.

그 원인에는 빌드 구성이 많은 비중을 차지할 것이라 생각합니다. 하지만 생각보다 어렵지 않고 누구나 할 수 있다는 걸 알았으면 해서 환경 방법과 관련 경험을 글로 작성해보려고 합니다.


1. 구현할 라이브러리의 방향

- React 기반, 컴포넌트 라이브러리
- typescript 환경에서도 동작할 것
- 용량은 최소한으로 가져갈 것

컴포넌트를 제공하는 리액트 라이브러리를 만들기
- 번들러는 Rollup을 사용하였습니다.


 

2.  환경 구성하기

2-1. 프로젝트 생성

npm init
/* package.json */

{
  "name": "test",
	...
    
   // 추가
  "type": "module"
  
  "main": "./dist/bundle.js",
  "module": "./dist/esm/bundle.js",
  
  "exports": {
    ".": {
      "require": "./dist/bundle.js",
      "import": "./dist/esm/bundle.js",
    }
  },
}

CLI를 통해 패키지를 생성하면, 초기 설정만 되어있는 package.json이 생성됩니다.

우선 type 필드에 "module"을 추가하여, ESM 모듈시스템을 사용할 것이라고 지정해 줍니다.

우리의 라이브러리는 commonJS, ESM을 모두 고려하여 총 두 개의 빌드 파일을 dist라는 폴더에 생성할 예정입니다.

commonJS, ESM이 궁금하다면?

 

main : 패키지 사용자가 패키지에 진입할 때 사용하는 진입점
module : esm 호환 환경에서 진입할 때 사용하는 진입점

exports를 통해 엔트리 포인트를 여러 개 두는 것도 가능합니다. require를 사용할 경우 cjs 번들을 참조하도록, import 구문 사용 시에는 esm 번들을 참조하도록 설정해 줄 수 있습니다.

 

2-2. Rollup 설치

npm i -D rollup
/* package.json */
{
	...
  "scripts": {
    "build": "rollup -c",
    "watch": "rollup -cw"
  // Option
    "prebuild" : "rm -rf dist",
  }
}

rollup을 설치한 이후에 pacakge.json 폴더에 build 관련 스크립트를 추가해 줍니다. 
build를 통해 현재 코드를 번들링 할 수 있습니다. 개발 중일 때는 rollup watch를 사용하는 것이 편리합니다.

! script 명령 중 'pre' 접두사가 추가된 스크립트는 해당 스크립트가 실행되기 이전에 먼저 작동됩니다. 

// rollup.config.js

export default {
    input: './src/index.js', // 진입 경로
    output: [
      {
        file: "./dist/esm/bundle.js", // 빌드 파일 저장 경로
        format: "es",                 // 출력 형식
        sourcemap: true               // 디버깅 유용
      },
      {
        file: "./dist/bundle.js",
        format: "cjs", 
        sourcemap: true
      }
    ],
};

루트 디렉터리에 rollup.config.js를 생성하고 위와 같이 설정합니다. 

빌드 파일의 진입 경로를 input에 명시한 뒤, output 파일을 명시해 줍니다.
output은 복수의 포맷을 원할 경우 위와 같이 배열의 형태로 전달 가능하며, 단일 포맷인 경우 객체를 그대로 전달합니다.

/* package.json */
{
	"peerDependencies": {
    	"react": "^18.2.0",
        "react-dom": "^18.2.0"
    }
}

우리는 react 컴포넌트를 사용해야 하기 때문에 peerDependencies에 추가하여 설치해 줍니다.

peerDependencies는 패키지가 작동하기 위해 필요한, 하지만 자동으로 설치되지 않아야 하는 종속성을 명시하는 섹션입니다.
(^) 틸드를 통해 18.2.0 이상, 19 미만 리액트 환경에서 호환되도록 명시해 줍니다.

 

2-3. 바벨 플러그인 설정 / peer plugin 설정

이후 JSX를 자바스크립트로 변환하기 위해 babel과 번들 플러그인들을 설치해 줍니다.

npm i -D @babel/core @babel/preset-env @babel/preset-react
npm i -D @rollup/plugin-babel

npm i -D rollup-plugin-peer-deps-external
// peerDependency 패키지 코드가 번들링된 결과에 포함되지 않고, import 구문으로 사용할 수 있도록 함
import babel from "@rollup/plugin-babel";
import peerDepsExternal from "rollup-plugin-peer-deps-external";


export default {
    input: "./src/index.js",
    output: [...],
    plugins: [
      // 바벨 트랜스파일러 설정
      babel({
        babelHelpers: "bundled",
        presets: ["@babel/preset-env", "@babel/preset-react"],
        extensions: [".js", ".jsx", ".ts", ".tsx"]
      }),
      peerDepsExternal()
    ]
 }

 

2-4. CSS 플러그인 설정

CSS에 대한 처리가 필요한 경우 설치합니다.

npm i -D rollup-plugin-postcss postcss-import autoprefixer
// rollup.config.js

import autoprefixer from "autoprefixer";
import cssimport from "postcss-import";
import postcss from "rollup-plugin-postcss";

export default {
  input: "./src/index.js",
  output: [...],
  plugins: [
    ...,
    postcss({
      plugins: [cssimport(), autoprefixer()]
    }),
  ]
};

rollup.config.js 파일 plugins에 추가해 줍니다.

// src/index.js

import LinkingTextarea from './LinkingTextarea';

export default LinkingTextarea;

rollup.config.js의 input에 정의되어 있는 파일에는 위와 같이 export default로 내보내고 싶은 파일을 정의할 수 있습니다.

watch 스크립트를 실행하여 jsx가 정상적으로 빌드가 되는지 확인합니다.

 

2-5. 타입스크립트 설정

npm i -D @rollup/plugin-typescript
npm i -D typescript tslib
npm i -D @babel/preset-typescript
npm i -D rollup-plugin-dts

npm i -D @types/react @types/react-dom
// # 리액트, 리액트 DOM 타입 패키지 추가
// tsconfig.json

{
  "compilerOptions": {
    "declaration": true, -> 타입 정의 파일 생성 여부
    "outDir": "dist/dts", -> 타입 정의 경로
    "emitDeclarationOnly": true, -> 바벨을 통해 ts를 js로 변환하는 경우 설정
    
    "target": "es2015",
    "module": "esnext",
    "jsx": "react-jsx",
    "sourceMap": true,
    "strict": true,
    "moduleResolution": "node",
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

 

root 디렉터리에 tsconfig.json을 생성한 뒤 위와 같이 설정해 줍니다. 
타입스크립트 환경에 최적화된 라이브러리를 위해선 타입 정의 파일 (*. d.ts)을 제공해야 합니다. 

이후 js, jsx를 ts, tsx로 바꾼 뒤 아래와 같이 rollup.config.js를 수정합니다.

// rollup.config.js

import autoprefixer from "autoprefixer";
import babel from "@rollup/plugin-babel";
import cssimport from "postcss-import";
import dts from "rollup-plugin-dts";
import peerDepsExternal from "rollup-plugin-peer-deps-external";
import postcss from "rollup-plugin-postcss";
import typescript from "@rollup/plugin-typescript";

export default [
  {
    input: "./src/index.ts",   	// ts로 input 확장자 변경
    output: [
      {
        file: "./dist/esm/bundle.js",
        format: "es",
        sourcemap: true
      },
      {
        file: "./dist/bundle.js",
        format: "cjs",
        sourcemap: true
      }
    ],
    plugins: [
      // 바벨 트랜스파일러 설정
      babel({
        babelHelpers: "bundled",
        presets: ["@babel/preset-env", "@babel/preset-react"],
        extensions: [".js", ".jsx", ".ts", ".tsx"]
      }),
      postcss({
        plugins: [cssimport(), autoprefixer()]
      }),
      typescript(),
      peerDepsExternal()
    ]
  },
  {
    // 타입 declation 위치
    input: "./dist/dts/index.d.ts",
    output: [{ file: "dist/index.d.ts", format: "es" }],
    external: [/\.css$/], // css파일이 존재할 경우, 추가
    plugins: [dts()]
  }
];

빌드 시 자동으로 d.ts 파일을 생성해 줄 수 있도록 타입 정의 부분에 d.ts 핸들링 방식을 위와 같이 설정해 준 뒤, 빌드를 진행하면 dist폴더 내부에 d.ts파일이 생성되어 있는 것을 확인할 수 있습니다.

이후 구현하고자 하는 코드를 작성하면 됩니다.

 

2-6. 개발 도중 디버깅하기

라이브러리 뼈대는 잡았는데 이제 디버깅을 어떻게 하면서 구현해야 하지?라는 생각이 들어 방법을 찾아보니 npm 배포 후 배포된 것을 다운로드하여 적용하는 방법, 심링크 방법이 존재했습니다.

전자의 경우 실제로 코드를 수정할 때마다 커밋을 하고, 또 배포하고 다시 업데이트해서 사용하는 과정을 거쳐야 하기 때문에 개발 과정에 적합한 방법은 아니었습니다.

로컬에 존재하는 프로젝트를 참조하여 다른 프로젝트에서 사용할 수 있는 후자의 방법을 선택했습니다.

심링크 방식

// 라이브러리 프로젝트에서 입력
npm link

// 라이브러리를 구동하고싶은 프로젝트에서 입력
npm link [package-name]

package-name은 작동시키고 싶은 라이브러리 프로젝트의 package.json에 존재하는 name을 입력합니다.

이렇게 연결이 완료되면 다른 프로젝트에서 라이브러리를 사용할 수 있으며, 라이브러리 코드를 수정하면 바로 반영되는 모습을 확인할 수 있습니다.

이후 개발이 완료되면 npm 배포를 진행합니다.

(리액트의 경우 다양한 환경 구성이 존재하기 때문에 Create-React-App / vite-react / Create-React-Next 세 가지 프로젝트를 디버깅 환경으로 구성하여 라이브러리의 호환성, 정상 동작 여부를 확인하시는 것을 추천드립니다.)

2-7. npm 배포

npm login

npm publish

npm 로그인 상태가 아니라면 CLI를 통해 로그인을 진행한 뒤 publish 명령어를 입력하여 패키지를 배포할 수 있습니다.
(npm 계정이 없다면 회원가입이 필요합니다.)

 

🥳 축하합니다. 우리 모두 라이브러리 배포에 성공했습니다~~~~~

 


드디어 번들러 파헤치기 시리즈가 끝났습니다.

원래는 오픈 소스 라이브러리를 제작하면서 회고 느낌으로 번들러를 구성해서 연관 지식을 가볍게 설명하는 글로 작성하려고 했는데, 관련된 걸 설명하려고 하다 보니 너무 방대해져서 결국 3부로 나누어 작성하게 되었습니다.

 

개인적으로 가장 약점이라고 생각했던 부분이 번들러, 빌드 부분이었는데 이번 기회로 많이 친해진 것 같아 뿌듯했습니다.
시간이 많이 걸리긴 했지만 라이브러리도 직접 만들면서 이번엔 유독 기억에 남는 재밌는 경험이었던 것 같네요.

같이 일했던 동료분께 라이브러리 만든 것을 말씀드렸는데, 출근 후 실제로 적용한 뒤 바로 배포했다는 연락을 받았습니다.

오랜 시간 애정을 갖고 담당하던 서비스였는데, 퇴사한 이후에도 내가 작성한 코드가 올라가 동작을 할 수 있다는 게 좀 감회가 새로웠던 것 같습니다.

사실 라이브러리를 만들긴 했지만 textarea 태그 자체가 자주 쓰이는 요소는 아니기도 해서 혼자 만들고 혼자 쓸 수도 있겠다 생각했습니다.
동료분께는 카운트 버그 아니냐면서 침착한 척했으나 사실 속으론 제발 아니어라 아니어라 하고 빌었네요.

TMI이긴 하지만, 개인적으로 개발자 인생에 미션이 하나 있는데 그건 생태계를 뒤흔들어 놓을 정도로 어마무시하게 혁신적인 오픈소스를 직접 만들고 제2의 에반 유가 되는 것입니다. (거창)

엄청나게 거창하죠? 지금은 터무니없어 보이지만 인생은 끝날 때까지 끝난 게 아닙니다. 아직 모른다! (거창)
거창한 미션에 한 발짝 내디딘 느낌도 들고 재밌는 경험이었던 것 같네요.

 

잘못된 정보는 댓글로 알려주시면 빠르게 수정하여 반영토록 하겠습니다.
다음엔 더 유익한 정보를 들고 포스팅으로 찾아오겠습니다.

감사합니다.

반응형