Vite를 통한 Typescript 라이브러리 만들기 - 1편
UI 라이브러리는 왜 필요한가?
IT회사의 소프트웨어 프로덕트는 대부분 브랜드 메이킹을 수행한다.
이 브랜드 메이킹은 FE 개발자와 디자이너가 함께 만들어가게 된다.
일관된 타이포그래피, 컬러, 아이콘, 레이아웃, 컴포넌트의 규격 및 동작 정책에 대해 선제적으로 디자이너가 설계할 것이다.
프런트엔드 개발자들은 이 설계도를 기반으로 프로덕트 개발 환경을 시스템화 할 수 있다.
이때 중요한 역할을 하는 것이 UI 라이브러리이다.
디자인 통일이라는 이유를 넘어 생산성, 일관성, 유지보수성, 협업 효율성을 높이는데 도움이 된다.
프로젝트 세팅하기
많은 선택지가 있지만 우리는 Vite를 사용하여 프로젝트를 생성 및 환경을 설정하자.
Vite는 빌드 단계에서 Rollup을 사용하여 번들링을 수행한다.
Rollup은 라이브러리 번들링 생태계에서 높은 사용빈도를 갖는 툴이다.
그만큼 다양한 이슈에 대한 대응이 축적되어 있고 장애 상황에 부딪혔을 때 참고할 수 있는 자료가 풍부하다.
다음 명령어를 실행하여 프로젝트를 생성하자.
1$ pnpm create vite my-ts-library --template react-swc-ts
생성된 프로젝트는 다음과 같은 구조를 갖고 있다.
처음에는 웹 서비스 개발을 위한 구조로 생성되어 있기 때문에 라이브러리를 개발하기 적합한 구조로 변경해야 한다.
다음의 코드 블럭에서 제거되어야 하는 부분에는 주석을 남겨두었다.
1.
2├── eslint.config.js
3├── index.html # 제거
4├── node_modules
5├── package.json
6├── pnpm-lock.yaml
7├── public # 제거
8│ └── vite.svg # 제거
9├── README.md
10├── src
11│ ├── App.css # 제거
12│ ├── App.tsx # 제거
13│ ├── assets # 제거
14│ │ └── react.svg # 제거
15│ ├── index.css # 제거
16│ └── main.tsx # 제거
17├── tsconfig.app.json # 제거
18├── tsconfig.json
19├── tsconfig.node.json # 제거
20└── vite.config.ts
결과적으로는 다음과 같은 구조가 될 것이다.
1.
2├── eslint.config.js
3├── node_modules
4├── package.json
5├── pnpm-lock.yaml
6├── README.md
7├── src
8│ └── index.tsx
9├── tsconfig.json # tsconfig.app.json의 설정 내용을 여기에 복사해 주세요!
10└── vite.config.ts
불필요한 리소스는 모두 제거했다.
다음으로 라이브러리를 개발하기 위한 구조를 만들자.
프로젝트 구조 설계하기
라이브러리를 만들기 위한 구조는 Barrel-Pattern을 따른다.
이 패턴은 라이브러리 구조를 설계하는 표준에 가까운 패턴이다.
Material-UI 라이브러리 또한 이 패턴을 따른다.
용도에 따라 다르겠지만 React 컴포넌트와 훅을 관리하는 라이브러리를 만드는 상황을 가정하자.
그러면 다음과 같은 폴더 구조가 형성된다.
1./src
2├── components
3│ ├── index.tsx
4│ ├── Button
5│ │ ├── index.css
6│ │ └── index.tsx
7│ ├── Dropdown
8│ │ ├── index.css
9│ │ └── index.tsx
10│ └── Modal
11│ ├── index.css
12│ └── index.tsx
13├── hooks
14│ ├── index.tsx
15│ ├── useAlert
16│ │ └── index.ts
17│ ├── useLocalStorage
18│ │ └── index.tsx
19│ └── useMediaQuery
20│ └── index.tsx
21└── index.tsx
각 모듈들의 내부 구현은 편의상 다음처럼 모의 구현으로 대체하였다.
components/Button/index.tsx
1interface ButtonProps {
2 onClick?: () => void;
3}
4
5export default function Button({ onClick }: ButtonProps) {
6 console.log({ onClick });
7 return <div>Button</div>;
8}
hooks/useAlert/index.tsx
1interface UseAlertProps {
2 message: string;
3}
4
5export default function useAlert({ message }: UseAlertProps) {
6 console.log({ message });
7 return { message };
8}
모듈 구현이 완료되었으면 components/index.tsx, hooks/index.tsx 파일에 각 모듈을 내보내는 코드를 작성하자.
components/index.tsx
1export { default as Button } from './Button';
2export { default as Dropdown } from './Dropdown';
3export { default as Modal } from './Modal';
hooks/index.tsx
1export { default as useAlert } from './useAlert';
2export { default as useLocalStorage } from './useLocalStorage';
3export { default as useMediaQuery } from './useMediaQuery';
루트 모듈인 index.tsx에 모듈을 모아 다시 내보내는 코드를 작성하자.
index.tsx
1export * from './components';
2export * from './hooks';
여기까지 구조화 및 모듈 구현을 완료하였다.
이제 라이브러리를 빌드하기 위한 설정을 해야 한다.
라이브러리 빌드를 위한 설정
라이브러리를 빌드하기 위해서는 vite.config.ts 파일을 수정해야 한다.
먼저 vite.config.ts 파일을 수정하자.
예시는 공식 문서에서도 참고할 수 있다.
vite.config.ts
1import { dirname, resolve } from 'node:path';
2import { fileURLToPath } from 'node:url';
3import { defineConfig } from 'vite';
4
5const __dirname = dirname(fileURLToPath(import.meta.url));
6
7export default defineConfig({
8 build: {
9 lib: {
10 entry: resolve(__dirname, 'src/index.tsx'),
11 name: 'my-ts-library',
12 fileName: 'my-ts-library',
13 },
14 rollupOptions: {
15 /* 번들에 포함시킬 필요가 없는 벤더 모듈을 리스팅합니다. */
16 external: ['react', 'react-dom'],
17 },
18 },
19});
build 명령어를 통해 라이브러리 빌드를 진행하여 보자.
빌드 결과 리소스를 확인하여 보면 ESM, UMD 모듈 형식으로 스크립트만 생성되어 있다.
1./dist
2├── my-ts-library.js
3├── my-ts-library.css
4└── my-ts-library.umd.cjs
UMD 모듈 형식까지는 보통 필요하지 않기 때문에 ESM, CJS 모듈 형식으로 빌드를 진행하는 것이 일반적이다.
빌드 설정에 다음 문장을 추가한다.
vite.config.ts
1build: {
2 lib: {
3 formats: ['es', 'cjs'],
4 },
5},
다시 빌드 명령어를 실행하면 ESM, CJS 모듈 형식으로 스크립트가 생성된다.
1./dist
2├── my-ts-library.cjs
3├── my-ts-library.css
4└── my-ts-library.js
문제는 각각의 컴포넌트와 훅마다 작성한 타입 인터페이스에 대한 d.ts 파일이 빌드 결과에 포함되지 않았다는 점이다.
이대로 라이브러리를 배포하면 라이브러리를 사용하는 프로젝트의 IDE상에서 가져오는 모듈의 타입 정보에 대한 IntelliSense 기능을 사용할 수 없다.
다음의 이미지와 같은 기능을 이야기 하는 것이다.
우리는 플러그인 설치를 통해 이 문제를 간단히 해결할 수 있다.
vite-plugin-dts플러그인을 패키지에 추가하고 설정을 추가하자.
vite.config.ts
1import dts from 'vite-plugin-dts';
2
3export default defineConfig({
4 plugins: [dts({ rollupTypes: true })],
5});
다시 빌드 명령어를 실행하면 모듈마다 작성한 모든 타입 인터페이스 정보가 d.ts 한 파일에 작성되어 생성된다.
dist/my-ts-library.d.ts
1import { JSX } from "react/jsx-runtime";
2
3export declare function Button({ onClick }: ButtonProps): JSX.Element;
4
5declare interface ButtonProps {
6 onClick?: () => void;
7}
8
9export declare function useAlert({ message }: UseAlertProps): {
10 message: string;
11};
12
13declare interface UseAlertProps {
14 message: string;
15}
16
17export {};
이제 라이브러리를 배포하기 위한 리소스가 모두 준비되었다.
라이브러리 배포를 위한 설정을 진행하자.
라이브러리 배포를 위한 설정
라이브러리 배포를 진행하기 위해서는 package.json 파일을 수정해야 한다.
기존의 파일 내용에서 다음 내용을 추가하자.
이때 exports 필드는 라이브러리를 사용하는 프로젝트의 런타임에서 라이브러리를 사용할 때 어떤 모듈 형식을 사용할지 결정하는 필드이다.
중요한 역할을 하는 필드이니 공식 문서를 한번쯤 참고하길 권한다.
package.json
1{
2 "name": "my-ts-library",
3 "type": "module",
4 "files": ["dist"],
5 "main": "./dist/my-ts-library.cjs",
6 "module": "./dist/my-ts-library.js",
7 "types": "./dist/my-ts-library.d.ts",
8 "exports": {
9 ".": {
10 "types": "./dist/my-ts-library.d.ts",
11 "import": "./dist/my-ts-library.js", // Browser 런타임에서는 ESM 모듈 형식을 사용하기 때문에 이 리소스에 접근하게 됩니다.
12 "require": "./dist/my-ts-library.cjs" // Node 런타임에서는 CJS 모듈 형식을 사용하기 때문에 이 리소스에 접근하게 됩니다.
13 },
14 "./style.css": "./dist/my-ts-library.css"
15 }
16}
그리고 우리의 라이브러리는 React 18버전 내에서 동작하는 것을 가정하므로 peerDependencies 필드에 해당 명세를 추가하자.
peerDependencies가 뭔가요?
peerDependencies 필드는 라이브러리를 사용하는 프로젝트에서 특정 라이브러리를 사용하기 위해 필요한 의존성 모듈의 버전 정보가 무엇인지 알려주는 필드입니다.
우리가 제작하고 있는 라이브러리가 정상적으로 동작하려면 라이브러리를 사용하는 프로젝트는 react와 react-dom패키지를 18버전 이상 19버전 미만으로 설치한 상태여야 하는 것입니다.
package.json
1"peerDependencies": {
2 "react": "^18.0.0",
3 "react-dom": "^18.0.0"
4}
npm registry에 배포하기 위해 다음 명령어를 실행하자.
로그인이 요구될 수 있다.
package.json 파일에 작성한 명세에 따라 dist 폴더의 내용을 취합하여 배포할 것이다.
배포된 라이브러리 링크
이제 이 라이브러리를 사용하는 프로젝트에서 의도한대로 동작하는지 테스트해보자.
라이브러리를 가져와서 사용하기
새로운 웹 프로젝트를 생성하고 이 라이브러리를 가져와서 사용해보자.
1$ pnpm add @hsjprime/my-ts-library
모듈의 정보와 모듈의 타입 인터페이스 정보가 IDE상에서 올바르게 인텔리센스 되는지 확인하자.
모듈의 정보는 잘 가져오는 것을 확인할 수 있다.
모듈의 타입 인터페이스 정보도 잘 노출되는 것을 확인할 수 있다.
프로젝트 런타임에서 모듈의 내부 로직이 잘 실행되는지 확인하자.
테스트 목적으로 가져온 Dropdown 모듈에 모의 구현했던 로직이 잘 실행되는 것을 확인할 수 있다.
정리
이 포스트는 당장 라이브러리를 만들고 싶은데 무엇만 하면 되는지 알고 싶은 핑거 프린세스 직장인들을 위한 글이다.
개발이라는 업을 하다보면 모든 지식과 원리를 다 알고가고 싶지만 그렇게 할 수 없는게 현실이다.
라이브러리를 만들기 위한 프로젝트 세팅, 구조화, 빌드 및 배포 설정, 사용 테스트를 진행했다.
이 일련의 작업은 라이브러리 구축을 위한 기본적인 스텝에 해당하는 작업이다.
다음편에서 우리는 라이브러리의 몇가지 부분을 개선 및 연구해 볼 것이다.
- TreeShaking
- Module Cherry Picking
- React Server Component Support