React에서 유명한 상태관리 라이브러리인 Recoil에 대해 알아보도록 하겠다.
상태관리하는 이유는 props drilling 현상을 최대한 줄이기위해 탄생했다고 해도 과언이 아니다.
부모 컴포넌트에서 자식 컴포넌트로 상태를 전달하기 위해서는 props
로 전달을 해야한다.
React는 계층적인 UI 구조를 가지기 때문에 자식이 28대손까지 있다면 Grand Grand Grand Grand .... Parents의 props를 받는 구조가 될 수도 있다.
그래서 React에서는 Context API를 만들었지만, 이는 관리하기도 귀찮아서 잘 사용하지 않는다.
원리 자체는 Recoil과 거의 유사하다.
하지만 상태 하나하나를 만들때마다 App 컴포넌트의 JSX에서 <~~~Context.Provider>
가 증식하는 현상을 볼 수 있을 것이다.
그래서 Recoil에서는 이들을 묶어서 <RecoilRoot>
이라는 태그로 한번에 여러 상태들을 전역으로 관리할 수 있도록 한다.
스플래시 화면에서 로그인과 회원가입 버튼이 네비게이션바로 있는 상황을 예제로 들자.
로그인하기전에는 로그인과 회원가입이 나오며, 로그인 후에는 로그아웃 버튼만 보인다.
그리고 로그인이나 회원가입 버튼을 누르면 각 버튼에 맞는 모달창이 나오는 정책이라고 하자.
import React from 'react';
import { Route, Routes } from 'react-router-dom';
import { ThemeProvider } from 'styled-components';
import GlobalStyles from './styles/GlobalStyles';
import { lightTheme } from './styles/theme';
import { Splash } from './ui/Splash';
import { RecoilRoot } from 'recoil';
function App() {
return (
<RecoilRoot>
<ThemeProvider theme={lightTheme}>
<GlobalStyles />
<Routes>
<Route path="/" element={<Splash />} />
</Routes>
</ThemeProvider>
</RecoilRoot>
);
}
export default App;
예시의 코드이다. 다른건 다 무시하고, RecoilRoot
로 최상위 JSX를 감싸주면 된다.
이렇게 되면 하위에 있는 모든 컴포넌트는 Recoil의 상태들을 가져와 사용할 수 있다.
Recoil에서는 상태들을 atoms
로 관리한다.
key
, default
로 키 값과 기본값을 정의할 수 있다.
import { atom } from 'recoil';
// 로그인 상태를 저장하는 atom (초기값: 로그인되지 않은 상태)
export const loginState = atom<boolean>({
key: 'loginState', // 고유한 키
default: false, // 초기값: 로그인되지 않은 상태
});
// 모달 상태를 저장하는 atom (초기값: 모달이 닫혀 있는 상태)
export const modalState = atom<string | null>({
key: 'modalState',
default: null, // 초기값: 모달이 닫혀 있는 상태
});
스플래시 화면에서 필요한 상태는 2가지 이다.
상태값을 변경을 할 수 있어야한다.
useState를 사용했을 때는 const [state, setStae] = useState()
로 상태와 변경하는 기능이 둘 다 제공되는 것처럼, Recoil에도 유사한 기능이 있는 훅이 있다.
useRecoilState
으로 다음처럼 사용할 수 있다.
import { useRecoilState } from 'recoil';
import { modalAtom, loginAtom } from '../recoil/splash';
function MyComponent() {
// 2. Recoil 상태 사용
const [modalState, setModalState] = useRecoilState(modalAtom);
const [loginState, setLoginState] = useRecoilState(loginAtom);
return (
// modalState, loginState를 활용
);
}
useRecoilState
에서는 state, setState를 둘다 제공하는데 이 훅은 두개를 분리해서 제공한다.
단순한 값만 가지는 atoms와는 다르게 set과 get을 포함하는 상태를 선언할 수 있다.
이를 selector
라고 하는데, 마찬가지로 key
가 존재하며 추가적으로 set
, get
을 선택적으로 정의할 수 있다.
import { selector } from 'recoil';
import { loginState, modalState } from './atoms';
// 로그인 상태를 가져오는 selector
export const loginStateSelector = selector<boolean>({
key: 'loginStateSelector',
get: ({ get }) => {
const isLoggedIn = get(loginState);
return isLoggedIn;
},
set: ({ set }, newValue) => {
set(loginState, newValue);
},
});
// 모달 상태를 가져오는 selector
export const modalStateSelector = selector<boolean>({
key: 'modalStateSelector',
get: ({ get }) => {
const isModalOpen = get(modalState);
return isModalOpen;
},
set: ({ set }, newValue) => {
set(modalState, newValue);
},
});
atoms를 가져와서 selector로 확장한 것이다.
selector도 atoms와 마찬가지로 useRecoilState
, useRecoilValue
등의 훅에서 사용이 가능하다.
위의 selector
코드를 보면 get, set이 그대로 정의되어있어서 사실 atoms
만 써도 잘 동작할 것처럼 보인다.
왜 굳이 selector
를 쓰는걸까?
예를들어 쇼핑카트가 있고 총합을 계산해야하는 상황이 있다고 하자.
import { atom, selector } from 'recoil';
// 쇼핑 카트의 각 상품을 나타내는 atom
export const cartItemsState = atom({
key: 'cartItemsState',
default: [
{ id: 1, name: 'Apple', price: 1.2, quantity: 2 },
{ id: 2, name: 'Orange', price: 0.9, quantity: 3 },
],
});
// 총합을 계산하는 selector
export const totalPriceSelector = selector({
key: 'totalPriceSelector',
get: ({ get }) => {
const cartItems = get(cartItemsState);
return cartItems.reduce((total, item) => total + item.price * item.quantity, 0);
},
});
이렇게 쇼핑카트의 상태는 atoms
로 저장하고, 파생된 상태인 쇼핑카트에 담긴 가격의 합을 selector
로 정의할 수 있다.
마치 useMemo
훅과 유사하다고 보면 쉽다.