스토어에서 속성이나 액션을 꺼내 쓸 때는 선택자(selectors)를 사용하는 것을 강력히 추천해요. 보통은 아래처럼 스토어의 값에 접근하실 거예요.
const bears = useBearStore((state) => state.bears)
하지만 매번 상태를 가져올 때마다 이렇게 화살표 함수를 작성하는 건 꽤 번거롭고 지루한 작업이 될 수 있어요. 만약 이 과정이 귀찮게 느껴지신다면, 선택자를 자동으로 생성해서 쓰는 아주 멋진 방법이 있답니다!
💡 강사의 실무 팁:
리액트(React.js)나 넥스트JS(Next.js)로 프로젝트를 진행하다 보면 상태 관리해야 할 값들이 정말 많아지죠. 이때 선택자를 자동 생성하는 유틸리티를 프로젝트 구조에 미리 세팅해 두면 코드 타이핑 양도 확 줄어들고 코드 베이스가 훨씬 깔끔해집니다. 나중에 개인 포트폴리오를 만들 때 이런 디테일한 개발자 경험(DX) 개선 코드를 녹여내면, 단순히 라이브러리를 쓰는 것을 넘어 유지보수성까지 깊게 고민하는 개발자로 보여서 면접관들에게 아주 좋은 인상을 줄 수 있어요!
createSelectorsimport { StoreApi, UseBoundStore } from 'zustand'
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
const createSelectors = <S extends UseBoundStore<StoreApi<object>>>(
_store: S,
) => {
const store = _store as WithSelectors<typeof _store>
store.use = {}
for (const k of Object.keys(store.getState())) {
;(store.use as any)[k] = () => store((s) => s[k as keyof typeof s])
}
return store
}
만약 여러분이 작성한 스토어가 아래와 같이 생겼다고 가정해 볼게요.
interface BearState {
bears: number
increase: (by: number) => void
increment: () => void
}
const useBearStoreBase = create<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
increment: () => set((state) => ({ bears: state.bears + 1 })),
}))
이제 아까 만든 createSelectors 함수를 방금 만든 스토어에 쓱 씌워주기만 하면 됩니다.
const useBearStore = createSelectors(useBearStoreBase)
짠! 이제 선택자들이 모두 자동으로 만들어졌어요. 굳이 콜백 함수를 쓰지 않아도 .use.를 통해 상태나 액션에 바로 접근할 수 있습니다.
// 속성(Property) 가져오기
const bears = useBearStore.use.bears()
// 액션(Action) 가져오기
const increment = useBearStore.use.increment()
💡 강사의 추가 팁:
이렇게 설정해 두면 타입스크립트의 자동완성(IntelliSense) 기능이 완벽하게 작동해요. IDE에서useBearStore.use.까지만 쳐도 내부에 있는 상태들이 목록으로 쫙 뜨니까, 오타도 줄일 수 있고 개발 속도가 훨씬 빨라질 겁니다!
리액트 컴포넌트 외부나 순수 자바스크립트 환경에서 바닐라 스토어를 사용하고 계신다면, 아래의 createSelectors 함수 버전을 사용해 주세요.
import { StoreApi, useStore } from 'zustand'
type WithSelectors<S> = S extends { getState: () => infer T }
? S & { use: { [K in keyof T]: () => T[K] } }
: never
const createSelectors = <S extends StoreApi<object>>(_store: S) => {
const store = _store as WithSelectors<typeof _store>
store.use = {}
for (const k of Object.keys(store.getState())) {
;(store.use as any)[k] = () =>
useStore(_store, (s) => s[k as keyof typeof s])
}
return store
}
사용하는 방법은 리액트 스토어 방식과 완전히 똑같아요. 여러분의 바닐라 스토어가 아래와 같다면요:
import { createStore } from 'zustand'
interface BearState {
bears: number
increase: (by: number) => void
increment: () => void
}
const store = createStore<BearState>()((set) => ({
bears: 0,
increase: (by) => set((state) => ({ bears: state.bears + by })),
increment: () => set((state) => ({ bears: state.bears + 1 })),
}))
아까처럼 함수를 스토어에 씌워 줍니다.
const useBearStore = createSelectors(store)
이제 똑같이 자동 생성된 선택자를 이용해 바로바로 접근할 수 있죠!
// 속성(Property) 가져오기
const bears = useBearStore.use.bears()
// 액션(Action) 가져오기
const increment = useBearStore.use.increment()
직접 코드를 만져보면서 눈으로 확인해 보는 게 이해하기 가장 좋겠죠? 실제로 어떻게 동작하는지 이 Code Sandbox 링크에서 테스트해 볼 수 있어요!
이런 유틸리티 함수를 프로젝트에 직접 작성하기 번거롭거나, 좀 더 확장된 기능이 필요하다면 이미 훌륭하게 만들어진 서드파티 라이브러리들을 활용해 보는 것도 실무에서 아주 흔히 쓰는 방식이에요. 아래 링크들을 참고해 보세요!
이전 글: Advanced TypeScript Guide
다음 글: Setup with Next.js