막개발글

상태관리 라이브러리 비교 (Redux Toolkit, Zustand, Recoil)

leexx 2023. 4. 2. 20:33

대상 라이브러리

  • Redux Toolkit (@reduxjs/toolkit)
  • Zustand
  • Recoil

 

비교

NPM Trends

간단 비교

  Redux Toolkit Zustand Recoil
러닝커브 Redux 를 알고 있으면 거의 없음 매우 낮음 낮음
(개인적으로는 안낮음 ^~^..)
디버깅 툴 Redux Devtools Redux Devtools 있으나 불안정함
제작자 Dan Abramov
(Redux 개발자)
카토 다이시
(Jotai 개발자)
Facebook
현재 버전 v1.9.2 v4.3.2 v0.7.6
출시 연도 2020 2019 2020
비고   Redux 에서 영감을 받아 만들었다고 함 아직 1.0 이 나오지 않았음 
자잘한 버그가 종종 있다고 함

 

소개 및 사용 예제

Redux Toolkit

  • RTK 라고도 함
  • Store 와 Action, Reducer 로 구성되어있음
  • 이전의 순수 Redux 보다 간결하게 코드 작성이 가능함

 

Store 만들기

예전에 createStore 로 만들고, middleware 를 복잡하게 감쌌던 부분이 보다 간결해졌음

configureStore() 로 store 정의하기

// store/store.js
export const store = configureStore({
    reducer: {
        todo: todoSlice
    },
    middleware: getDefaultMiddleware => getDefaultMiddleware({
        serializableCheck: false
    })
});

<Provider /> 로 감싼 후, store 추가하기

// index.js
root.render(
    <Provider store={store}>
        <RouterProvider router={router}>
            <App/>
        </RouterProvider>
    </Provider>
);

 

 

Action 및 Reducer 만들기

createSlice 라는 함수를 통해 slice 를 만들고, 만든 slice 를 통해서 action 과 reducer 를 만들 수 있음

// store/todoSlice.js
const initialState = {
    list: [{id: 0, text: "...", isDone: true}]
}

let sequence = 1;

// slice 만듦
const todoSlice = createSlice({
    name: "todo", // unique
    initialState,
    reducers: {
        addTodo(state, action) {
            // payload = text
            const todoItem = new TodoItem(sequence, action.payload);
            sequence++;
            state.list = [...state.list, todoItem];
        },
        toggleTodo(state, action) {
            // payload = id
            state.list = state.list.map((todoItem) => todoItem.id === action.payload ? {
                    ...todoItem,
                    isDone: !todoItem.isDone
                } : todoItem
            )
        }
    }
});

// actions
export const {addTodo, toggleTodo} = todoSlice.actions;

// reducer
export default todoSlice.reducer;

 

 

State 를 불러오고, Action 을 Dispatch 하기

이 부분은 Pure Redux 에서 사용했던 방법과 동일함, useDispatch 로 dispatch 를 가져온 후, action dispatch 하기

// page/TodoList/TodoList.jsx
import {useDispatch, useSelector} from "react-redux";
import {addTodo, toggleTodo} from '../../store/todoSlice';

const list = useSelector((state) => state.todo.list);
const dispatch = useDispatch();

return (
  <div>
    <h2>TodoList</h2>
    <div>
      <input type="text" value={text} onChange={updateText}/>
      <button onClick={() => dispatch(addTodo(text))}>추가</button>
    </div>
    <div>
      {
        list.map((todo) => <TodoItem key={todo.id} {...todo}/>)
      }
    </div>
  </div>
);

 

Zustand

  • Redux 에서 영감을 받아 만든 만큼, Redux 와 유사한 부분이 많이 있음
    • Store 와 Action, Reducer 로 구성되어있음
  • 아주 간결하게 코드 작성이 가능함

 

store 및 action 정의하기

Redux Devtools 도 간단하게 추가가 가능함

// zustand/store.js
import {create} from 'zustand'
import {devtools} from 'zustand/middleware'

import {TodoItem} from "../domain/TodoItem";

let sequence = 0;

// store 만들기
export const useTodoStore = create(
    devtools((set) => ({
        list: [],
        addTodo: (text) => set((state) => { // action 만들기
            const todoItem = new TodoItem(sequence, text);
            sequence++;
            return {
                list: [...state.list, todoItem]
            }
        }),
        toggleTodo: (id) => set((state) => { // action 만들기
            return {
                list: state.list.map((todoItem) =>
                    todoItem.id === id ? {...todoItem, isDone: !todoItem.isDone} : todoItem)
            }
        })
    }))
)

 

State 를 불러오고, Action 을 Dispatch 하기

// page/TodoList/TodoList.jsx
import React, {useState} from 'react';

import {useTodoStore} from '../../zustand/store';

const TodoList = () => {
    const [text, setText] = useState('');
    const list = useTodoStore((state) => state.list); // state 가져오기
    const addTodo = useTodoStore((state) => state.addTodo); // action 가져오기

    const updateText = ({target}) => {
        setText(target.value);
    }

    return (
        <div>
            <h2>TodoList</h2>
            <div>
                <input value={text} onChange={updateText} type="text"/>
                <button onClick={() => addTodo(text)}>추가</button> // action dispatch 하기
            </div>
            <div>
                {
                    list.map((todo) => <TodoItem key={todo.id} {...todo}/>)
                }
            </div>
        </div>
    );
};

 

 

Recoil

  • atom 과 selector 로 구성되어있음
  • atom 은 전역 state 임
    • selector 는 state 를 바탕으로 값을 만들어내는 것을 말함, 예를들어 state 의 길이를 구하는 함수를 만들어두고, state 의 길이를 구해야 할 때마다 그 함수를 호출해서 state 처럼 사용할 수 있음

atom 정의하기

key 는 unique 해야함

// recoil/todoList/state.js
import {atom, selector} from "recoil";

// atom 정의
export const todoState = atom({
    key: "todo", // unique
    default: []
});

 

Selector 정의하기

selector 는 state 를 바탕으로 값을 만들어내는 것을 말함
예를 들어, todoList 가 아래와 같을 때, [0, 0, 1, 1] 의 배열로 만들려고 함

[
    { id: 0, isDone: false, text: "todo1" },
    { id: 1, isDone: true, text: "todo2" },
    { id: 2, isDone: true, text: "todo3" },
    { id: 3, isDone: false, text: "todo4" }
]

이렇게 작성하면 됨

// recoil/todoList/state.js
import {atom, selector} from "recoil";

// selector 정의
export const getDoneTodoListState = selector({
    key: "todo/done",
    get: ({get}) => {
        const todoList = get(todoState);
        return todoList.reduce((acc, curr) => {
            const isDone = curr.isDone ? 1 : 0;
            return [...acc, isDone];
        }, []).sort((a, b) => a-b);
    }
})

 

Selector 에서 만들어둔 함수 사용해서 값 불러오기

import {getDoneTodoListState} from "../../recoil/todoList/state";

const Statistics = () => {
    const doneTodoList = useRecoilValue(getDoneTodoListState); // 가져오기

    return (
        <div>
            <h2>Statistics</h2>
            <div className="graph">
                {
                    doneTodoList.map((isDone) =>
                        <div className={clsx("block", {
                            done: isDone,
                        })}/>)
                }
            </div>
        </div>

    );
};

 

 

atom update 하기

TodoItem 추가하기

// page/TodoList/TodoList.jsx
import {useRecoilState} from "recoil";
import {todoState} from "../../recoil/todoList/state";
import {TodoItem} from "../../domain/TodoItem";

const [list, setList] = useRecoilState(todoState); // atom 가져오기

const addTodo = () => {
    setList((prevList) => [...prevList, new TodoItem(sequence, text)]); // atom update 하기
    setSequence(sequence + 1);
}

 

 

사용기

  • Redux Toolkit
    • Redux 를 써본 적이 있어서 그런지 크게 어렵지 않았음
    • Zustand, Recoil 에 비해 코드가 복잡하다고 하지만 그다지 복잡한 것 같지 않음
  • Zustand
    • Redux 와 개념이 비슷한데다가 간단해서 매우 쉽고 간단했음
    • Redux DevTools 를 같이 사용할 수 있어서 편했음
    • 다만 너무 쉽고 간단해서 큰 프로젝트에 적용해도 괜찮을지 잘 모르겠음
  • Recoil
    • 개념은 간단하지만, 용어와 더불어 낯설어서 (atom, selector) 받아들이는데 러닝커브가 있었음
    • 개념을 너무 간단하게 가져가서 그런지, 오히려 개발자는 해야 할 일이 많다고 느낌
      • Redux Toolkit 이나 Zustand 의 경우, action 에서 todoList 에 todoItem 을 추가하는 할 때에, Redux Toolkit 이나 Zustand 의 경우, action 에서 로직 (text 를 input 으로 받으면 TodoList 를 추가해줌) 을 구현해두고 호출이 가능함
      • 하지만 Recoil 은 state 가 너무 간단하다보니, 로직을 component 에서 작성해야 했음
// recoil
setList((prevList) => [...prevList, new TodoItem(sequence, text)]);

// zustand
addTodo(text)

// RTK
dispatch(addTodo(text))

 

 

다른 개발자들은 큰 프로젝트에 주로 무엇을 사용할까?

  • Jotai
  • RTK