[Turborepo/Vite] React & Vue 컴포넌트 라이브러리화를 위한 모노레포 구축
새로운 프로젝트에서 코어와 공통모듈을 별도의 패키지로 분리하려는 시도를 하고 있는데요. Turborepo 와 Vite 를 이용하여 React 와 Vue 컴포넌트 라이브러리를 위한 모노레포 구축 과정을 기록하고자 합니다.
History
기존의 서비스는 당분간 Vue 로 유지보수하며 React 로 점진적 마이그레이션을 진행하고, 새로운 서비스는 React/Next 로 제공하기 위한 효율적인 방법을 찾고 있었습니다.
그 대안 중 하나가 "공통 레포지토리를 두어 중복된 코드들과 UI 를 한 곳에서 관리하자" 입니다!
사실 저는 SDK 나 코어 개발을 해본적이 없어 기대가 되더라구요. 또 Webpack 과 Lerna 와 같이 대중적인 툴이 아닌 새로운 툴도 써보고 싶었습니다 ㅎㅎ
( Webpack 과 Lerna 는 정말 좋은 툴입니다..! )
시험삼아 함께 공통모듈을 개발하기로 한 팀원이 NX를 이용해 구성을 해보고, 저는 Turborepo 와 Vite 를 이용해보기로 했어요!
알겠는데, 굳이 UI 까지..?
앗.. 사실 UI 컴포넌트는 Vue 프로젝트는 Vue 패키지 내에서, React 프로젝트는 React 패키지 내에서 사용하는게 맞습니다.
하지만 현재 진행하고 있는 프로젝트는 규모도 크고, 애초에 메뉴 단위로 패키지/레포지토리가 분리되어 있더라구요...ㅎㅎ
이렇다보니 메뉴를 넘나드는 모달과 같은 공통으로 사용해야하는 UI 컴포넌트도 패키지 별로 중복 작성되어 있었습니다..!
아무리 생각해도 끔찍해...
협력업체 사정상 그렇게 구현을 해야했다고 하더라구요. 별수있나요~
Vite
이번 패키지 구성을 위해 사용할 Vite 를 간단히 조사해 보았습니다. Vite 가 Webpack 에 비해 훨씬 빠른 속도로 빌드를 제공한다는데 사실일까요?
Vite 는 애플리케이션의 모듈을 Dependencies 와 Source code 두 가지 카테고리로 나누어 개발 서버의 시작 시간을 개선할 수 있습니다.
- Dependencies: 개발 시 그 내용이 바뀌지 않을 일반적인(Plain) JavaScript 소스 코드입니다. ( ts, js 파일로 작성된 함수 같이 상태값이나 외부 api에 영향을 받지 않는 코드를 의미 )
기존 번들러로는 컴포넌트 라이브러리와 같이 몇 백 개의 JavaScript 모듈을 갖고 있는 매우 큰 디펜던시에 대한 번들링 과정이 매우 비효율적이었고 많은 시간을 필요로 했습니다. - Source code: JSX, CSS 또는 React/Vue 컴포넌트와 같은 컴파일이 필요한 코드 Vite는 Native ESM 을 이용해 소스 코드를 제공하여, 브라우저에게 번들러 작업의 일부를 맡김 브라우저가 요청할 때 소스코드를 변환하고 제공하는 방식입니다.
Webpack 등과 같은 기존 모듈 번들러는 모든 소스코드를 번들링한 후 실제 페이지를 제공하지만, Vite는 Dependencies 를 ESbuild 로 사전 번들링하여, 기존 번들러 대비 10-100배 빠른 속도를 제공한다고 하네요 ㄷㄷ
Turborepo
Turborepo 는 Vercel 에서 제공하는 모노레포 구성을 도와주는 툴인데요. Turborepo 의 특징을 몇가지 나열해보겠습니다.
- Incremental builds (증분 빌드)
작업 진행상황을 캐싱하여, 이미 계산된 내용은 무시합니다. (빌드를 한번만 실행) - Parallel execution (병렬 처리)
지정된 테스크 단위로 의존성을 판단하여, 병렬적으로 작업을 진행합니다.
- 테스크 간, 연결을 정의하여 빌드를 언제, 어떻게 실행해야 할지 판단하여 최적화할 수 있습니다.
- 자바스크립트와 타입스크립트 코드베이스를 위한 고성능 빌드 시스템입니다.
이제 이 툴들을 이용하여 실질적으로 제가 패키지를 어떻게 구성했는지 보겠습니다..!
레포지토리 구조
아래와 같이 core 라고 칭하는 공통 디렉토리 하위에 react 와 vue, common 각각의 패키지를 구성합니다.
Turborepo 를 이용해 빠르게 기본적인 세팅을 구성할 수 있는데요.
npx create-turbo@latest
위 커멘드를 이용하여 yarn 과 pnpm 중 한가지를 이용하여 빠르게 세팅 할 수 있도록, 가이드를 지원합니다.
기본적으로 Next.js 기반의 패키지들로 모노레포가 구성되는데요.
apps 에는 화면에 띄울 실제 프로젝트, packages 는 ui 와 같은 라이브러리 패키지로 보시면 됩니다.
저는 프로젝트에 맞게 구조를 변경했어요~
apps 아래에 있는 test-react 와 test-vue 는 각각 이름에 해당하는 프레임워크(라이브러리)로 프로젝트가 구성되어 있습니다.
구성된 각 프로젝트에 존재하는 package.json 에 dependencies 에 필요한 공통 모듈이나 UI를 추가하면 됩니다.
"core-common": "*",
"core-react": "*",
그 다음, 최상단의 package.json 내 workspaces 에 모노레포 내에서 접근할 패키지 경로를 추가하면 Turborepo 설정 끝!
너무 쉬움 ㄷㄷ
"workspaces": [
"apps/*",
"packages/core/*"
]
Vite 라이브러리 모드
레포지토리 구조를 정하고 Turborepo 기본 설정이 끝났다면, packages의 각 라이브러리 디렉토리에서 번들러 툴을 이용하여 패키지를 구성할 차례입니다~
아래 코드는 core/react 에 있는 vite.config.ts 파일입니다.
// react
import { defineConfig } from 'vite';
import path from 'path';
import react from '@vitejs/plugin-react';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
export default defineConfig({
plugins: [
react(),
dts({
insertTypesEntry: true, // 컴포넌트 타입 생성
}),
tsconfigPaths(), // 절대 경로 생성
],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'core-react',
formats: ['es', 'cjs'],
fileName: (format) => `core-react.${format}.js`,
},
rollupOptions: {
external: ['react', 'react-dom', 'styled-components'],
output: {
globals: {
react: 'React',
'react-dom': 'ReactDOM',
'styled-components': 'styled',
},
},
},
},
});
React 를 위한 Vite 라이브러리와 빌드 파일을 만들수 있는 dts 플러그인을 추가합니다.
Vue 도 동일한 방식으로 구성할 수 있습니다. 아래는 core/vue 에 있는 vite.config.ts 입니다.
// vue
import { defineConfig } from 'vite';
import path from 'path';
import dts from 'vite-plugin-dts';
import tsconfigPaths from 'vite-tsconfig-paths';
import vue from '@vitejs/plugin-vue';
export default defineConfig({
plugins: [
vue(),
dts({
insertTypesEntry: true, // 컴포넌트 타입 생성
}),
tsconfigPaths(), // 절대 경로 생성
],
build: {
lib: {
entry: path.resolve(__dirname, 'src/index.ts'),
name: 'core-vue',
formats: ['es', 'cjs'],
fileName: (format) => `core-vue.${format}.js`,
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue',
},
},
},
},
});
plugins 과 rollupOptions 에 각 프레임워크에 해당하는 설정을 해주면 뚝딱!
package.json & tsconfig.json
이제 라이브러리 패키지에 있는 package.json 과 tsconfig.json 만 설정하면, 패키지 내에서 구현한 UI 컴포넌트를 프로젝트에서 사용할 수 있습니다.
// package.json
"files": [
"dist"
],
"types": "./dist/index.d.ts",
"main": "./dist/core-react.cjs.js",
"module": "./dist/core-react.es.js",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/core-react.es.js",
"require": "./dist/core-react.cjs.js"
}
},
files 에 dist 를 추가하고 위와 같이 경로 설정을 추가합니다. Vue 에서도 ./dist/core-vue 를 기준으로 설정합니다.
tsconfig 는 더 간단하다능~ paths 만 추가하면 됩니다 ㅎㅎ
// tsconfig.json
"paths": {
"@core-react": ["src/*"]
}
컴포넌트가 아닌 스크립트로만 작성된 모듈 패키지 core/common 도 위와 동일하게 설정하면 됩니다!
실행해보기
이제 간단하게 라이브러리로 만들 UI 컴포넌트를 구성해보죠.
interface Props {
children: React.ReactNode;
onClick: () => void;
color?: string;
}
export const ReactButton = ({ children, onClick, color }: Props) => {
return (
<Button onClick={onClick} color={color}>
{children}
</Button>
);
};
const Button = styled('button')<Pick<Props, 'color'>>`
background-color: ${({ color }) => color || 'white'};
color: ${({ color }) => (color ? 'white' : 'black')};
`;
ReactButton 컴포넌트를 작성하고 빌드합니다.
빌드가 마무리 되면 dist 파일이 생깁니다.
간단하게 alert 함수도 common/utils 에 작성후 빌드했습니다.
// core/common/utils/alert.ts
export const customAlert = (text: string) => {
window.alert(`${text}\n'core-common'에서 가져온 Alert 함수입니다.`);
};
이제 apps 내 프로젝트의 App.tsx 에서 import 만 하면 정말정말 끝입니다!!
아래와 같이 import 하여, 코드를 작성하고 실행해봅시다.
// apps/test-react/App.tsx
import { ReactButton } from 'core-react';
import { customAlert } from 'core-common';
function App() {
...
<ReactButton onClick={() => customAlert('여긴 리액트')} color="blue">
코어에서 불러온 리액트 버튼
</ReactButton>
...
}
yarn run dev 가즈아
성공적으로 UI 와 utils 함수가 적용된 것을 볼 수 있습니다..!
Vue 도 동일하게 작성하고 실행해 봅시다. 컴포넌트 이름은 VueButton 으로 하겠습니다.
<script setup lang="ts">
import { reactive } from 'vue';
interface Props {
color?: string;
onClick: () => void;
}
const props = withDefaults(defineProps<Props>(), {
color: 'white',
onClick: () => {},
});
const { color } = reactive(props);
</script>
<template>
<button
class="btn"
v-on:click="onClick"
v-bind:style="{
backgroundColor: color || 'white',
color: color ? 'white' : 'black',
}"
>
<slot />
</button>
</template>
위와 같이 컴포넌트를 작성하고, 아래 Vue 프로젝트에 import 하여 적용해보겠습니다.
// apps/test-vue/App.vue
<template>
...
<vue-button v-bind:color="green" v-bind:onClick="onClick">
코어에서 불러온 뷰 버튼
</vue-button>
...
</template>
<script>
import { VueButton } from 'core-vue';
import { customAlert } from 'core-common';
export default {
...
components: {
VueButton,
},
setup() {
const onClick = () => {
customAlert('여긴 뷰');
};
return {
onClick,
};
},
};
</script>
yarn run dev 두구두구ㄷ
Vue에서도 잘 동작하네요!
마무리하며
실무 프로젝트 전 테스트용도로 진행해 본 내용들이라, Turborepo 와 Vite 를 깊게 다루어 보지는 못했습니다만, Turborepo.. 이자식..
너무 편리하게 모노레포 세팅이 가능하고 공식문서도 잘 나와있어 좋은 인상이 남았습니다 ㅎㅎ
Vite의 속도를 아직 체감해보지 못해서 조금 아쉽지만, 현재 진행 중인 사이드 프로젝트의 Webpack 을 추후 Vite 로 바꾸어보면 장단점을 잘 알 수 있을 것 같네요!