React 애플리케이션에서 사용 가능한 디자인 시스템 라이브러리를 만들어보자
Github: https://github.com/eunnbi/remix-practice/tree/main/packages/design-system
mkdir design-system
cd design-system
pnpm init # Create package.json
pnpm i -D typescript
pnpm tsc --init # Create tsconfig.json
다음과 같이 tsconfig.json
파일을 구성합니다.
{
"include": ["**/*.ts", "**/*.tsx"],
"exclude": ["node_modules"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ESNext"], /* 프로젝트에 포함할 라이브러리 타입 선언 파일을 선택할 수 있다. */
"skipLibCheck": true, /* 선언 파일의 타입 검사를 하지 않는다. */
"isolatedModules": true, /* 각 파일이 다른 파일의 정보에 의존하지 않고, 독립적으로 트랜스파일될 수 있도록 보장한다. */
"moduleDetection": "force", /* 프로젝트의 함수와 변수의 범위를 결정한다. force 옵션은 import와 export 구문 존재 여부 상관없이 모든 TS 파일을 스크립트가 아닌 모듈로 처리한다. */
"esModuleInterop": true, /* CommonJS와 ESModule 간의 호환성을 높인다. CommonJS 모듈을 ESModule에서 쉽게 가져올 수 있도록 컴파일 시 JS 코드를 추가한다. */
"allowImportingTsExtensions": true, /* import 문에서 타입스크립트 파일 확장자를 포함할 수 있다. */
"jsx": "react-jsx", /* JSX 구문을 처리하는 방법을 지정한다. 타입스크립트는 JSX 구문의 트랜스파일링을 기본적으로 지원한다. */
"moduleResolution": "Bundler", /* 각 import가 어떤 모듈을 가리키는지 해석하는 방법을 지정한다. 번들러가 처리하는 방식과 유사한 방식으로 모듈을 해석하도록 설정한다. */
"resolveJsonModule": true, /* JSON 파일을 TS 프로젝트로 가져올 수 있다. */
"module": "ESNext", /* 트랜스파일 수행 시 사용할 모듈 시스템을 지정한다. */
"target": "ESNext", /* JS 코드를 생성할 때 타겟팅하는 ECMAScript 버전을 지정한다. */
"strict": true, /* 엄격한 타입 검사를 활성화한다. */
"noEmit": true, /* JS 파일을 생성하지 말라고 지시한다. 만약 외부 도구를 사용해 트랜스파일한다면 이 옵션을 활성화해 tsc를 트랜스파일러가 아닌 린터로 활용할 수 있다. */
/* Path alias */
"paths": {
"~/*": ["./src/*"]
}
}
}
tsc
CLI를 활용해 JS 코드를 생성할 수 있지만, esbuild
, babel
, swc
와 같은 도구를 이용해 트랜스파일을 수행할 수 있다.const enums
와 네임스페이스와 같은 일부 타입스크립트 기능에서 런타임 문제가 발생할 수 있다.isolatedModules
옵션을 설정하면 단일 파일 변환 프로세스로 변환할 수 없는 TypeScript 기능을 사용할 경우 경고가 표시된다.preserve
: JSX 구문을 그대로 유지한다.react
: JSX를 React.createElement
호출로 변환한다. React 16 이전 버전에 유용하다.react-jsx
: JSX를 _jsx
호출로 변환하고, react/jsx-runtime
에서 자동으로 가져온다. React 17 이상 버전에 유용하다.pnpm i -D vite
프로젝트 루트에 vite.config.ts
파일을 추가한다.
// vite.config.ts
import { defineConfig } from 'vite';
export default defineConfig({})
pnpm i --save-peer react react-dom # add packages to peer dependencies and install them as dev dependencies
pnpm i -D @types/react @types/react-dom
pnpm i -D @vanilla-extract/css
peerDependencies
란 패키지가 의존하지만, 최종 사용자가 직접 설치해야 하는 의존성을 말한다. JavaScript 패키지에서 특정 라이브러리나 프레임워크와 함께 동작해야 하는 경우 이를 명시적으로 지정하기 위해 사용된다. 동일한 라이브러리나 프레임워크가 여러 버전으로 중복 설치되는 것을 방지한다.
pnpm i -D @vitejs/plugin-react @vanilla-extract/vite-plugin vite-plugin-dts vite-tsconfig-paths
// vite.config.ts
export default defineConfig({
plugins: [
react(),
vanillaExtractPlugin(),
tsconfigPaths(),
dts({ include: ['src'] }),
],
})
Vite의 Library mode를 활용해 빌드 옵션을 구성한다.
build.lib
옵션을 사용해 엔트리 파일과 파일 이름 및 형식을 지정한다.build.rollupOptions.external
옵션을 사용해 라이브러리에 번들링하고 싶지 않은 종속성을 외부화한다.export default defineConfig({
// ...
build: {
lib: {
entry: resolve(__dirname, 'src/index.ts'),
fileName: (format, entryName) => `${entryName}.${format}.js`,
formats: ['es', 'cjs'],
},
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
}
}
})
vite-plugin-lib-inject-css 플러그인을 사용해 JS 번들 파일 상단에 CSS 파일 임포트문을 추가한다.
pnpm i -D vite-plugin-lib-inject-css
// vite.config.ts
export default defineConfig({
plugins: [
// ...
libInjectCss(),
],
})
현 설정으로는 라이브러리의 어떤 모듈을 import할 때 사용되지 않은 JS 코드와 모든 스타일이 담긴 CSS 파일이 불필요하게 번들에 포함된다.
예를 들어, Button 컴포넌트만 사용했는데 빌드 결과물에 TextField 컴포넌트 관련 코드와 스타일이 포함된다.
libInjectCSS
플러그인은 각 청크에 대해 별도의 CSS 파일을 생성하고 각 청크의 출력 파일 시작 부분에 가져오기 문을 포함한다. 따라서 JavaScript 코드를 분리하면 해당 JavaScript 파일을 가져올 때만 필요한 별도의 CSS 파일이 생긴다.
모든 파일을 rollup 진입 지점으로 번환해보자.
pnpm i -D glob
export default defineConfig({
// ...
build: {
// ...
rollupOptions: {
external: ['react', 'react/jsx-runtime'],
input: Object.fromEntries(
glob
.sync('src/**/*.{ts,tsx,css}', {
ignore: ['src/**/*.d.ts'],
})
.map((file) => [
// 1. The name of the entry point
relative('src', file.slice(0, file.length - extname(file).length)),
// 2. The absolute path to the entry file
fileURLToPath(new URL(file, import.meta.url)),
])
),
output: {
chunkFileNames: 'chunks/[name].[hash].js',
assetFileNames: 'assets/[name][extname]',
entryFileNames: '[name].[format].js',
},
},
},
});
Before
dist
- index.css
- index.es.js
- index.cjs.js
- index.d.ts
After
dist
- assets
- Button.css (imported by Button.{es.cjs}.js)
- TextField.css (imported by TextField.{es.cjs}.js)
- components
- Button
- Button.es.js (imported by index.es.js)
- Button.cjs.js (imported by index.cjs.js)
- Button.d.ts
- TextField
- TextField.es.js (imported by index.es.js)
- TextField.cjs.js (imported by index.cjs.js)
- TextField.d.ts
- index.es.js
- index.cjs.js
- index.d.ts
type
필드는 모듈 시스템을 결정한다. commonjs
(기본값) 혹은 module
로 지정할 수 있다.main
필드와 exports
필드를 사용해 패키지의 진입점을 설정한다. exports
필드는 main
필드와 다르게 여러 개의 진입점을 설정할 수 있다.module
필드는 main
필드와 유사한 목적으로 사용된다. ES6 모듈과의 호환성을 위한 패키지 진입 경로이다. types
필드를 사용해 타입 선언 파일 경로를 지정한다.files
필드는 패키지가 설치될 때 포함될 항목을 가리킨다.sideEffects
필드는 특정 파일이나 모듈이 부수 효과(side effect)가 있는지 여부를 명시하는 데 사용된다. 주로 트리 쉐이킹 최적화를 위해 사용되며, 번들러가 부수 효과가 없는 코드를 안전하게 제거하도록 돕는 설정이다.false
: 패키지 내의 모든 파일이 부수 효과가 없음을 나타낸다. 즉, 가져오지 않은 코드라면 안전하게 제거할 수 있다.{
"name": "design-system",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.es.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.es.js",
"require": "./dist/index.cjs.js"
},
"./theme.css": "./dist/assets/theme.css.ts.css",
"./reset.css": "./dist/assets/styles/reset.css"
},
"files": ["dist"],
"sideEffects": ["**/*.css"],
}