Recoil은 Redux를 대체할 수 있을까?
개요
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
을 비교해보겠습니다.
Redux
폴더 구조는 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;
Recoil
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의 값을 변경하면 해당 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);
},
});
장단점
장점
- 러닝커브가 낮다
- Concurrent Mode를 지원한다. (비동기 selector를 만들고 suspense로 감싸면 쉽게 동시성 모드를 구현가능)
- 페이스북에서 만들었다.
단점
- hook을 기반으로 하기 때문에 class형 컴포넌트에서는 사용할 수 없다.
- Redux보다 직관적이지는 않은 것 같다.
결론
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
Author And Source
이 문제에 관하여(Recoil은 Redux를 대체할 수 있을까?), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@dinomoon/Redux와-Recoil-비교해보기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)