컴포넌트 라이브러리를 개발하면서 번들 크기 문제로 한참 고민했다. 사용자가 우리 라이브러리에서 단 하나의 컴포넌트만 사용하더라도 전체 라이브러리가 번들에 포함된다면 성능에 좋지 않을 거라고 생각했다. 그래서 트리 쉐이킹 최적화 작업을 진행했고, 이 과정에서 배운 내용을 공유하려고 한다.
주요 UI 라이브러리 번들 크기 비교
먼저 다른 인기 라이브러리들과 우리 라이브러리의 번들 크기를 비교해봤다.
| 라이브러리 | Unpacked Size | Gzipped Size | 단일 컴포넌트 사용 시 |
| 우리 라이브러리 | 947 kB | ~185 kB | ~15-30 kB |
| Chakra UI | 2.5 MB | ~520 kB | ~30-45 kB |
| Mantine | 1.8 MB | ~410 kB | ~20-35 kB |
| Material UI | 3.3 MB | ~680 kB | ~35-50 kB |
생각보다 우리 라이브러리가 다른 인기 라이브러리들보다 전체 크기가 작았다. 하지만 더 중요한 건 사용자가 실제로 필요한 컴포넌트만 가져갈 때의 크기다. 여기서 트리 쉐이킹이 중요한 이유가 나온다.
트리 쉐이킹과 preserveModules 옵션의 비밀
트리 쉐이킹이란 사용하지 않는 코드를 최종 번들에서 제외하는 기술이다. 간단히 말하면 나무를 흔들어서 죽은 잎사귀만 떨어뜨리는 것과 비슷하다.Rollup 설정에서 preserveModules: true 옵션은 트리 쉐이킹 구현의 핵심인데, 처음에는 이 설정의 중요성을 몰랐다.
output: [
{
dir: 'dist/esm',
format: 'esm',
preserveModules: true,
preserveModulesRoot: 'src'
}
]
이 옵션이 왜 그렇게 중요할까? 처음에는 단순히 "모듈 구조를 유지한다"는 설명만 보고 별로 중요하지 않다고 생각했다. 하지만 실제로 적용해보니 차이가 확 와닿았다.preserveModules: true가 하는 일:
- 파일 구조 그대로 유지:
- 이 설정이 없으면 Rollup은 모든 코드를 하나의 큰 파일로 뭉쳐버린다.
- true로 설정하면 src 폴더 구조를 그대로 dist에 유지해준다.
- 개별 컴포넌트 임포트 가능:
- 사용자가 이런 식으로 가져올 수 있게 해준다:
import Button from 'library/Button'; // 파일 구조가 유지되어서 가능
- 빌드 도구의 정적 분석 도움:
- 모듈 경계가 유지되어 Webpack이나 Rollup 같은 번들러가 사용하지 않는 코드를 더 정확히 식별할 수 있다.
한참을 시행착오 겪으면서 이해한 내용을 코드로 표현하면 이렇다:
// preserveModules: false (기본값)일 때:
// dist/index.js
import React from 'react';
// 모든 컴포넌트 코드가 이 파일에 포함됨
export const Button = () => {...};
export const Slider = () => {...};
// ... 다른 모든 컴포넌트
// preserveModules: true일 때:
// dist/components/button/Button.js
import React from 'react';
export const Button = () => {...};
// dist/components/slider/Slider.js
import React from 'react';
export const Slider = () => {...};
이 작은 차이가 트리 쉐이킹 효과에 엄청난 영향을 미친다!
Mantine의 접근 방식 분석
Mantine의 Accordion 컴포넌트 코드를 살펴보면서 많은 인사이트를 얻었다.
// Mantine의 코드에서 배운 패턴
import { useId, useUncontrolled } from '@mantine/hooks';
import {
Box,
BoxProps,
createVarsResolver,
// ... 다른 가져오기
} from '../../core';
// 하위 컴포넌트 구조
Accordion.Item = AccordionItem;
Accordion.Panel = AccordionPanel;
Accordion.Control = AccordionControl;
Accordion.Chevron = AccordionChevron;
Mantine이 트리 쉐이킹을 위해 사용하는 방법들:
- 필요한 것만 정확히 가져오기: import * 대신 필요한 것만 명시적으로 가져온다.
- 하위 컴포넌트를 정적 속성으로: 번들러가 컴포넌트 관계를 쉽게 파악할 수 있게 한다.
- 공통 코드 재사용: 유틸리티 함수를 적절히 분리하고 재사용한다.
이런 패턴을 보고 우리 라이브러리도 비슷하게 구현해봤다.
다른 라이브러리들과 비교
Chakra UI는 어떻게 하나?
Chakra UI는 약간 다른 접근 방식을 사용한다:
// Chakra UI 임포트 방식
import { Button } from '@chakra-ui/react' // 전체 라이브러리에서
import Button from '@chakra-ui/button' // 개별 패키지에서
Chakra UI는 모노레포 구조로 각 컴포넌트를 별도 패키지로 발행한다. 효과적이지만, 여러 컴포넌트를 사용할 때 의존성 관리가 복잡해질 수 있다고 생각했다.
Mantine은?
Mantine은 단일 패키지 내에서 모듈 구조를 잘 유지한다:
/ Mantine 임포트 방식
import { Button } from '@mantine/core' // 전체 라이브러리에서
Mantine도 내부적으로 preserveModules: true를 사용하는 것 같았다. 그래서 우리도 이 접근 방식을 따라가기로 했다.
우리 라이브러리의 선택
우리는 두 장점을 모두 취하는 방식을 선택했다:
// 우리 라이브러리의 임포트 방식
import { Button } from 'react-common-components-library' // 전체에서
import Button from 'react-common-components-library/Button' // 개별 컴포넌트
package.json의 exports 필드를 설정해서 두 가지 방식 모두 지원하도록 했다:
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/types/index.d.ts"
},
"./styles.css": "./dist/styles.css",
"./*/styles.css": "./dist/*/styles.css",
"./*": {
"import": "./dist/esm/components/*/index.js",
"require": "./dist/cjs/components/*/index.js",
"types": "./dist/types/components/*/index.d.ts"
}
},
실제 번들 크기 측정 결과
테스트 앱을 만들어서 각 라이브러리의 Button 컴포넌트만 가져와 사용했을 때 번들 크기를 비교해봤다:
// 각 라이브러리에서 Button만 사용
import { Button } from 'react-common-components-library';
// vs
import { Button } from '@chakra-ui/react';
// vs
import { Button } from '@mantine/core';
| 라이브러리 | Button만 사용 시 번들 크기 |
| 우리 라이브러리 | ~18 kB |
| Chakra UI | ~42 kB |
| Mantine | ~32 kB |
트리 쉐이킹 최적화 팁
트리 쉐이킹 최적화 작업을 하면서 배운 몇 가지 팁을 공유한다:
- preserveModules: true 꼭 사용하기:
- 이거 하나로 트리 쉐이킹 효과가 극대화된다.
1. 사이드 이펙트 표시하기:
- CSS 파일만 사이드 이펙트로 표시하고 나머지는 제거 가능하게 한다.
- ESM 형식 지원하기:
- CJS와 ESM 모두 지원하되, ESM을 기본으로 설정하는 게 좋다.
- 개별 컴포넌트 접근 제공하기:
- exports 필드로 개별 컴포넌트 접근을 지원한다.
// 이렇게 하면 안 된다
import { Button } from './components';
// 이렇게 하는 게 좋다
import Button from './components/Button';
마무리
번들 크기 비교를 통해 우리 라이브러리가 Chakra UI나 Mantine보다 더 작은 번들 크기를 가지고 있음을 확인했다. preserveModules: true 설정은 처음에는 중요한지 몰랐는데, 알고보니 트리 쉐이킹 최적화의 핵심이었다.중요한 건 전체 패키지 크기가 아니라 실제 사용자 애플리케이션에서의 성능이라는 걸 깨달았다. 우리 라이브러리는 작은 번들 크기와 효율적인 트리 쉐이킹 지원으로 사용자 경험을 최적화하는 데 초점을 맞추고 있다.다음에는 성능 최적화와 접근성에 대한 이야기를 해볼까 한다.
'생각 > react' 카테고리의 다른 글
[GitHub Pages 배포기] React 컴포넌트 라이브러리 문서화하고 배포하기 (0) | 2025.03.04 |
---|---|
[npm 라이브러리 제작기] 라이브러리 만들 때 어떻게 만들까? (0) | 2025.02.24 |
리액트 Suspense & lazy (0) | 2023.06.29 |
useMemo와 useCallback (0) | 2023.06.26 |
react소리 조절 (0) | 2023.06.23 |