🧩 junukim.dev

그리고

Typescript + React + Rollup으로 풀세트 Component Library만들기 😎

최근에 회사에서 ui-library처럼 사용할 global-package를 만들며 setup한 내용들을 공유합니다.

FCC 😘

최근에 회사에서 ui-library처럼 사용할 global-package를 만들었었다. 이름은 FCC (Flex Custome Contract - Flex 커스텀 계약서)로 고객사에서 커스텀 한 계약서(근로, 연봉, 경업금지 등..)를 간편하게 등록 및 관리할 수 있게 하였다.

계약서에 들어가야 하는 여러 placeholder들을 채워 넣어야 하는데 각각의 로직이 매우 복잡하게 이루어진다. 예를 들면 이름, 계약일, 연 근무 시간, 초과 근로 수당 등이 있다.

근로자 A연 근무 시간을 구하기 위해 주당 일하는 시간 및 휴게 시간, 휴일 근로 여부와 시간, 연봉 정보 등 여러 가지의 데이터가 필요한데 척 보기에도 복잡하게 생긴 데이터들을 소수점 단위로 계산해야 한다.

그리고 FCC에선 이런 placeholder들을 모두 계산해서 자동으로 지정된 위치에 넣어주는 역할을 한다.

FCC를 라이브러리처럼 만든 이유는 Flex Client(클라이언트 - ts) Aws lambda(서버 - js)에서 사용해야 하기에 어차피 build 파일이 필요한 구조라면 libray처럼 만들어서 어디와도 의존성을 완전 제거해보고 싶었다.

비하인드 😢

FCC는 현 product에 영향력이 강한 feature를 개발하는데 사용해야 해서 최대한 이쁘고 완벽하게 만들려고 노력했다. 그리고 셋업을 진행하며 피드백과 코드리뷰를 많이 받고 기쁜 마음으로 반영했다.

하지만... feature의 방향성이 바뀌면서 library prclose했다.

열심히 만든 FCC가 close 된 게 너무 아쉬워서 블로그 글로 남겨본다.

왜 rollup인가 ❓

rollup이 생소하고 익숙하지 않은 사람이 있을 수 있다. 애초에 라이브러리화하는 패키지가 webpack으로 빌드되는 react (cra 기준)로 개발되는데 webpack을 사용하면 된다고 생각할 수 있다.

맞다. webpack으로 빌드해도 ui-library를 만드는데 아무런 문제가 없다. 다만, webpack과 비교했을 때 라이브러리로써 얻을 수 있는 이점이 몇 있다.

  • 웹펙에서는 ESM 번들이 힘들다. -> rollup은 가능
  • 복잡한 웹펙보다 설정이 쉽다.
  • build 결과물이 좀 더 가볍다
  • webpack보다 강한 tree shaking을 지원한다

이를 제외하고 개인마다 느끼는 이점은 다 다르겠지만 보편적인 예를 들어보았다.

FCC의 rollup 👀

FCC를 설계할 때 rollup 말고 webpack을 사용할 수도 있었다. 그런데도 rollup을 사용한 이유는 위에 나열되어있는 이유가 대부분이다.

개인적으로 esmcjs를 둘 다 제공하고 싶었고, 최근에 rollup관련 블로그를 많이 읽으면서 package.json의 강력함을 알게 되어서 이를 활용 해보고 싶기도 하였다.

rollup setting ⚒

FCC는 typescript + react + scss + rollup의 구조로 이루어진다. 그리고 cjsesm 빌드를 제공한다. (FCC와 동일한 세팅을 하겠습니다.)

+eslint를 적용하고 깔끔한 환경에서 개발 하려 했었는데 전부 다 세팅하고 보니까 상위 package에서 적용되고 있는 별도의 lint때문에 내가 설정한 lint는 먹히지 않았다.

1. package.json init

먼저 package.json을 만들기 위해 터미널에 명령어를 작성해준다.

npm init

명령어를 작성하고 나면 여러 질문을 할 것이다. 전부 엔터 누르고 마지막에 yes 하면 된다. 그러면 package.json 파일이 생성된다. 그리고 이어서 필요한 모듈들을 다운로드한다.

명령어는 네 줄 다 작성해야 한다.

// babel
npm i -D @babel/core babel-preset-react-app

// rollup
npm i -D @rollup/plugin-commonjs @rollup/plugin-json @rollup/plugin-node-resolve rollup rollup-plugin-peer-deps-external rollup-plugin-postcss rollup-plugin-typescript2

// typescript, react
npm i -D @types/react typescript

// postcss
npm i -D node-sass postcss postcss-loader postcss-prefixer

모든 모듈이 다운로드 되고 나면 package.json에 peerDependency를 추가한다. (peerDependency는 간단히 말해, 현 package(FCC)에 모듈을 다운하는 게 아니라 package를 사용하는 쪽(FCC를 사용하는 곳)에서 dependeny를 갖고 있어야 한다.)

{
  ...
  "devDependencies": {
    ...
  },
  "peerDependencies": {
    "react": "^16.8.6",
    "react-dom": "^16.8.6"
  }
}

peerDependency를 끝으로 모듈 다운로드는 끝이 났다.

그리고 build파일들을 외부에서 캐치할 수 있게 타입들과 빌드파일들을 package.json에 명시해주어야한다.

+추가로 간편 명령어 등록도 해주었다.

{
  "name": "flex-customer-contract",
  "version": "1.0.0",
  "description": "components for contract in flex",
  "license": "UNLICENSED",
  // type 명시
  "types": "lib/index.d.ts",
  // 외부에서 접근할 index 명시
  "main": "dist/esm.js",
  "directories": {
    "example": "example"
  },
  "scripts": {
    "start": "npm run exam & npm run watch",
    "build": "rollup -c",
    "watch": "rollup -cw",
    "exam": "cd example && npm start",
    "lint": "eslint . --ext .js,.jsx,.ts,.tsx"
  },
  "author": "junwoo@flex.team",
  "devDependencies": {
    ...
  },
  "peerDependencies": {
    ...
  }
}

2. rollup config

다음으로 rollup.config.js 파일을 생성한다. rollup은 특이하게 config를 배열로 설정할 수 있다. 배열로 설정하게 되면 배열의 인자에 맞춰서 여러 개의 build파일이 생성된다.

import pkg from './package.json';
import typescript from 'rollup-plugin-typescript2';
import peerDepsExternal from 'rollup-plugin-peer-deps-external';
import postcss from 'rollup-plugin-postcss';
import postcssPrefixer from 'postcss-prefixer';
import resolve from '@rollup/plugin-node-resolve';
import commonjs from '@rollup/plugin-commonjs';
import json from '@rollup/plugin-json';

const extensions = ['.js', '.jsx', '.ts', '.tsx', '.scss'];

process.env.BABEL_ENV = 'production';

function setUpRollup({ input, output }) {
  return {
    input,
    exports: 'named',
    output,
    watch: {
      include: '*',
      exclude: 'node_modules/**',
    },
    plugins: [
      peerDepsExternal(),
      json(),
      resolve({ extensions }),
      commonjs({
        include: /node_modules/,
      }),
      typescript({ useTsconfigDeclarationDir: true }),
      postcss({
        extract: true,
        modules: true,
        sourceMap: true,
        use: ['sass'],
        plugins: [
          postcssPrefixer({
            prefix: `${pkg.name}__`,
          }),
        ],
      }),
    ],
    external: ['react', 'react-dom'],
  };
}

export default [
  setUpRollup({
    input: 'index.ts',
    output: {
      file: 'dist/cjs.js',
      sourcemap: true,
      format: 'cjs',
    },
  }),
  setUpRollup({
    input: 'index.ts',
    output: {
      file: 'dist/esm.js',
      sourcemap: true,
      format: 'esm',
    },
  }),
];

위 config파일을 간단히 설명하면

  • setUpRollup이라는 함수를 만들어서 rollup config setting을 한다.
  • index.ts파일을 불러와서 빌드하고 각각 dist/cjs.jsdis/dsm.js로 추출한다.
  • source-map파일을 함께 만들며 각각의 format은 cjs, esm으로 한다.
  • peerDependency를 사용할 수 있다.
  • json파일을 읽을 수 있다. (package.json을 읽기 위함)
  • ts 컴파일이 가능하다.
  • postcss를 통해 scss를 컴파일링 할 수 있다. 그리고 scss를 통해 만들어진 파일들의 이름앞에 prefix를 설정한다.

3. tsconfig.json 설정

FCC는 타입스크립트를 사용하기 때문에 ts를 관리하는 tsconfig 파일을 만들어 줘야한다. (tsconfig 설정은 ts빌드 파일 관련한 설정을 제외하곤 생략)

{
  "compilerOptions": {
    "declaration": true,
    "declarationDir": "./lib", // lib폴더에 ts 타입 속성을 빌드한다.
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "module": "es6",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "jsx": "react",
    "typeRoots": ["@types"]
  },
  "include": ["**/*.tsx", "**/*.ts", "@types", "index.ts"],
  "exclude": ["node_modules", "build", "dist", "lib", "example", "rollup.config.js"]
}

4. 테스트 환경 구성

FCC는 라이브러리기 때문에 다른 React 프로젝트에서 install 해서 테스트를 해봐야했다. 때문에 나는 cra를 이용하여 테스트 환경을 구성하였다.

// rollup.config.js가 있는 path에서
create-react-app example

그리고 cra로 만들어진 package.json 에 FCC를 install 하였다.

{
  "name": "example",
  "version": "0.1.0",
  "dependencies": {
    "@testing-library/jest-dom": "^5.11.4",
    "@testing-library/react": "^11.1.0",
    "@testing-library/user-event": "^12.1.10",
    // 요기
    "flex-customer-contract": "file:..",
    "react": "^17.0.1",
    "react-dom": "^17.0.1",
    "react-scripts": "4.0.1",
    "web-vitals": "^0.2.4"
  },
  "scripts": {
    "start": "PORT=8000 react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  }
}

package.json에서 FCC를 install 할 때 path를 file:..으로 해주었는데, example 밖에 존재하는 FCC의 package.json에서 index.js를 main으로 설정해 주었기에 동작한다.

5. index.ts 생성 및 개발

기본적인 세팅이 마무리되었으니 직접 ts, tsx파일을 생성해서 library를 만들어야한다.

먼저 명령어로 rollup watch를 실행시킨다. rollup watch는 react의 hot-loader와 같은 역할을 한다. 그리고 example에서 테스트 해야하니까 함께 실행시킨다.

// ter 1
npm run watch

// ter 2
npm run exam

본격적으로 개발에 들어가서 *.scss파일들의 타입을 정의한다.

// @types/index.d.ts
declare module '*.scss' {
  const content: { [className: string]: string };
  export = content;
}

그리고 root가 될 index.ts와 demo component Input.tsx 를 작성한다

// index.ts

export { default as Input } from './components/Input';
// components/Input.tsx
import React from 'react';

import 'index.scss';

const Input = () => {
  return (
    <div class="like-input">인풋인 척 하는 div</div>
  );
};

export default Input;

그리고 scss테스트를 위해 index.scss파일을 생성한다.

// index.scss
.like-input {
  width: 20px;
  height: 20px;
  background: red;
}

6. 라이브러리 테스트

빌드가 성공적으로 돌아가면 /example/src에서 FCC를 install후 사용할 수 있다.

import React from 'react';
import * as FCC from 'flex-customer-contract';

function App() {
  return (
    <div className="App">
      <FCC.Input />
    </div>
  );
}

export default App;

코드리뷰로 코드 다듬기 ✨

코드리뷰 과정에서 53개의 코멘트를 받았다. 그리고 그 중 반영하면서 삽질을 많이 했던 부분들을 정리해보았다.

  1. 절대경로를 설정하면 build 결과물이 회손될 수 있다. (path문제)
  2. tslint는 deprecated 되었다. -> eslint로
  3. rollup이 모노레포로 변화하면서 @rollup/~의 꼴로 package들이 변했다.
  4. rollup-plugin-typescript2 package는 build시간을 많이 저하시킨다.
  5. ant design의 build를 봤더니 .css, .js 이렇게만 build하더라. -> FCC도 이렇게 해보자!

FCC잘가 🧵

FCC가 close된 것은 아쉽지만 재미있게 셋업 했던 것 같다.

요즘들어 회사일이 많이 바빠서 피곤했는데 잠시나마 코딩이아니라 빌드 셋업을 하니까 뭔가 많이 배운 것 같아서 기분이 좋다.

+) 최근 받은 투자에 힘입어 전 직군에서 함께 의지하며 성장하는 좋은 동료 분들을 모시고 있습니다. 관심이 가시거든 많은 지원 부탁드립니다. flex 채용 링크 바로가기 😆

⇠ 이전 글 보러가기
React Synthetic Event