React가 세상에 나온 이후부터 현재까지 React에서 가장 많이 사용되는 (약 45%) 상태 관리 라이브러리는 Redux입니다. 그런데 약 2년 전 2020년 5월에 페이스북에서는 Recoil 이라는 React를 위한 상태 관리 라이브러리를 세상에 내놓았습니다.
그렇다면 페이스북에서는 왜 Recoil을 만들게 되었을까요?
페이스북은 복잡한 UI를 대상으로 전역 상태 관리를 위한 최적화 방법을 찾으려고 했지만 성능 및 효율성이라는 장벽에 부딪혔고 이 문제를 해결하기 위해서 직접 라이브러리를 만들게 되었다고 합니다. (https://medium.com/swlh/recoil-another-react-state-management-library-97fc979a8d2b )(구체적으로 어떤 상황에서 성능과 효율성이 한계에 다다르는지는 찾아내지 못했습니다.)
일단 공식문서에서도 알 수 있듯이 Redux와 Recoil의 가장 큰 차이점은 Redux는 React를 위한 라이브러리가 아닌 반면에 Recoil은 React를 위한 라이브러리라는 것입니다.


그래서 Recoil은 Redux보다 React와 함께 사용하기 편합니다. 예제를 통해 Recoil이 어떻게 더 사용하기 편한지 알아봅시다.
간단한 카운터 예제를 만들어보면서 Redux와 Recoil을 비교해보겠습니다.

폴더 구조는 Ducks 패턴을 따르겠습니다.
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;
}
}
rootReducer 만들기// src/modules/index.js
import { combineReducers } from "redux";
import counter from "./counter";
const rootReducer = combineReducers({counter});
export default rootReducer;
createStore로 store 만들고 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>
);
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;
atom으로 counterState 만들기import { atom } from "recoil";
export const counterState = atom({
key: 'counterState',
default: {
number: 0,
diff: 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>
);
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을 사용하고 있는 컴포넌트들은 모두 다시 렌더링됩니다. key에는 고유한 값을 넣어주고 default에는 초기값을 넣어줍니다. default에는 객체, 배열, 함수도 넣을 수 있습니다.
export const counterState = atom({
key: 'counterState',
default: {
number: 0,
diff: 1
}
})
useState 처럼 상태와 상태 변경 함수를 리턴합니다.
import { counterState } from './recoil/atoms/counterState'
const [counter, setCounter] = useRecoilState(counterState);
값만 리턴합니다.
import { counterState } from './recoil/atoms/counterState'
const counter = useRecoilValue(counterState);
상태 변경 함수만 리턴합니다.
import { counterState } from './recoil/atoms/counterState'
const setCounter = useSetRecoilState(counterState);
다른 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>
);
}
마지막으로 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);
},
});
Redux는 Recoil이 나타나기 전 독보적인 상태 관리 라이브러리였기 때문에 현재 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