프로젝트를 세팅할 때마다 자연스럽게 설치하게 되는 것들이 있습니다.
react, react-dom, @tanstack/react-query 같은 라이브러리들 그리고 전역 상태를 관리해주는 상태관리 라이브러리도 그중 하나죠.
예전에는 redux를 주로 썼고 최근에는 더 단순하고 가벼운 zustand를 자주 사용합니다.
이번에도 여느 때처럼 zustand를 설치하려던 순간, 문득 이런 생각이 들었습니다.
“이런 라이브러리는 내부에서 어떻게 상태를 관리할까? React가 아닌 환경에서도 동작할 수 있는데, React에서는 또 어떻게 연결되는 걸까? 나도 한번 직접 만들어보면 재미있지 않을까?”
단순히 쓰는 데서 그치지 않고, React와 독립적으로 상태를 관리하면서도 React와는 어떻게 잘 이어지는지 그 내부 동작을 이해하고 싶어졌습니다.
그렇게 해서 나만의 작고 단순한 상태 관리 라이브러리를 만들어보기로 결심했습니다.
라이브러리를 배포할 때는 다양한 실행 환경을 고려해야 합니다.
요즘은 대부분 ES Modules(ESM) 를 사용하지만 여전히 CommonJS(CJS) 나 브라우저에서 바로 불러서 쓰는 UMD(IIFE) 형태도 필요합니다.
그래서 번들러로는 tsup을 선택했고, ESM, CJS, UMD 형태로 모두 빌드되도록 설정했습니다.
각각의 형식에 대해 간단히 설명드리면
최신 JavaScript 표준 모듈 시스템입니다.
import
, export
구문을 사용합니다.
트리쉐이킹이 잘 동작하고 브라우저 뿐만 아니라 Node.js 가능합니다.
빌드 시점에 정적 분석합니다.
ESM의 import/export는 코드가 실행되기 전에 파일을 읽어 의존성 그래프를 구성할 수 있도록 정적으로 정의됩니다.
따라서 빌드 도구가 코드를 읽으면서(빌드 시점) 사용하지 않는 코드를 제거하고 최적화를 할 수 있습니다.
import { something } from 'my-lib';
Node.js에서 오래 사용된 모듈 시스템입니다.
require(), module.exports 문법을 사용합니다.
현재도 많은 Node.js 프로젝트와 오래된 도구들이 CJS를 사용하고 있습니다.
정적 분석이 어렵고 실행 시점까지는 정확한 모듈 경로를 알 수 없습니다.
const { something } = require('my-lib');
브라우저에서
빌드하면 보통 하나의 .min.js 파일이 생성되고 브라우저에서는 window.MyLib 같은 전역 객체로 접근할 수 있습니다.
<script src="my-lib.umd.js"></script>
<script>
MyLib.something();
</script>
“특정 프레임워크(React, Vue, Svelte 등)에 의존하지 않고 어디서든 사용할 수 있다”
즉 라이브러리나 도구가 특정 프레임워크의 API나 동작에 묶여 있지 않고 순수한 JavaScript로 동작하기 때문에 어떤 환경에서도 사용할 수 있습니다.
저는 대부분 React로 작업을 하기 때문에 React에서만 사용할 수 있어도 크게 불편하진 않습니다.
하지만 라이브러리를 만들다 보면 생각보다 React 외의 환경에서도 써야 하는 경우가 생기거나 다른 사람들도 사용할 수 있도록 만들고 싶어질 때가 있더군요.
예를 들어 Vue, Svelte로 마이그레이션된 프로젝트에 재활용하려면 React 전용으로 설계된 라이브러리는 한계가 있습니다.
물론 처음부터 모든 것을 범용적으로 만들 필요는 없지만 최소한 의존성을 분리하고 핵심 로직을 프레임워크와 분리해 두는 것으로도 훨씬 유연한 설계를 할 수 있다고 생각했습니다.
상태 관리 라이브러리를 만들면서 특히 중요하게 생각한 부분 중 하나가 바로 렌더링 최적화입니다.
단순히 상태를 저장하고 변경만 할 수 있다면 어렵지 않지만 React와 연결했을 때는 이야기가 조금 달라집니다.
React 컴포넌트는 상태가 변경될 때마다 다시 렌더링됩니다.
문제는 불필요한 렌더링이 자주 발생하면 성능이 크게 저하될 수 있다는 점입니다.
예를 들어 상태 객체 안에 여러 속성이 있는데 그중 하나만 바뀌었더라도 관련 없는 컴포넌트들까지 모두 렌더링된다면 낭비가 생깁니다.
그래서 다음과 같은 방식으로 최적화를 고민했습니다.
구독 기반으로 상태를 관리한다
shallow comparison(얕은 비교)로 변경 여부를 판단한다
selector 함수를 지원한다
결국 핵심은 “필요한 곳만 렌더링하도록 만드는 것”이라고 생각했습니다.
오늘 살펴본 이 3가지 고려사항을 바탕으로 앞으로 라이브러리를 만들어가는 과정을 글로 남겨보려 합니다. 혹시 정리한 부분에서 잘못된 정보가 있다면 조언 부탁드리겠습니다.
다음 글에서는 React 외부에서 store를 생성하고 관리하는 경우 이를 React에서 어떻게 사용할 수 있을지에 대해 기존 라이브러리 zustand와 jotai의 내부 동작 원리를 살펴보려고 합니다.
또한 확인한 내용을 바탕으로 제작할 라이브러리의 최종 설계, 기능은 어떻게 정해졌는지도 정리하겠습니다.