최근에 회사에서 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 pr
을 close
했다.
열심히 만든 FCC가 close 된 게 너무 아쉬워서 블로그 글로 남겨본다.
rollup이 생소하고 익숙하지 않은 사람이 있을 수 있다. 애초에 라이브러리화하는 패키지가 webpack으로 빌드되는 react (cra 기준)
로 개발되는데 webpack
을 사용하면 된다고 생각할 수 있다.
맞다. webpack으로 빌드해도 ui-library를 만드는데 아무런 문제가 없다.
다만, webpack과 비교했을 때 라이브러리로써 얻을 수 있는 이점이 몇 있다.
웹펙에서는 ESM 번들이 힘들다. -> rollup은 가능
복잡한 웹펙보다 설정이 쉽다.
build 결과물이 좀 더 가볍다
webpack보다 강한 tree shaking을 지원한다
이를 제외하고 개인마다 느끼는 이점은 다 다르겠지만 보편적인 예를 들어보았다.
FCC를 설계할 때 rollup
말고 webpack
을 사용할 수도 있었다. 그런데도 rollup을 사용한 이유는 위에 나열되어있는 이유가 대부분이다.
개인적으로 esm
과 cjs
를 둘 다 제공하고 싶었고, 최근에 rollup관련 블로그를 많이 읽으면서 package.json
의 강력함을 알게 되어서 이를 활용 해보고 싶기도 하였다.
FCC는 typescript
+ react
+ scss
+ rollup
의 구조로 이루어진다.
그리고 cjs
와 esm
빌드를 제공한다. (FCC와 동일한 세팅을 하겠습니다.)
+eslint
를 적용하고 깔끔한 환경에서 개발 하려 했었는데 전부 다 세팅하고 보니까 상위 package에서 적용되고 있는 별도의 lint때문에 내가 설정한 lint는 먹히지 않았다.
먼저 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": {
...
}
}
다음으로 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.js
와 dis/dsm.js
로 추출한다.source-map
파일을 함께 만들며 각각의 format은 cjs, esm
으로 한다.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"]
}
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으로 설정해 주었기에 동작한다.
기본적인 세팅이 마무리되었으니 직접 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;
}
빌드가 성공적으로 돌아가면 /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개의 코멘트를 받았다. 그리고 그 중 반영하면서 삽질을 많이 했던 부분들을 정리해보았다.
build 결과물
이 회손될 수 있다. (path문제)@rollup/~
의 꼴로 package들이 변했다.rollup-plugin-typescript2
package는 build시간을 많이 저하시킨다..css, .js
이렇게만 build하더라. -> FCC도 이렇게 해보자!FCC가 close된 것은 아쉽지만 재미있게 셋업 했던 것 같다.
요즘들어 회사일이 많이 바빠서 피곤했는데 잠시나마 코딩이아니라 빌드 셋업을 하니까 뭔가 많이 배운 것 같아서 기분이 좋다.
+) 최근 받은 투자에 힘입어 전 직군에서 함께 의지하며 성장하는 좋은 동료 분들을 모시고 있습니다. 관심이 가시거든 많은 지원 부탁드립니다.
flex 채용 링크 바로가기 😆