기존 프로젝트를 리팩토링하며 다양한 상태관리 라이브러리를 사용해보고자 Recoil에서 Zustand로 바꿔서 사용해봤습니다. 사실 Zustand는 Provider 설정할 필요가 없다는 이야기에 더 편하다고 생각해 선정했습니다...하핳 이 과정에서 이해한 Recoil과 Zustand의 차이점을 비교해보며 각 라이브러리에 대해 더 깊이 공부해보고자 합니다.

 

각 라이브러리의 사용법과 개념을 먼저 알아보고 차이점을 비교해보며, 사용하기에 적절한 상황을 이해해봅시다.

 

사용법 설명 부분이 생각보다 길어져 두 라이브러리를 비교한 것을 먼저 작성하고

각 상태관리 라이브러리에 대한 사용법을 아래에 작성하겠습니다.

 

Recoil vs Zustand

Recoil

특징:

  • Recoil은 Facebook에서 개발한 라이브러리로, React의 기능과 밀접하게 통합되어 설계되었습니다.
  • atom을 사용하여 상태관리하며, 이 원자들은 애플리케이션의 어느 부분에서나 재사용 가능합니다.
  • selector를 통해 파생된 상태를 생성하고, 비동기 작업을 관리할 수 있습니다.
  • RecoilRoot 컴포넌트를 통해 상태를 전역적으로 제공하며,
    상태 변화에 따른 컴포넌트의 선택적 리렌더링이 가능합니다.

적합한 사용 시나리오:

  • 대규모 프로젝트에서 여러 컴포넌트 간 복잡한 상태 공유가 필요한 경우.
  • 비동기 데이터 처리와 같이 복잡한 상태 로직을 관리해야 할 때.
  • 상태의 일부만을 구독하고, 해당 부분의 변경이 있을 때만 컴포넌트를 업데이트 하고 싶을 때.

Zustand

특징:

  • Zustand는 상태 관리를 위한 더 가벼운 라이브러리로, 빠르고 간단하게 설정할 수 있습니다.
  • 설정이 간단하며, Redux 같은 추가적인 보일러플레이트나 리듀서를 사용하지 않습니다.
  • React 외의 다른 환경에서도 사용 가능하며, 상태 저장소를 쉽게 생성하고 사용할 수 있습니다.
  • 컴포넌트 리렌더링은 구독하는 상태의 변경이 있을 때만 발생합니다.

적합한 사용 시나리오:

  • 중소규모 프로젝트단순한 상태 로직을 가진 애플리케이션에서.
  • 빠르고 간단한 상태 관리가 필요할 때.
  • 다른 프레임워크 또는 바닐라 자바스크립트와 함께 사용해야 할 때. 

 

제가 짚고 싶었던 둘의 큰 차이는 Provider 사용 여부입니다. Zustand는 애플리케이션의 모든 상태 정보를 한 곳에서 관리하는 구조를 가지기 때문에 별도의 Provider를 설정하지 않아도 되지만, Recoil은 React의 컴포넌트 트리 내에서 사용되기 때문에 Provider 설정이 필요합니다. 리팩토링 과정에서 Zustand 선택 이유는 단순히 Provider를 설정하지 않는다는 것과 새로운 상태 관리 라이브러리를 배워보자 였습니다. 다시 깊게 공부하며 간단하게 Provider 설정 유무만 보고 '더 편하네'라고 생각하고 넘어갔던 저를 반성할 수 있었습니다. 또한 상태 관리 라이브러리를 이용한 효율적인 비동기 처리를 학습하며, 한단계 성장할 수 있었던 값진 시간이었습니다.

 


Recoil

Facebook(현 Meta)에서 만든 상태 관리 라이브러리로, React 애플리케이션에 적합하게 설계되었습니다. 이 라이브러리는 React의 Hook API를 기반으로 하여 상태를 구성하고, 상태를 컴포넌트에 전달하는 작업을 간소화합니다. 이러한 Recoil을 사용하기 위해선 패키지를 설치한 후, RecoilRoot로 애플리케이션를 감싸고, atom과 selector라는 개념으로
상태 관리를 추상화하고 조직화하는 기능을 제공 합니다. 그리고 Recoil에서 useRecoilState를 가져와 useState처럼 사용할 수 있습니다. 

 

사용하는 과정을 더 자세히 알아보겠습니다.

 

1. npm 혹은 yarn을 이용해 recoil을 설치합니다.

npm i recoil
#or
yarn add recoil

 

 

2. 애플리케이션의 최상위 컴포넌트를 <RecoilRoot>로 감싸줍니다.

import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import App from './App';
 
ReactDOM.render(
  <RecoilRoot>
    <App />
  </RecoilRoot>,
  document.getElementById('root')
);

 

Next에서는 RecoilRootProvider를 만들어서 layout.tsx에서 컴포넌트를 감싸줘야 합니다.

//RecoilRootProvider

'use client'

import { RecoilRoot } from 'recoil';
export default function RecoilRootProvider({
    children,
}: {
    children: React.ReactNode
}) {
    return <RecoilRoot>{children}</RecoilRoot>
}
// layout.tsx

import "./globals.css";
import RecoilRootProvider from "@/component/RecoilRootProvider";

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <RecoilRootProvider>
          {children}
        </RecoilRootProvider>
      </body>
    </html>
  );
}

 

 

3. Atom을 이용한 상태 정의

// recoilState.ts
import { atom } from 'recoil';
 
export const countState = atom<number>({
  key: "countState",
  default: 0,
});

위와 같이 상태를 설정합니다. 그리고 키 값은 유일한 값이어야 합니다.

이렇게 정의한 상태는 useRecoilState 훅을 이용해 값을 가져오고, 변경도 가능합니다.

import { useRecoilState } from 'recoil';
import { countState } from './recoilState';

export default function CountComponent() {
	const [count, setCount] = useRecoilState(countState);
    
    const plus = () => {
    	setCount(count + 1)
    }
    
    retrun (
    <div>
    	<p>{count}</p>
    	<button onClick = {plus}> count 증가 </button>
    </div>
    )
	
}

 

여기서 둘다 가져오지 않고 상태값 혹은 상태값 설정만 하고 싶다면 

useRecoilValue : 상태값만

useSetRecoilState : 상태값 설정

를 사용하면 된다.

const count = useRecoilValue(countState)
const setCount = useSetRecoilState(countState)

 

그리고 디폴트값으로 상태를 초기화하는 useResetRecoilState()도 있다.

 const resetCount = useResetRecoilState(countState);
 //default로 설정된 값을 가져온다.

 

4-1. selector + get

살짝 어려운 개념일 수 있으나 사용 코드를 보면 이해하기 쉽다.

간단히 말해서 selector를 이용해 다른 state를 파생할 수 있다. 아래 예시를 보자

import {atom, selector, useRecoilState, useRecoilValue} from 'recoil';


// 상태 정의
const countState = atom<number>({
	key: "countState",
    default: 0,
});

// 파생 상태 <= selector 사용
// 정의된 상태값을 가져와서 state를 변환, 조합, 필터링 할 수 있다.
const evenOrOddState = selector<string>({
	key: "evenOrOddState",
    get: ({get}) => {
    	const count:number = get(countState);
        return count % 2 == 0 ? '짝수' : '홀수';
    },
});


export default function Counter() {
	const [count, setCount] = useRecoilState(countState);
    const evenOrOdd = useRecoilValue(evenOrOddState);
    
    const plus = () => {
    	setCount(count + 1)
    }
    
    return(
    <main>
    	<p>Count : {count}</p>
    	<p>Even Or Odd : {evenOrOdd}</p>
        <button onClick={plus}>Plus</button>
    </main>
    )
}

selector와 get을 이용해 정의한 상태값을 가져올 수 있고, 정의한 상태값에 대해 원하는 처리를 할 수 있습니다.

그리고 현재에는 get만 이용해 값을 가져오기 때문에 select 로 파생한 상태값을 변경할 수 없습니다.

 

그러므로 evenOrOddState 는 useSetRecoilState 나 useRecoilState 의 파라미터로는 넣을 수 없습니다.

 

4-2. selector + get + set

set도 함께 적으며 어떻게 사용하는지 알아봅시다.

import {atom, selector, useRecoilState} from 'recoil';

const nameState = atom<string>({
	key: "nameState",
    default: "hong",
});
const greetingState = selector<string>({
	key: "greetingState",
    get: ({get}) => {
    	const name = get(nameState);
        return `Hello ${name}!`;
    },
    set: ({set}, newValue) => {
    	const newName = newValue.replace("Hello, ", "").replace("!", "");
        set(nameState, newName);
    },
});
export default function MyComponent() {
	const [greeting, setGreeting] = useRecoilState(greetingState);
    
    const handleChangeGreeting = () => {
    	setGreeting("Hello, Alice!"); //set 함수를 사용해 greetingState 업데이트
    }
    
    
    return (
    <main>
    	<p>{greeting}</p>
    	<button onClick={handleChangeGreeting}> Change Greeting </button>
    </main>
    )
}

 

 

greetingState는 nameState에 의존해서 파생된 state를 계산하는 selector 입니다. 파생한 후 set함수를 사용해 업데이트할 새로운 값을 인자로 받아서 nameState를 업데이트 해줍니다.

즉 Hello, Alice!라는 문자열을 받았을 때, Hello,와 !를 없애주고 Alice만 남겨서 nameState에 업데이트 해주는 것입니다.

 

4-3. selector + 비동기 처리 + useRecoilValueLoadable

selector의 특징과 useRecoilValueLoadable를  이용해 성능상 유리한 비동기 데이터 호출을 할 수 있다.

import React from 'react';
import { atom, selector, useRecoilValueLoadable } from 'recoil';

const dataState = atom({
	key: "dataState",
    default: null,
});


const dataSelector = selector({
	key: "dataSelector",
    get: async ({get}) => {
    	try{
        	//비동기 API 호출을 수행
            const response = await fetch(url);
            condt data = await response.json();
            return data;
        } catch(error) {
        	throw new Error("데이터 호출 실패", error);
        }
    },
});



export default MyComponent(){
	const dataLoadable = useRecoilValueLoadable(dataSelector);
    
    switch (dataLoadable.state) {
    	case "loading":
        	return <div>로딩 중...</div>;
    	case "hasValue":
            const data = dataLoadable.contents;
            return(
            <div>
            	<p>Data : {data}</p>
            </div>
            );
    	case "hasError":
        	return <div>Error: {dataLoadable.contents.message}</div>;
        default:
        	return null;
    }

}

dataSelect의 get함수는 데이터를 비동기로 가지고 옵니다.

그리고 useRecoilValueLoadable 훅으로 dataSelector를 가지고 오면

dataLoabable.state 를 사용해 데이터 상태를 확인할 수 있다.

 

이러한 Loabable의 객체는 state와 contents 라는 프로퍼티가 있습니다.

 

state : hasValue, hasError, loading

contents : hasValue 일 경우 value, hasError 일 경우 Error 객체, loading 일 경우 Promise

 

4-4. selectorFamily

 외부에서 파라미터로 값을 가져와 selector에 적용해야할 때 사용합니다.

export const githubRepo = selectorFamily({
  key: "github/get",
  get: (githubId) => async () => {
    if (!githubId) return "";
 
    const { data } = await axios.get(
      `https://api.github.com/repos/${githubId}`
    );
    return data;
  },
});

 

import { useRecoilValue } from 'recoil';
import { selectorFamily } from '../../state';

export default const Github = () => {
  const githubId = 'Dawon00';
  const githubRepos = useRecoilValue(githubRepo(githubId));
  
  return(
    <>
      <div>Repos : {githubRepos}</div>
    </>
  )
 
}

 

 

 

참고:

 

[React] Recoil 로 React 스럽게 상태관리하는법

상태관리 라이브러리 없이 프로젝트를 하다가 아, 이건 안되겠다. 전역으로 상태관리해야겠다! 싶은 부분이 있었다. 근데 오늘 아침 다른 개발자분 티스토리 피드를 보다가 Recoil 을 추천하시는

dawonny.tistory.com


 

Zustand

가볍고 설정이 간단한 상태 관리 라이브러리입니다. Zustand는 특별한 Redux의 보일러플레이트 없이 상태를 관리할 수 있는 방법을 제공합니다. 이 라이브러리는 중앙집중식 상태 저장소(모든 상태가 하나의 저장소에서 관리)를 만들어 사용하지만, Redux처럼 액션과 리듀서에 의존하지 않습니다. 대신, 상태 업데이트 로직을 직접 저장소 내의 함수로 구현할 수 있습니다. Zustand는 React뿐만 아니라 Vanilla JavaScript나 다른 프레임워크에서도 사용할 수 있어 유연성이 높습니다.

중앙집중식 상태 저장소를 사용하기 때문에 별도로 Provider를 설정하지 않아도 된다는 점에서 높은 간결성을 지니고 있으며, 상태 변경 시 관련된 컴포넌트만 리렌더링되도록 최적화되어 있어, 성능 측면에서 이점을 제공합니다.

 

이러한 Zustand를 사용하는 방법은 매우 간단합니다.

 

1. npm 혹은 yarn으로 패키지를 설치합니다.

npm i zustand
# or
yarn add zustand

 

2. 상태 정의

// store.js
import create from "zustand";

const useStore = create((set) => ({
  count: 0,
  selectedButton: null,

  setSelectedButton: (button) => set({ selectedButton: button }),
  incrementCount: () => set((state) => ({ count: state.count + 1 })),
  removeCount: () => set({ count: 0 }),
}));

export default useStore;

 

Zustand에서 상태 저장소(store)는 create 함수를 사용하여 생성됩니다.

여기서 set 함수는 상태를 업데이트하는 데 사용됩니다.

또한, 상태를 변경하는 함수들(increasePopulation, removeAllBears)을 정의하여

상태 변화를 쉽게 관리할 수 있습니다.

 

3. 컴포넌트에서 사용하기

import React from "react";
import useStore from "../store/store.js";

export default function FirstChild() {
  const setSelectedButton = useStore((state) => state.setSelectedButton);
  const incrementCount = useStore((state) => state.incrementCount);
  const removeCount = useStore((state) => state.removeCount);

  const handleClick = (button) => {
    setSelectedButton(button);
  };

  return (
    <div>
      <h1>FirstChild</h1>
      <div>
        <button onClick={() => handleClick("O")}>O</button>
        <button onClick={() => handleClick("X")}>X</button>
      </div>
      <div>
        <button onClick={incrementCount}>카운트 증가</button>
        <button onClick={removeCount}>카운트 리셋</button>
      </div>
    </div>
  );
}

만즌 저장소를 불러와 위와 같이 사용할 수도 있고, 아래와 같이 store 액션을 한 번에 가져와 사용할 수도 있습니다.

import React from "react";
import useStore from "../store/store.js";

export default function FirstChild() {
  const { setSelectedButton, incrementCount, removeCount } = useStore((state) => state);

  const handleClick = (button) => {
    setSelectedButton(button);
  };

  return (
    <div>
      <h1>FirstChild</h1>
      <div>
        <button onClick={() => handleClick("O")}>O</button>
        <button onClick={() => handleClick("X")}>X</button>
      </div>
      <div>
        <button onClick={incrementCount}>카운트 증가</button>
        <button onClick={removeCount}>카운트 리셋</button>
      </div>
    </div>
  );
}

 

Zustand도 비동기 처리가 가능합니다.

 

4-1. 비동기 상태 정의

import create from 'zustand'

const useStore = create(set => ({
  fishes: 0,
  fetchFishes: async () => {
    const response = await fetch("https://api.example.com/fishes");
    const fishes = await response.json();
    set({ fishes: fishes.length });
  }
}));

 

4-2. 컴포넌트에서 사용

import React, { useEffect } from 'react';
import useStore from './store'; // Zustand 저장소를 불러옵니다.

function FishesComponent() {
  const fishes = useStore(state => state.fishes);
  const fetchFishes = useStore(state => state.fetchFishes);

  useEffect(() => {
    fetchFishes(); // 컴포넌트가 마운트되었을 때 데이터를 불러옵니다.
  }, [fetchFishes]);

  return <h1>{fishes} fishes found</h1>;
}

 

위의 작업 흐름은 다음과 같습니다.

  1. 컴포넌트 마운트: FishesComponent가 마운트되면, useEffect 훅이 실행되어 fetchFishes 함수를 호출합니다.
  2. 데이터 불러오기: fetchFishes 함수는 API 호출을 수행하고, 응답에서 얻은 데이터를 이용하여 fishes 상태를 업데이트합니다.
  3. 상태 업데이트: 상태가 업데이트되면, Zustand의 상태 구독 기능에 따라 FishesComponent가 자동으로 리렌더링되고 업데이트된 fishes 값을 표시합니다.

 

+ Recent posts