TypeScript로 Zustand Reducer를 타이핑하고 사용해 보기

Hyungeol94·2023년 11월 19일
0
post-thumbnail

인기를 얻고 있는 Zustand 라이브러리

최근의 패키지 다운로드 카운트를 시각화하여 보여주는 사이트인 npm trends에서 전역상태관리 라이브러리의 다운로드 현황을 보면, ZustandReduxReact-Redux 다음으로 인기를 얻고 있는 전역상태관리 라이브러리임을 알 수 있다. 중앙집권형의 플럭스 아키텍처라는 점에서 리덕스와 유사한 사용방식을 가지지만, 리덕스에 비해 상대적으로 러닝커브가 가파르지 않기 때문에 인기를 얻고 있는 듯하다. 나 또한 최근 프로젝트를 시작하게 되면서 처음으로 상태관리 라이브러리를 사용하게 되면서 Zustand를 선택하게 되었다. 그런데 Zustand의 공식문서는 React에 비해서는 조금 불친절한 편이었고, TypeScript를 사용하고자 한다면 조금 난감해지는 지점들이 있었기 때문에 기록해 보고자 한다.

TypeScript로 Zustand의 action, dispatch, reducer 타이핑하기

플럭스 아키텍처를 지원하는 Zustand

플럭스 아키텍처에서는 UI로직상태관리 로직을 철저하게 분리한다. 상태관리 로직이벤트 핸들러 내부에서 구현하는 대신reducer로 옮겨서 별도로 관리하고, 이벤트 핸들러에서는 사용자가 무엇을 했는지(action)를 reducer로 전달(dispatch)해주기만 하면 된다. 나머지 상태관리 로직은 reducer에서 처리한다. Zustand의 공식문서에서도 이러한 플럭스 아키텍처를 구현할 수 있음을 보여준 예시가 있다.

이 예시에서 Store에서는 grumpiness라는 전역상태와 dispatch 함수만이 선언되어 있다.(이 함수 또한 전역상태라고 할 수 있다) 이렇게 하면 Store를 사용하는 곳에서 상태관리 로직에 직접 접근할 수 없고, action을 전달하는 일(dispatch)만 하게 된다. 그렇게 전달된 action을 받아서 상태를 업데이트하는 로직은 reducer에서 내부적으로 관리될 것이다.

플럭스 아키텍처를 구현한 Zustand Store에 TypeScript 입히기

'할 일 목록'을 관리하는 store를 타이핑해 보자.

먼저, Store에서 실제로 관리되는 전역상태(TodoState)를 타이핑해 보자.

type TodoActionTypes = {
  registerTodoItem: 'REGISTER_TODO_ITEM'
  deleteTodoItem: 'DELETE_TODO_ITEM',
  editTodoItem: 'EDIT_TODO_ITEM',
}

type DispatchedAction = {type: keyof TodoActionTypes, item: TodoInfoItem}

interface TodoState {
  todoItems: TodoInfoItem[];
  dispatch: (args: DispatchedAction) => void;
}

ToDoDoState 내부의 dispatch 함수를 타이핑하기 위해서 DispatchedActionTodoActionTypes의 타이핑이 추가적으로 요구되었다. DispatchedActiondispatch에 매개변수로 전달될 action의 타입이다. DispatchedAction은 이어서 keyof TodoActionTypes으로 브랜딩된다. 만약 keyof TodoActionTypes에 해당하지 않는 actiondispatch에 전달된다면, 타입 에러가 날 것이다.

이제 Reducer를 구현해 보자.

const todoItemsReducer = (state: TodoState, action: DispatchedAction) => { // type: keyof typeof types
  switch(action.type){
    case 'registerTodoItem':
        try {
          const response = axios.post<TodoInfoItem>(
          "api url", 
          action.item,
        ).then((response) => {
          alert("할 일이 등록되었습니다.");
          return {todoItems: [...state.todoItems]}
      }).catch((error: any) => {
        console.error("Error:", error);
        return {todoItems: [...state.todoItems]}
      })
    }
      
    case 'deleteTodoItem':
      return {todoItems: [...state.todoItems]}

    default:
      return state
    } 
}

구체적인 로직을 조금 더 일반화하여 표현하였다. reducerstateaction을 매개변수로 받아서, 기존의 state와 머지될 새로운 state를 반환한다. 이때 state의 일부를 반환하여도 괜찮다. 그러니까, state extends Partial<todoState>true여야 한다는 거다. 예시에서, registerTodoItem에서 반환하는 {todoItems: [...state.todoItems]}이 그 조건을 만족할 것이다.
머지는 Zustand에서 제공하는 set을 사용하면 된다.

export const useTodoStore = create<TodoState>()((set) => ({
    todoItems : [], 
    dispatch: (action: DispatchedAction) => set((state) => (todoItemsReducer(state, action))),
}))

store에서 실제로 구현된 dispatch 함수의 형태를 보라. set을 통해서, 기존의 전역상태 state와, todoItemsReducer(state, action)가 반환하는 새로운 state를 머지하고, void를 반환한다.

이제 이렇게 구현된 Store를 import하여 사용할 수 있다.

//사용 예시
const todoStore = useTodoStore()
todoStore.dispatch({type: 'registerTodoItem', item: makeTodoItem()}) 
profile
가치를 전달하는 개발을 지향합니다.

0개의 댓글