Recoil은 Redux를 대체할 수 있을까?

개요

React가 세상에 나온 이후부터 현재까지 React에서 가장 많이 사용되는 (약 45%) 상태 관리 라이브러리는 Redux입니다. 그런데 약 2년 전 2020년 5월에 페이스북에서는 Recoil 이라는 React를 위한 상태 관리 라이브러리를 세상에 내놓았습니다.

그렇다면 페이스북에서는 왜 Recoil을 만들게 되었을까요?

페이스북은 복잡한 UI를 대상으로 전역 상태 관리를 위한 최적화 방법을 찾으려고 했지만 성능 및 효율성이라는 장벽에 부딪혔고 이 문제를 해결하기 위해서 직접 라이브러리를 만들게 되었다고 합니다. (https://medium.com/swlh/recoil-another-react-state-management-library-97fc979a8d2b )(구체적으로 어떤 상황에서 성능과 효율성이 한계에 다다르는지는 찾아내지 못했습니다.)

일단 공식문서에서도 알 수 있듯이 ReduxRecoil의 가장 큰 차이점은 ReduxReact를 위한 라이브러리가 아닌 반면에 RecoilReact를 위한 라이브러리라는 것입니다.

그래서 RecoilRedux보다 React와 함께 사용하기 편합니다. 예제를 통해 Recoil이 어떻게 더 사용하기 편한지 알아봅시다.

예제

간단한 카운터 예제를 만들어보면서 ReduxRecoil을 비교해보겠습니다.

Redux

폴더 구조는 Ducks 패턴을 따르겠습니다.

  1. counter 모듈 만들기
// src/modules/counter.js
/* 액션 타입 만들기 */
const SET_DIFF = 'counter/SET_DIFF';
const INCREASE = 'counter/INCREASE';
const DECREASE = 'counter/DECREASE';

/* 액션 생성함수 만들고 내보내기 */
export const setDiff = diff => ({ type: SET_DIFF, diff });
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });

/* 초기 상태 선언 */
const initialState = {
    number: 0,
    diff: 1
};

/* 리듀서 선언하고 default로 내보내기 */
export default function counter(state = initialState, action) {
    switch (action.type) {
        case SET_DIFF:
            return {
                ...state,
                diff: action.diff
            };
        case INCREASE:
            return {
                ...state,
                number: state.number + state.diff
            };
        case DECREASE:
            return {
                ...state,
                number: state.number - state.diff
            };
        default:
            return state;
    }
}

  1. rootReducer 만들기
// src/modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";

const rootReducer = combineReducers({counter});

export default rootReducer;

  1. createStorestore 만들고 Provider로 감싸주기
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { createStore } from "redux";
import { Provider } from "react-redux";
import rootReducer from "./modules";

const store = createStore(rootReducer);

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <Provider store={store}>
        <App />
    </Provider>
  </React.StrictMode>
);

  1. useSelector, useDispatch를 사용해 상태 가져오고 관리하기
import React from 'react';
import { useDispatch, useSelector } from "react-redux";
import { decrease, increase, setDiff } from "../modules/counter";

function Counter() {
    const { number, diff } = useSelector(state => ({
        number: state.counter.number,
        diff: state.counter.diff
    }))
    const dispatch = useDispatch();

    const onIncrease = () => dispatch(increase());
    const onDecrease = () => dispatch(decrease());
    const onChange = (e) => dispatch(setDiff(parseInt(e.target.value, 10)));

    return (
        <div>
            <h1>{number}</h1>
            <div>
                <input type="number" value={diff} min="1" onChange={onChange} />
                <button onClick={onIncrease}>+</button>
                <button onClick={onDecrease}>-</button>
            </div>
        </div>
    );
}

export default Counter;

Recoil

  1. atom으로 counterState 만들기
import { atom } from "recoil";

export const counterState = atom({
    key: 'counterState',
    default: {
        number: 0,
        diff: 1
    }
})

  1. RecoilRoot로 감싸주기
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import { RecoilRoot } from 'recoil';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <RecoilRoot>
        <App />
    </RecoilRoot>
  </React.StrictMode>
);

  1. useRecoilState로 가져와서 사용하기
import {counterState} from "../recoil/atoms/counterState";
import {useRecoilState} from "recoil";

const Counter = () => {
    const [counter, setCounter] = useRecoilState(counterState);
    const onIncrease = () => setCounter({...counter, number: counter.number + counter.diff});
    const onDecrease = () => setCounter({...counter, number: counter.number - counter.diff});
    const onChange = (e) => setCounter({...counter, diff: parseInt(e.target.value, 10)})

    return (
        <>
            <h1>{counter.number}</h1>
            <input type="number" value={counter.diff} min="1" onChange={onChange}/>
            <button onClick={onIncrease}>+1</button>
            <button onClick={onDecrease}>-1</button>
        </>
    )
}

export default Counter;

위의 예제를 통해서 Recoil이 작성해야할 코드도 훨씬 적고 hooks와 사용법이 유사하기 때문에 이해하기도 쉽다는 것을 알 수 있습니다.

자세히 살펴보기

Atom

atom은 하나의 상태입니다. atom의 값을 변경하면 해당 atom을 사용하고 있는 컴포넌트들은 모두 다시 렌더링됩니다. key에는 고유한 값을 넣어주고 default에는 초기값을 넣어줍니다. default에는 객체, 배열, 함수도 넣을 수 있습니다.

export const counterState = atom({
	key: 'counterState',
 	default: {
    	number: 0,
      	diff: 1
    }
})

useRecoilState

useState 처럼 상태와 상태 변경 함수를 리턴합니다.

import { counterState } from './recoil/atoms/counterState'

const [counter, setCounter] = useRecoilState(counterState);

useRecoilValue

값만 리턴합니다.

import { counterState } from './recoil/atoms/counterState'

const counter = useRecoilValue(counterState);

useSetRecoilState

상태 변경 함수만 리턴합니다.

import { counterState } from './recoil/atoms/counterState'

const setCounter = useSetRecoilState(counterState);

Selector

다른 atom이나 selector를 가져와서 동적으로 데이터를 변형할 수 있습니다. 따라서 selector 안에서 사용한 atom 또는 selector가 업데이트되면 해당 selector 함수도 다시 실행됩니다.

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

const userState = atom({
  key: 'user',
  default: {
    firstName: 'Gildong',
    lastName: 'Hong',
    age: 30
  }
});

const userNameSelector = selector({
  key: 'userName',
  get: ({get}) => {
    const user = get(userState);
    return user.firstName +  ' ' + user.lastName;
  },
  set: ({set}, name) => {
    const names = name.split(' ');
    set(
      userState,
      (prevState) => ({
        ...prevState,
        firstName: names[0],
        lastName: names[1] || ''
      })
    );
  }
});

function User() {
  const [userName, setUserName] = useRecoilState(userNameSelector);
  const inputHandler = (event) => setUserName(event.target.value);

  return (
    <div>
      Full name: {userName}
      <br />
      <input type="text" onInput={inputHandler} />
    </div>
  );
}

Redux to Recoil

마지막으로 thunk를 사용해 비동기처리를 하고 있는 앱을 recoil을 사용해 리팩토링 해보겠습니다.

비동기 처리하는 코드 부분만 살펴보겠습니다.

아래는 Ducks 패턴으로 작성한 Redux 코드입니다.

// src/modules/user.js
const SET_USER_PROFILE = "user/SET_USER_PROFILE";
const SET_LOADING_DATA = "user/SET_LOADING_DATA";

const URL = "https://user-profile-json-j7n0j4c8ican.runkit.sh/";

export const fetchUserProfile = (userId = "") => {
    return (dispatch, getState) => {
        dispatch(setLoadingData(true));
        fetch(`${URL}${userId}`)
            .then((res) => res.json())
            .then((data) => dispatch(setUserProfile(data)));
    };
}

export const setUserProfile = (data) => ({ type: SET_USER_PROFILE, payload: data });
export const setLoadingData = (val) => ({ type: SET_LOADING_DATA, payload: val });


const reducer = (state = {}, action) => {
    const { type, payload } = action;
    switch (type) {
        case SET_USER_PROFILE:
            return {
                ...payload,
                isLoading: false,
            };
        case SET_LOADING_DATA:
            return {
                ...state,
                isLoading: payload,
            };
        default:
            return state;
    }
};

export default reducer;

Recoil에서는 selector 함수에서 비동기처리를 할 수 있습니다. 그래서 비동기처리를 위해서 별도의 라이브러리를 설치해주지 않아도 되고 코드도 더 간결하다는 것을 알 수 있습니다.

// src/recoil/atoms/user.js
import { atom } from "recoil";

export const userIDState = atom({
    key: "currentUserId",
    default: "",
});
// src/api.js
const URL = "https://user-profile-json-j7n0j4c8ican.runkit.sh/";
export const fetchUserProfile = async (id) =>
    await fetch(`${URL}${id}`).then((res) => res.json());
// src/recoil/selectors/user.js
import { selector } from "recoil";
import { userIDState } from "../atoms/user";
import { fetchUserProfile } from "../../api";

export const userProfileState = selector({
    key: "userProfile",
    get: async ({ get }) => {
        const id = get(userIDState);
        return await fetchUserProfile(id);
    },
});

장단점

장점

  1. 러닝커브가 낮다
  2. Concurrent Mode를 지원한다. (비동기 selector를 만들고 suspense로 감싸면 쉽게 동시성 모드를 구현가능)
  3. 페이스북에서 만들었다.

단점

  1. hook을 기반으로 하기 때문에 class형 컴포넌트에서는 사용할 수 없다.
  2. Redux보다 직관적이지는 않은 것 같다.

결론

ReduxRecoil이 나타나기 전 독보적인 상태 관리 라이브러리였기 때문에 현재 Redux를 사용해 만들어진 프로젝트들이 압도적으로 많지만 페이스북이 앞으로 꾸준히 버전업을 하고 정식 버전까지 릴리즈된다면 'React의 상태 관리 라이브러리는 Recoil을 써야지! 라는 인식이 점점 생기지 않을까?' 하고 생각해봅니다.


레퍼런스

https://recoiljs.org/ko
https://ui.toast.com/weekly-pick/ko_20200616
https://react.vlpt.us/redux
https://blog.logrocket.com/refactoring-redux-app-to-use-recoil

좋은 웹페이지 즐겨찾기