[ React ] 전역 상태 관리 라이브러리 비교

꾸개·2024년 6월 25일
0

React

목록 보기
7/9
post-thumbnail
post-custom-banner

몸으로 경험하기

취준생으로써 작은 단위에 프로젝트만 진행하다 보니 전역 상태를 사용할 일이 많지 않았다. 웬만한거는 모두 props나 애초에 전역 상태를 사용할 정도로 컴포넌트 단위가 크지 않았기 때문이다.
만약 사용할 일이 있다면 프로젝트 진행하면서 학습하면 되지 않을까? 라는 안일한 생각을 갖고 있다가 정작 프로젝트 때에는 시간에 쫓겨 '가장 쉬운', '가장 간단한' 라이브러리들을 사용했다 그래서 사용한 것이 Recoil이었다.
전역 상태 관리 라이브러리의 시초격인 Redux는 딱봐도 작성해야 할 코드도 많았고, 코드는 짧고 간단하면 좋은 것이라는 또 다른 안일한 생각으로 지내왔다.

시간이 흘러, 본격적인 취준 생활때에 이러한 생각들이 발목을 잡았다. 회사 스택들은 대부분 Redux를 사용했고, Recoil은 거의 사용하지 않았다. 또한, '왜 Recoil을 사용했나요?'라는 면접 질문을 받으면 '그냥... 학습으로요...?' 정도로만 답하여 광탈했던 경험이 있다.

이러한 실패 경험들 때문에 Redux에 대한 학습 열망이 커졌고, 더불어 '여러가지 라이브러리들 중에 어떤것이 가장 좋은가?' 라는 궁금증도 항상 있었다. 그리하여, 총 6개의 전역 상태관리 라이브러리들을 직접 사용해보고 props로 상태를 상속할때와 contextAPI와는 어떻게 다른지도 직접 비교해보면서 경험하는 프로젝트를 만들었다.


전역 상태 관리 사용 시나리오

전역 상태를 사용하면 오히려 상태 추적이 어려워지므로 몇 가지 사용할 수 있는 상황에만 사용하는 것이 활용적이다. 이것에 정답은 없지만, 리액트 훅을 활용한 마이크로 상태관리 책을 읽고, 여러 아티클의 의견을 찾아보고 종합해본 결과 나만의 조건을 찾을 수 있었다.

  1. 부모 컴포넌트가 다른 자식 컴포넌트들의 상태 공유가 필요할 때

  2. 상태 데이터가 리액트 환경 외에 있을 때

  3. 뎁스가 너무 깊어졌다고 생각이 될 때

  4. 도저히 전역 상태를 사용하지 않고서는 사용할 수 없을 때

라고 나만의 시나리오를 추려보았다. 이 중 간단하게 프로젝트에 적용할 방법이 있을까? 라고 생각해 봤을 때 다크 모드를 구현할 때 가장 전역 상태 관리를 하기에 적합한 환경이라는 생각이 들었다.

다크 모드는 같은 부모 컴포넌트에게 상태를 상속 받아서 구현할 수 있지만, 뎁스가 너무 깊어짐에 따라 props를 일일히 받아줘야 된다고 생각하니 너무 귀찮고, 라우터에서 상태를 넘길 때 불편하기에 이럴 때는 간단하게 모든 컴포넌트가 하나의 상태를 바라보고 그에 따른 스타일 변화를 주면 좋다고 생각이 들었다. 이는 나만의 시나리오 1번에 적합하다고 생각했다.

또한, 이용자가 브라우저를 닫아도 다크모드가 유지되게끔 하기 위해 로컬스토리지를 사용했고, 이는 나만의 시나리오 2번에도 적합하기에 1번과 2번을 결합한 프로젝트가 되었다.


주의

  • 각 라이브러리를 간단하게 경험하기 위한 학습 목적이기에 실제 실무 구현과는 다를 수 있음
  • 한 프로젝트 안에 구현되었기 때문에 라이브러리들끼리 상태 호환이 안되어 불가피하게 useEffect를 사용해 마운트 시에 전역상태를 초기화 할 필요가 있었음

전역 상태관리 라이브러리 선정

이번 프로젝트에서 사용해 본 라이브러리들은 총 6개로 Redux-toolkit, Zustand, Recoil, Jotai, MobX, Valtio를 사용했다. 모두 리액트 훅을 활용한 마이크로 상태관리 책에서 비교한 개념으로 직접 경험해보기 위해 선정했고 리액트에서 제공하는 Context APIProps로 상속하는 구조도 구현해보면서 차이를 느껴봤다.

  • 각 버튼을 누르면 각 상태 페이지로 이동하고 모두 상태를 공유하고 있다.

Props

상태 상속 페이지를 먼저 마운트하고 상속 받을 컴포넌트를 반환하는 구조를 갖고 있음

// PropsStatePage.tsx
import PropsPage from "./PropsPage"
import { useState } from "react";
import { ThemeType } from "../../types/types";

const PropsStatePage = () => {
    const [theme, setTheme] = useState<ThemeType>(() => {
        const themeStorage = localStorage.getItem("theme");
        return themeStorage === "dark" ? "dark" : "light";
      });
    
    return <PropsPage theme={theme} setTheme={setTheme}/>
}

export default PropsStatePage
  • 먼저 useState로 상태를 관리하는데, 로컬 스토리지의 최신 데이터를 상태로 가져오기 위해 콜백으로 마운트 마다 스토리지 값을 가져오고 PropsPage에 상태 상속

import { ThemeStateType } from "../../types/types"
import styles from "../../public/App.module.css"
import { Link } from "react-router-dom"

const PropsPage:React.FC<ThemeStateType> = (props) => {
    const { theme, setTheme } = props

    const toggleTheme = () => {
      const newTheme = theme === "light" ? "dark" : "light"
      setTheme(newTheme)
      localStorage.setItem("theme", newTheme)
    }


    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Props</h1>
          <button onClick={toggleTheme}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}

export default PropsPage
  • 상태를 PropsPage에서 변경하고 이를 부모 컴포넌트로 전달하여 상태를 관리하는 패턴을 "상태 끌어올리기" (lifting state up)를 사용한다.
  • 이 패턴에서는 상태를 부모 컴포넌트에서 관리하고, 자식 컴포넌트는 상태를 변경할 수 있는 함수를 props로 받아서 사용한다.

장점

  • 라이브러리를 사용할 필요 없이 간단하게 상태를 상속해 구현할 수 있다.
  • 상태 추적이 비교적 쉽다.

단점

  • 뎁스가 깊어지면 prop을 일일히 작성해야 하는 것이 번거롭다.
  • 상태를 끌어올려야 하는 번거로움이 있다.
  • 상위 컴포넌트가 재렌더링 되면 하위 컴포넌트들도 재렌더링이 되어 성능 이슈 우려가 있다.

Context API

Props로 상태를 전달하는 것을 Context API로 유사하게 전달할 수 있다. Context API로는 Provider로 지정한 컴포넌트의 하위 컴포넌트들은 모두 해당 Provider가 공유하는 상태를 공유받을 수 있다.

//ContextAPIState.tsx
import { ReactNode, useState } from "react";
import { ThemeType } from "../../types/types";
import { themeContext } from "./themeContext";


const ContextAPIState = ({children}: {children: ReactNode}) => {
    const [theme, setTheme] = useState<ThemeType>(() => {
        const themeStorage = localStorage.getItem("theme");
        return themeStorage === "dark" ? "dark" : "light";
    });

    const toggleTheme = (): void => {
        const newTheme = theme === "dark" ? "light" : "dark";
        setTheme(newTheme);
        localStorage.setItem('theme', newTheme)
    };

    return (
        <themeContext.Provider value={{ theme, toggleTheme }}>
            {children}
        </themeContext.Provider>
    );
};

export default ContextAPIState;
//themeContext.ts
import { createContext } from "react";
import { ContextAPIType } from "../../types/types";

export const themeContext = createContext<ContextAPIType>({
    theme: "light",
    toggleTheme: () => {}
});
  • themeContext로 createContext를 사용해 초기 상태를 만든다.
  • themeContext.Provider로 value에 공유할 상태를 지정한다.
  • 상태를 공유할 컴포넌트를 themeContext.Provider로 감싸준다.
  • 감싸진 컴포넌트는 가장 가까운 Provider의 value를 상태로 받는다.

//ContextAPI.tsx
import styles from "../../public/App.module.css";
import { Link } from "react-router-dom";
import { useContext } from "react";
import { themeContext } from "./themeContext";

const ContextAPI = () => {
    const { theme, toggleTheme } = useContext(themeContext);

    return (
        <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
            <header className={styles.header}>
                <h1>This page is for Context API</h1>
                <button onClick={toggleTheme}>
                    Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
                </button>
            </header>
            <Link to="/">
                <button>
                    Back to Main
                </button>
            </Link>
        </div>
    );
};

export default ContextAPI;
  • 공유받은 상태를 사용하려면 useContext() 훅을 사용한다.

장점

  • 라이브러리 없이 사용할 수 있다.
  • 뎁스가 깊어져도 props보다는 간단하게 사용할 수 있다.

단점

  • props와 마찬가지로 상위 컴포넌트가 재렌더링되면 Context를 사용하는 하위 컴포넌트도 재렌더링될 우려가 있다.
  • 가까운 Provider의 값을 공유받기에, 원하는 값을 받지 못하여 디버깅이 어려워질 수 있다.
  • Context API는 주로 값을 공유하는 목적으로 설계되었기 때문에, 전역 상태 관리 도구의 역할을 완전히 대체하기에는 한계가 있을 수 있다.

Redux-toolkit

Redux를 사용할 수 있지만, 작성해야 할 보일러 플레이트 코드가 너무 많고, 한 번 경험해 보기 위해서는 과하다고 생각이 들어 많은 코드 양을 줄인 Redux-toolkit을 사용했다.

기본적으로 Redux를 사용하기 위해서는 몇 가지 개념을 알고 가야한다.

store

라이브러리들은 스토어라는 개념을 사용하는데 상태를 저장하는 곳이라고 생각하면 된다. 스토어에 상태를 저장하고 업데이트 하면서 상태를 하위 컴포넌트에서 받을 수 있게끔 설계 되어 있다.

Action

store의 상태를 업데이트 할 수 있는 유일한 수단이다. type으로 상태에 속성을 지정할 수 있다.

Reducer

리듀서는 상태와 액션을 입력으로 받아서 새로운 상태를 반환하는 순수 함수이다. 리듀서는 상태 변경 로직을 정의하며, 상태는 불변성을 유지해야 하므로 기존 상태를 복사하여 새로운 상태를 반환한다. RTK(Redux-toolkit)에서는 createSlice()로 reducer와 action을 한꺼번에 사용할 수 있다.

Selector

Selector는 스토어에서 특정 상태를 추출하는 함수이다. Selector를 사용하면 컴포넌트에서 필요한 상태를 쉽게 가져올 수 있다.

구현

먼저 slice와 store를 구현한다.

//themeSlice.ts
import { createSlice } from '@reduxjs/toolkit';

const initialTheme = "light"

const initialState = {// 초기상태
  theme: initialTheme
};


export const themeSlice = createSlice({
  name: 'theme', // 슬라이스의 이름
  initialState, // 초기 상태
  reducers: { // 리듀서 함수 
    reduxToggleTheme: state => {
      const newTheme = state.theme === 'light' ? 'dark' : 'light';
      localStorage.setItem('theme', newTheme);
      state.theme = newTheme;
    },
    initState: state => {
      state.theme = localStorage.getItem("theme") || "light"
    }
  }
});

export const { reduxToggleTheme, initState } = themeSlice.actions;

export default themeSlice.reducer;
  • 리듀서와 액션을 지정해주어 액션이 실행되면 리듀서 로직이 실행된다.

// store.ts
import { configureStore } from '@reduxjs/toolkit';
import themeSlice from './themeSlice';

const initialTheme = "light"

const store = configureStore({
  reducer: {
    theme: themeSlice
  }
});

store.subscribe(() => {
  console.log('changed!')
});

export default store;
  • 스토어에서 변경되면 해당 상태를 업데이트 해준다. 변경되면 콘솔에 changed! 문자가 출력된다.

// ReduxToolkitStatePage.tsx
import ReduxToolkitPage from "./ReduxToolkitPage";
import { Provider } from "react-redux";
import store from "../../store/reduxStore/store";

const ReduxTookitStatePage = () => {
    return (
    <Provider store={store}>
        <ReduxToolkitPage/>
    </Provider>
    )
}

export default ReduxTookitStatePage
  • provider라는 전역 상태 관리 컴포넌트를 생성하고 전역 상태를 사용할 컴포넌트를 자식으로 컴포넌트로 넣어준다.

// ReduxToolkitPage.tsx
import styles from "../../public/App.module.css"
import { Link } from "react-router-dom"
import { initState, reduxToggleTheme } from "../../store/reduxStore/themeSlice"
import { useDispatch, useSelector } from "react-redux"
import { useEffect } from "react"
import { rootStateType } from "../../types/rootStateType"

const ReduxToolkitPage = () => {
  const { theme } = useSelector((state: rootStateType) => state.theme)
  const dispatch = useDispatch()  

  useEffect(() => {
    dispatch(initState())
  },[])

    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Redux Toolkit</h1>
          <button onClick={() => dispatch(reduxToggleTheme())}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}

export default ReduxToolkitPage
  • useSelector로 스토어에 전역 상태를 받아온다.
  • useDispatch로 액션을 트리거해준다.
  • 버튼을 클릭할 때 reduxToggleTheme 리듀서를 실행한다.

장점

  • 액션을 트리거 했을 떄 어떤 로직을 실행시키며 어떻게 상태가 저장될지 세세하게 작성할 수 있다.
  • store라는 저장소를 바라보기에 상태 추적이 쉽다.

단점

  • 알아야 할 개념이 많다.
  • 작성해야 할 코드가 많다.
  • 러닝커브가 높다.

Zustand

Redux의 reducer와store개념을 모티브로 삼아 더욱 간단하게 나온 전역 상태 관리 라이브러리이다. 독일어로 Zustand는 곰이라는 뜻으로 귀여운 곰이 마스코트이다.

Redux를 모티브로 삼았기에 store와 reducer을 구현해야 한다. 하지만, Redux와 비교했을 때 굉장히 간단하다.


//store.ts
import { create } from "zustand";

interface types{
    theme: "light" | "dark"
    toggleTheme: () => void
    initState: () => void
}

export const useStore = create<types>((set) => ({
    theme: "light",
    toggleTheme: () => {
        set((state) => {
            const newTheme = state.theme === "light" ? "dark" : "light";
            localStorage.setItem("theme", newTheme);
            return { theme: newTheme };
        });
    },
    initState: () => {
        const storedTheme = localStorage.getItem("theme");
        if (storedTheme !== null) {
            set({ theme: storedTheme as "light" | "dark"});
        }
    },
}));
  • create()set을 사용해 store를 생성한다. 특이한 점은 storereducer가 함께 구현된다.
  • 초기 상태로 localStorage값을 가져오게 구현했다.

//ZustandStatePage.tsx
import ZustandPage from "./ZustandPage"

const ZustandStatePage = () => {
    return <ZustandPage/>
}

export default ZustandStatePage
  • Zustand는 provider가 필요없다. 따라서 따로 하위 컴포넌트를 감싸 줄 필요가 없다. 학습을 하기 위해 일부러 뎁스를 하나 늘렸다.

//Zustand.tsx
import { useEffect } from 'react';
import styles from '../../public/App.module.css'
import { useStore } from '../../store/zustandStore/store'
import { Link } from 'react-router-dom';

const ZustandPage = () => {
    const { theme, toggleTheme, initState } = useStore(state => ({ theme: state.theme, toggleTheme: state.toggleTheme, initState: state.initState }));

    useEffect(() => {
        initState()
    },[])


    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Zustand</h1>
          <button onClick={toggleTheme}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}

export default ZustandPage
  • store에서 reducerstate를 모두 꺼내서 바로 사용할 수 있다.
  • dispatchaction을 트리거 할 필요없다. reducer로 바로 값을 업데이트 할 수 있다.

장점

  • Redux를 모티브로 하여 Redux 유저들에게는 더욱 간단한 코드를 제공하고 입문자들에게는 낮은 러닝커브를 제공한다.
  • Redux를 모티브로 하여 reducer로 세세한 코드를 작성할 수 있다.
  • provider를 따로 만들 필요가 없어 더 간단한 코드를 작성할 수 있다.

단점

  • Redux를 학습하지 않은 초심자들은 어려울 수 있다.
  • store와 reducer를 작성할 때 간소하지만, 복잡해진다면 코드가 길어질 수 있다.

Recoil

Recoil은 페이스북이 2020년 5월에 소개한 React 전용으로 나온 상태 관리 라이브러리다. Redux보다 훨씬 간편하여 당시 많은 사람들이 관심을 보였다.

Recoil은 React전용인 만큼 React와 아주 유사한 코드 구조를 가지고 있다.

// store.ts
import { atom } from "recoil";

interface StatusType {
    theme: "light" | "dark";
}

export const themeState = atom<StatusType>({
    key: 'theme',
    default: {
        theme: "light"
    }
});
  • atom이라는 키워드로 상태를 지정한다.
  • key로 상태의 이름을 지정한다.
  • default로 초기 상태를 지정한다.

//RecoilStatePage.tsx
import { RecoilRoot } from 'recoil'
import RecoilPage from './RecoilPage'

const RecoilStatePage = () => {
    return (
        <RecoilRoot>
            <RecoilPage/>
        </RecoilRoot>
    )
}

export default RecoilStatePage
  • RecoilRoot라는 provider를 제공한다. 전역상태를 사용할 컴포넌트들을 감싸준다.

//RecoilPage.tsx
import { useEffect } from 'react';
import styles from '../../public/App.module.css'
import { Link } from 'react-router-dom';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { themeState } from '../../store/recoilStore/store';


const RecoilPage = () => {
    const setTheme = useSetRecoilState(themeState);
    const recoilValue = useRecoilValue(themeState)
    const { theme } = recoilValue

    const toggleTheme = (prevTheme: "light" | "dark") => {
      if(prevTheme === "light"){
        setTheme({theme: "dark"})
        localStorage.setItem("theme", "dark")
      }else{
        setTheme({theme: "light"})
        localStorage.setItem("theme", "light")
      }
    }


    useEffect(() => {
        const newTheme = localStorage.getItem("theme") === "dark" ? "dark" : "light"
        if(newTheme === null){
            return
        }
        setTheme({theme: newTheme})
    },[])

    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Recoil</h1>
          <button onClick={() => toggleTheme(theme)}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}

export default RecoilPage
  • useRecoilState, useRecoilValue와 같이 리액트의 훅과 같은 작명으로 친근함이 든다.
  • useRecoilState로는 상태를 업데이트한다.
  • useRecoilValue로는 상태를 가져온다.

장점

  • React에 익숙한 유저들에게 러닝커브가 매우 낮다.
  • Redux와 관련된 라이브러리보다 보일러 플레이트 코드가 적다.

단점

  • 상태를 만들때마다 작성해야 하는 코드들이 번거롭다. ex) key

Jotai


Recoil을 모티브로 만든 전역 상태 관리 라이브러리로 Recoil의 Atom개념을 그대로 가지고 왔고, Recoil보다 더욱 간편한 환경을 제공한다.

//store.ts
import { atom } from "jotai"

export const themeAtom = atom<"light" | "dark">("light")
  • Recoil과 비교했을 때 key가 빠지고 초기 상태를 직접 전달한다.

//JotaiStatePage.tsx
import JotaiPage from "./JotaiPage"

const JotaiStatePage = () => {
    return <JotaiPage/>
}

export default JotaiStatePage
  • RecoilRoot 혹은 Provider도 필요 없다.

import { useEffect } from 'react';
import styles from '../../public/App.module.css'
import { Link } from 'react-router-dom';
import { useAtom } from 'jotai';
import { themeAtom } from '../../store/jotaiStore/store';

const JotaiPage = () => {
    const [theme, setTheme] = useAtom(themeAtom)


    const toggleTheme = (prevTheme: "light" | "dark") => {
      if(prevTheme === "light"){
        setTheme("dark")
        localStorage.setItem("theme", "dark")
      }else{
        setTheme("light")
        localStorage.setItem("theme", "light")
      }
    }


    useEffect(() => {
        const newTheme = localStorage.getItem("theme") === "dark" ? "dark" : "light"
        if(newTheme === null){
            return
        }
        setTheme(newTheme)
    },[setTheme])


    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Jotai</h1>
          <button onClick={() => toggleTheme(theme)}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}
export default JotaiPage
  • 지역상태 관리 훅 useState()과 매우 유사하게 useAtom으로 전역 상태를 읽고 관리할 수 있다.

장점

  • 사용해본 라이브러리 중 가장 간소화되어 있고 리액트와 가장 유사했다.
  • 보일러플레이트 코드가 가장 적었다.

단점

  • 타 라이브러리들과 비교했을 때 커뮤니티 활성도가 적다.
  • 아마 너무나 간소해서 커뮤니티를 활성화할 필요성을 못느끼지 않았을까..?

MobX


전역 상태관리 라이브러리를 깊게 사용해보지 나에게는 조금 생소한 라이브러리였다. 하지만, 재미있는 사실이 있었는데 타 라이브러리들과 다른 방법으로 전역상태를 관리한다 바로 Proxy 객체를 사용한다.

Proxy 객체는 객체의 기본 동작을 가로채서 사용자가 원하는 방식으로 처리할 수 있다. MobX는 이를 활용하여 상태 객체를 관찰하고, 상태 변경을 추적하여 필요한 곳에 자동으로 업데이트를 반영한다.

MobX에서는 다른 라이브러리들과 다른 3가지 개념이 있다.

Action

Redux에서는 Reducer를 실행하는 트리거 역할을 했지만, MobX는 Action자체가 상태를 업데이트 한다.

Observable 상태

Observable 상태는 상태 변화를 자동으로 감지하고 관리할 수 있도록 돕는 객체이다. MobX는 observable 객체 내의 속성이 변경되는 것을 감지할 수 있지만, 중첩된 객체나 배열의 내부까지 깊게 관찰하지는 않는다. 따라서 객체의 속성이 변경될 때는 별도의 처리가 필요하다.

Derivation

Derivation은 '유도', '파생'이라는 단어로 MobX에서는 2가지로 나뉜다.

1. computed

Computed는 MobX에서 파생된 값(computed value)이다. 이는 다른 observable 상태의 변화에 의해 자동으로 업데이트되는 값이다.
MobX는 이러한 Computed 값을 자동으로 관리하여, 값이 의존하는 상태가 변할 때마다 적절하게 업데이트한다.

2. Reaction

Reaction은 특정 상태의 변화에 반응하여 부작용(side effect)을 실행하는 것을 말한다. MobX의 reaction은 주로 외부 상태 변화에 대응하여 특정 동작을 수행하거나, UI를 업데이트하는 등의 작업을 할 때 사용된다. Reaction은 특정 observable 상태를 관찰하고, 그 상태가 변경될 때마다 실행할 콜백 함수를 등록한다.

//store.ts
import { makeAutoObservable } from 'mobx';

const createThemeStore = () => {
    const theme = localStorage.getItem('theme') || 'light';

    const store = {
        theme,
        setTheme(newTheme: "light" | "dark") {
            store.theme = newTheme;
            localStorage.setItem('theme', newTheme);
        },
        toggleTheme() {
            store.setTheme(store.theme === 'light' ? 'dark' : 'light');
        }
    };

    makeAutoObservable(store);
    return store;
};

const themeStore = createThemeStore();
export default themeStore;
  • makeAutoObservable 모듈명에서 알 수 있듯이 먼저 store를 선언하고 observable 상태로 만들어준다.

//MobXPageState.tsx
import MobXPage from "./MobXPage"

const MobXStatePage = () => {
    return <MobXPage/>
}

export default MobXStatePage
  • MobX는 Provider가 필요없다.

//MobXPage.tsx
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import themeStore from '../../store/mobXStore/store';
import styles from '../../public/App.module.css'
import { observer } from 'mobx-react';

const MobXPage = observer(() => {
    const { theme, toggleTheme, setTheme } = themeStore


    useEffect(() => {
        const newTheme = localStorage.getItem("theme") === "dark" ? "dark" : "light"
        if(newTheme === null){
            return
        }
        setTheme(newTheme)
    },[theme])


    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for MobX</h1>
          <button onClick={toggleTheme}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
})

export default MobXPage
  • observer로 감지할 컴포넌트를 지정한다.
  • 지정한 Action과 State를 store에서 받아온다.

장점

  • Action이 직접 상태를 변경하여 코드를 간결하게 유지할 수 있다.
  • Observable 상태와 Computed를 통해 Reactive한 상태 관리를 할 수 있다.

단점

  • Observable 객체 내의 중첩된 객체나 배열의 내부 변경은 MobX에서 자동으로 감지하지 않으므로, 별도의 처리가 필요하다.
  • MobX의 개념을 이해하고 사용하기 위해 러닝커브가 있다.
  • 상대적으로 타 라이브러리들보다 커뮤니티가 활성화 되어있지 않다.

Valtio

Valtio는 MobX를 모티브로 하지 않았지만, 유사한 구조를 가지고 있다. Derivation 개념과 Observable 상태 개념을 사용하고 있다.

//store.ts
import { proxy } from 'valtio';

const themeStore = proxy({
    theme: localStorage.getItem('theme') || 'light',
    setTheme(newTheme: "light" | "dark") {
        themeStore.theme = newTheme;
        localStorage.setItem('theme', newTheme);
    },
    toggleTheme() {
        themeStore.setTheme(themeStore.theme === 'light' ? 'dark' : 'light');
    }
});

export default themeStore;
  • MobX와 달리 store객체를 따로 생성하지 않고 바로 proxy로 넘겨준다.

//VatioStatePage.tsx
import ValtioPage from "./ValtioPage"

const ValtioStatePage = () => {
    return <ValtioPage/>
}

export default ValtioStatePage
  • 마찬가지로 Valtio도 Provider를 사용하지 않는다.

//ValtioPage.tsx
import { useEffect } from 'react';
import { Link } from 'react-router-dom';
import themeStore from '../../store/valtioStore/store';
import { useSnapshot } from 'valtio';
import styles from '../../public/App.module.css'

const ValtioPage = () => {
    const { theme, toggleTheme, setTheme } = useSnapshot(themeStore)


    useEffect(() => {
        const newTheme = localStorage.getItem("theme") === "dark" ? "dark" : "light"
        if(newTheme === null){
            return
        }
        setTheme(newTheme)
    },[theme])


    return (
    <div className={`${theme === "light" ? styles.light : styles.dark} ${styles.container}`}>
        <header className={styles.header}>
          <h1>This page is for Valtio</h1>
          <button onClick={toggleTheme}>
            Switch to {theme === "light" ? 'Dark' : 'Light'} Mode
          </button>
        </header>
          <Link to="/">
            <button>
                Back to Main
            </button>
          </Link>
      </div>
    )
}

export default ValtioPage
  • useSnapShot()이라는 함수를 사용하는데 이를 통해 store에 접근해서 Action과 State을 받는다.
  • MobX와 달리 observer를 따로 지정할 필요가 없다.

장점

  • MobX와 비슷한 기능을 더 직관적으로 제공하여 상태 관리를 간편하게 할 수 있다.
  • 상태 변화를 자동으로 감지하고 업데이트할 수 있어 개발 생산성을 높일 수 있다.

단점

  • 복잡한 상태 관리가 필요한 경우에는 다른 라이브러리보다는 부족할 수 있다.
  • 역시나 커뮤니티가 타 라이브러리들보다 활성화 되어있지 않다.

어떤 라이브러리를 써야할까?

자! 나름 열심히 끙끙대며 모든 라이브러리들을 써보는 특별한 경험을 했다. 그렇다면 어떤 라이브러리를 써야 할 지 정답을 알 수 있었을까?

리액트를 훅을 활용한 마이크로 상태관리 책에서는 전역 상태관리를 마이크로하게 관리하라 하고 이렇게 설명한다.

마이크로 상태관리란?

  • 컴포넌트 단위에서 로컬 상태를 관리하고, 필요에 따라 적절하게 전역 상태와 결합하는 방식이다.
  • 이는 애플리케이션의 상태 관리 복잡성을 줄이고, 상태 관리를 보다 직관적이고 간단하게 하기 위한 접근법이다.
  • 이를 위해 경량화 된 라이브러리들을 사용한다.

정답은 경량화 된 라이브러리를 사용해야하는 것이다. 애초에 전역 상태 관리 자체가 그렇게 비용을 소모할 필요가 없다고 생각한다. 그러므로 최대한 경량화 된 라이브러리를 사용하는 것을 권장한다.

여기까지 글을 읽은 독자들도 나와 생각이 같다면 Jotai가 이러한 조건에 가장 적합하다고 생각이 든다. 필자가 속한 오픈 카톡방에서 실무 개발자들의 후기들을 살펴볼 수 있었다.

카톡 내용에서도 보다시피 Jotai가 편한것은 사실이지만 모든 프로젝트를 Jotai로 지정할 필요도 없고 때에 맞게 사용하면 된다.

굳이 잘 굴러가는 프로젝트에 Jotai를 마이그레이션 하여 비용을 소모할 필요도 없고, 팀원들을 설득할 필요도 없다. 더군다나 나같은 신입은 회사가 Redux를 사용하면 그냥 Redux를 사용하면 된다. 까라면 까 아까도 언급했지만, 애초에 전역 상태 관리가 크게 역량이 필요한 기능이 아니라서 팀원들이 불편하지 않다면 그냥 사용해도 무관하다고 생각한다.

중요한 것은 무엇을 사용하느냐 보다 전역상태를 어떨 때 사용하는지 파악하고 어떻게 해야 상태를 잘 추적해서 버그나 이슈를 안 발생시킬 지 고민하는 것이 먼저라는 생각이 들었다.

프로젝트 및 깃헙주소

https://state-comparison.vercel.app/
https://github.com/joshyeom/state-comparison

profile
내 꿈은 프론트 왕
post-custom-banner

0개의 댓글