React 기반 프레임워크를 사용하여 개발을 할 때 규모가 조금씩 커지다 보면, '상태'라는 것을 어떻게 다뤄야 할지 막막해지는 순간이 찾아옵니다.
A 컴포넌트의 버튼을 눌렀을 때, 저 멀리 떨어진 B 컴포넌트의 내용이 바뀌어야 하는 상황을 상상해 보세요.
이처럼 여러 컴포넌트가 함께 공유하고 사용하는 데이터를 '전역 상태'라고 부릅니다.
그리고 이 전역 상태를 효과적으로 관리하는 것을 '상태 관리'라고 합니다.
이번 시간에는 수많은 상태 관리 라이브러리 중에서, 간결하고 쉬운 사용법으로 많은 사랑을 받고 있는 Zustand에 대해 알아보겠습니다.
상태 관리가 왜 필요한지부터 차근차근, 누구나 이해할 수 있도록 쉽게 설명해 드리겠습니다.
리액트는 부모 컴포넌트에서 자식 컴포넌트로 데이터를 props를 통해 전달하는 하향식 데이터 흐름을 가지고 있습니다.
그런데 만약 최상위 컴포넌트의 상태를 아주 깊숙한 곳에 있는 하위 컴포넌트에서 사용해야 한다면 어떨까요?.
상태를 필요로 하지 않는 중간의 모든 컴포넌트들을 거쳐 props를 전달해야만 합니다.
이것을 바로
prop drilling
이라고 부릅니다.
Prop drilling
prop drilling은 코드를 복잡하게 만들고, 유지보수를 어렵게 만듭니다.
상태 관리 라이브러리는 이러한 문제를 해결하기 위해 등장했습니다.
컴포넌트 트리 바깥에 '전역 스토어'라는 상태 저장 공간을 만들고, 어떤 컴포넌트든 필요할 때 이 스토어에 직접 접근하여 상태를 읽거나 업데이트할 수 있게 해줍니다.
Zustand가 등장하기 전에도 여러 상태 관리 도구들이 있었습니다.
대표적인 몇 가지를 간단히 비교해 보겠습니다.
리액트에 내장된 기능으로, 별도의 라이브러리 설치 없이 사용할 수 있습니다.
하지만 Context API는 상태의 작은 일부만 변경되어도 해당 Context를 구독하는 모든 컴포넌트가 불필요하게 다시 렌더링되는 단점이 있습니다.
따라서 자주 변경되지 않는 상태(예: 테마 정보, 사용자 인증 정보)에 사용하는 것이 적합합니다.
가장 오래되고 유명한 상태 관리 라이브러리입니다.
Flux 아키텍처를 기반으로 하여 상태 변화를 예측 가능하게 만들고, 미들웨어를 통한 비동기 처리, 강력한 개발자 도구 등 생태계가 매우 발달해 있습니다.
하지만 '보일러플레이트'라고 불리는, 하나의 상태를 추가하기 위해 작성해야 하는 코드(Action, Reducer, Dispatch 등)가 너무 많다는 단점이 있습니다.
페이스북에서 만든 상태 관리 라이브러리로, Redux의 복잡함을 개선하고자 등장했습니다.
상태를 'atom'이라는 작은 단위로 쪼개어 관리하고, 이 atom들을 조합하여 새로운 파생 상태(selector)를 만들 수 있습니다.
Context API와 비슷한 사용법을 가지면서도, 상태 변화에 따른 리렌더링 최적화 문제를 해결했습니다.
Zustand는 독일어로 '상태'를 의미합니다.
이름처럼 상태 관리에만 집중한, 아주 작고 빠른 라이브러리입니다.
Zustand는 Redux와 Recoil의 장점을 합치고, 단점은 개선하려는 목표를 가지고 만들어졌습니다.
아주 간단하다
Redux처럼 복잡한 설정이나 보일러플레이트가 거의 없습니다. 몇 줄의 코드로 바로 스토어를 만들고 사용할 수 있습니다.
Provider가 필요 없다
Context API와 달리, 앱 전체를 Provider 컴포넌트로 감쌀 필요가 없습니다. 이는 코드를 더 깔끔하게 유지해 줍니다.
훅(Hook) 기반으로 동작한다
useState
를 사용하듯, 훅을 호출하여 스토어의 상태와 상태 변경 함수를 가져옵니다. 매우 직관적입니다.
리렌더링 최적화
상태의 특정 부분만 구독하여, 불필요한 리렌더링을 자동으로 방지해 줍니다.
백문이 불여일견입니다.
간단한 카운터 예제를 통해 Zustand를 어떻게 사용하는지 직접 코드로 확인해 보겠습니다.
먼저 프로젝트에 Zustand를 설치합니다.
스토어는 상태와 그 상태를 변경하는 함수들을 담고 있는 공간입니다.
src
폴더 아래에
store.ts
파일을 하나 만들어 보겠습니다.
src/store.ts import { create } from 'zustand'
const useCounterStore = create (( set ) => ({
count: 0 ,
increase : () => set (( state ) => ({ count: state.count + 1 })),
decrease : () => set (( state ) => ({ count: state.count - 1 })),
reset : () => set ({ count: 0 }),
}))
export default useCounterStore
create
함수를 zustand
에서 가져옵니다.
create
함수 안에 설정 함수를 전달합니다. 이 함수의 set
매개변수는 상태를 업데이트하는 함수입니다.
스토어 안에는 count
라는 숫자형 상태와, 이 count
를 변경하는 increase
, decrease
, reset
함수를 정의했습니다.
set
함수를 호출할 때, 현재 상태(state
)를 받아와 새로운 상태 객체를 반환하는 방식으로 상태를 안전하게 업데이트합니다.
이제 리액트 컴포넌트에서 방금 만든 스토어를 사용해 보겠습니다.
App.tsx import React from 'react'
import useCounterStore from './store'
function Counter () {
const { count , increase , decrease , reset } = useCounterStore ()
return (
< div >
< h1 >카운터</ h1 >
< p > { count } </ p >
< button onClick ={ increase } >+1</ button >
< button onClick ={ decrease } >-1</ button >
< button onClick ={ reset } >리셋</ button >
</ div >
)
}
export default Counter
방금 만든 useCounterStore
훅을 컴포넌트로 가져옵니다.
useCounterStore()
를 호출하면 스토어에 정의된 모든 상태와 함수를 객체 형태로 꺼내 쓸 수 있습니다.
마치 useState
를 사용하는 것처럼, count
값을 화면에 보여주고 각 버튼의 onClick
이벤트에 increase
, decrease
함수를 연결했습니다.
정말 간단하지 않나요?.
별도의 Provider 설정 없이,
create
로 스토어를 만들고, 컴포넌트에서 훅을 호출하는 것만으로 전역 상태 관리가 가능해졌습니다.
Zustand가 편리하다고 해서 모든 상태를 전역 스토어에 넣는 것은 좋지 않습니다.
상태는 사용 범위에 따라
전역 상태
와
지역 상태
로 구분하여 관리하는 것이 바람직합니다.
특정 컴포넌트 안에서만 사용되고, 다른 컴포넌트와 공유할 필요가 없는 상태입니다.
예: 입력 폼의 입력값, 모달 창의 열림/닫힘 여부 등.
리액트의 기본 훅인 useState
나 useReducer
로 관리하는 것이 가장 좋습니다.
여러 컴포넌트가 함께 공유하고 접근해야 하는 상태입니다.
예: 로그인한 사용자 정보, 다크 모드와 같은 테마 설정, 장바구니 목록 등.
바로 이럴 때 Zustand와 같은 상태 관리 라이브러리를 사용합니다.
“이 상태가 다른 컴포넌트에서도 필요한가?”
라는 질문을 스스로에게 던져보세요.
'아니오'라면 지역 상태로, '예'라면 전역 상태로 관리하는 것을 고려하면 됩니다.
오늘은 상태 관리가 왜 필요한지부터 시작해서, 기존의 여러 상태 관리 도구들과 Zustand를 비교해 보았습니다.
그리고 간단한 카운터 예제를 통해 Zustand의 기본적인 사용법을 익혔습니다.
Zustand는 복잡한 설정 없이도 강력한 상태 관리를 가능하게 해주는, 매우 직관적이고 효율적인 도구입니다.
이번 시간에는 Zustand의 가장 기본적인 모습만 살펴보았습니다.
다음 편에서는 Zustand의 핵심 개념인 selector
와 shallow
, 그리고 스토어를 다루는 다양한 함수들에 대해 더 깊이 파고들어 보겠습니다.