React Recoil 사용하기
React Recoil 사용하기
React에서 Recoil을 사용하는 방법에 대해 알아보겠습니다.
1. Recoil
Recoil은 메타(구 페이스북)에서 만든 상태 관리 라이브러리입니다. 기존의 Redux와 같은 상태 관리 라이브러리들은 store 구성을 위해 많은 boilerplate 코드들을 필요로 하였고 이로 인하여 설정이 복잡해지게 되었습니다. Recoil은 이러한 문제의 해결과 전역 상태 관리의 최적화 및 성능과 효율성 향상을 위해 만들어졌습니다. 또한 React를 공식적으로 지원하는 상태 관리 라이브러리이기 때문에 React 내부에 접근이 가능하며 Concurrent 모드, Suspense 등을 손쉽게 사용할 수 있습니다. 이전에 React의 Hooks를 사용해봤다면 쉽게 적응하여 사용할 수 있습니다.
Recoil에는 다음과 같은 특징이 있습니다.
- atom
Recoil에서 상태를 정의하는 개념. (Redux의 store와 유사한 개념)
컴포넌트가 구독할 수 있는 state이며 atom을 생성하기 위해 고유한 key와 default 값 설정이 필요.
atom의 값이 변경되면 해당 atom을 구독하고 있는 컴포넌트들이 모두 다시 re-render 됨. - selector
파생된 상태(derived state)의 데이터를 의미.
read-only 객체이며 순수 함수.
Recoil에서 관리하는 상태의 일부만 선택하거나, 상태를 이용하여 연산한 값을 조회할 때 사용.
비동기 처리를 통해 atom에 의존적인 동적인 데이터 사용 가능.
1.1. Recoil 설치
다음 명령어를 실행하여 Recoil을 설치해줍니다.
$ yarn add recoil
2. Recoil 사용하기
Recoil을 사용해보면서 주요 특징들에 대해 알아보겠습니다.
src 하위에 다음과 같이 파일 구조를 만들어줍니다.
src
├── states
│ ├── recoilCounterState.ts
│ └── recoilTodoState.ts
├── screens
│ ├── RecoilCounter.tsx
│ └── RecoilTodo.tsx
├── ...
App.tsx
index.tsx
2.1. RecoilRoot
RecoilRoot는 애플리케이션에서 Recoil을 연동하기 위해 사용합니다. Redux에서 Provider를 이용하여 store를 설정했던 것과 비슷한 역할을 합니다.
다음과 같은 특징을 갖습니다.
- RecoilRoot
여러 개의 RecoilRoot를 사용 가능하며 이러한 경우에 각각의 RecoilRoot는 독립적인 provider/store로 동작.
RecoilRoot의 override 속성이 false인 경우, atom state는 각각의 RecoilRoot에 따라 다른 값을 갖게 됨.
RecoilRoot의 override 속성이 true인 경우, atom state는 중첩된 RecoilRoot에 따라 동일한 값을 갖게 됨.
index.tsx 파일을 다음과 같이 작성해줍니다.
/* index.tsx */
import React from 'react';
import ReactDOM from 'react-dom';
import { RecoilRoot } from 'recoil';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
ReactDOM.render(
<RecoilRoot>
<App />
</RecoilRoot>,
document.getElementById('root')
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
2.2. atom
atom은 Recoil에서 상태를 정의하여 사용하기 위한 방법입니다. atom을 이용하여 상태를 정의할 때는 고유값(key)과 기본값(default)을 설정해줍니다.
atom을 이용하여 다음과 같이 상태를 정의해줍니다.
const recoilCounterState = atom({
key: 'recoilCounterState',
default: initialState
});
atom으로 정의한 상태는 아래의 Hook을 이용하여 사용할 수 있습니다.
const [recoilCounter, setRecoilCounter] = useRecoilState(recoilCounterState);
const recoilCounterValue = useRecoilValue(recoilCounterState);
const setRecoilCounter = useSetRecoilState(recoilCounterState);
const resetRecoilCounter = useResetRecoilState(recoilCounterState);
각각의 Hook에 대해 정리하면 다음과 같습니다.
- useRecoilState
atom의 상태를 구독.
useState Hook과 같이 배열의 첫번째 파라미터로 상태, 두번째 파라미터로 상태에 대한 setter 함수를 반환. - useRecoilValue
setter 함수 없이 atom의 상태만 반환. - useSetRecoilState
atom 상태 없이 setter 함수만 반환. - useResetRecoilState
atom 상태를 default 상태로 reset.
컴포넌트에서의 동작 확인을 위해 먼저 states/recoilCounterState.ts, states/recoilTodoState.ts 파일을 작성해줍니다.
/* states/recoilCounterState.ts */
import { atom, selector } from 'recoil';
export interface CommonState {
value: number
};
const initialState: CommonState = {
value: 0
};
export const recoilCounterState = atom({
key: 'recoilCounterState',
default: initialState
});
/* states/recoilTodoState.ts */
import { atom, selector } from 'recoil';
interface TodoItem {
id: number,
title: string,
checked: boolean
};
export interface CommonState {
todoList: TodoItem[]
};
const initialState: CommonState = {
todoList: []
};
export const recoilTodoState = atom({
key: 'recoilTodoState',
default: initialState
});
다음으로 screens/RecoilCounter.tsx, screens/RecoilTodo.tsx 파일을 작성해줍니다.
/* screens/RecoilCounter.tsx */
import React, { useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { recoilCounterState } from '../states/recoilCounterState';
interface CommonState {
value: number
};
function RecoilCounter() {
////////////////////////////////////////
// Hooks
////////////////////////////////////////
const [recoilCounter, setRecoilCounter] = useRecoilState(recoilCounterState);
const defaultRecoilCounterState: CommonState = {...recoilCounter};
////////////////////////////////////////
// Functions
////////////////////////////////////////
const onIncrease = useCallback(() => {
defaultRecoilCounterState.value = recoilCounter.value + 1;
setRecoilCounter(defaultRecoilCounterState);
}, [recoilCounter]);
const onDecrease = useCallback(() => {
defaultRecoilCounterState.value = recoilCounter.value - 1;
setRecoilCounter(defaultRecoilCounterState);
}, [recoilCounter]);
////////////////////////////////////////
// View
////////////////////////////////////////
return (
<>
<div>
<h1>{recoilCounter.value}</h1>
<div>
<button onClick={onIncrease}>+</button>
<button onClick={onDecrease}>-</button>
</div>
</div>
</>
);
};
export default RecoilCounter;
/* screens/RecoilTodo.tsx */
import React, { useState, useCallback } from 'react';
import { useRecoilState } from 'recoil';
import { recoilTodoState } from '../states/recoilTodoState';
interface TodoItem {
id: number,
title: string,
checked: boolean
};
export interface CommonState {
todoList: TodoItem[]
};
function RecoilTodo() {
////////////////////////////////////////
// Hooks
////////////////////////////////////////
const [recoilTodo, setRecoilTodo] = useRecoilState(recoilTodoState);
const [inputTitle, setInputTitle] = useState('');
const defaultRecoilTodoState: CommonState = {...recoilTodo};
////////////////////////////////////////
// Functions
////////////////////////////////////////
const onChange = useCallback((event) => {
setInputTitle(event.target.value);
}, [inputTitle]);
const onSubmit = useCallback((event) => {
event.preventDefault();
const insertItem: TodoItem = {
id: (!recoilTodo.todoList.length) ? 0 : Math.max(...recoilTodo.todoList.map((item) => item.id)) + 1,
title: inputTitle,
checked: false
};
const setTodoList = [...recoilTodo.todoList, insertItem];
defaultRecoilTodoState.todoList = setTodoList;
setRecoilTodo(defaultRecoilTodoState);
setInputTitle('');
}, [inputTitle, recoilTodo]);
const onToggle = useCallback((id) => {
const setTodoList = [...recoilTodo.todoList];
const toggleIndex = setTodoList.findIndex(item => item.id === id);
const toggleItem = {...setTodoList[toggleIndex]};
toggleItem.checked = !toggleItem.checked;
setTodoList[toggleIndex] = {...toggleItem};
defaultRecoilTodoState.todoList = setTodoList;
setRecoilTodo(defaultRecoilTodoState);
}, [recoilTodo]);
const onRemove = useCallback((id) => {
const setTodoList = [...recoilTodo.todoList];
const removeIndex = setTodoList.findIndex(item => item.id === id);
setTodoList.splice(removeIndex, 1);
defaultRecoilTodoState.todoList = setTodoList;
setRecoilTodo(defaultRecoilTodoState);
}, [recoilTodo]);
////////////////////////////////////////
// View
////////////////////////////////////////
return (
<div>
<form onSubmit={onSubmit}>
<input value={inputTitle} onChange={onChange} />
<button type="submit">Add</button>
</form>
<div>
{
recoilTodo.todoList.map((item, index) => (
<div key={index}>
<input
type="checkbox"
checked={item.checked}
readOnly={true}
onClick={() => onToggle(item.id)}
/>
<span style={{ textDecoration: item.checked ? 'line-through' : 'none' }}>
{item.title}
</span>
<button onClick={() => onRemove(item.id)}>X</button>
</div>
))
}
</div>
</div>
);
};
export default RecoilTodo;
2.3. selector
Recoil에서 selector는 순수 함수이며 파생된 상태(derived state)라고 정의하고 있습니다. 파생된 상태는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있는 개념입니다. 하나의 상태를 순수 함수인 selector에 전달하여 반환 받은 결과물에 대해 파생된 상태로 볼 수 있습니다.
파생된 상태에 대한 예시는 다음과 같습니다.
- 특정 항목들이 필터링된 todo list
- todo list의 속성들을 계산한 통계
selector는 다음과 같이 구성하여 사용합니다.
export const recoilTodoSelector = selector({
key: 'recoilTodoSelector',
get: ({ get }) => {
return get(recoilTodoState);
},
set: ({ set }, value: CommonState)=> {
set(recoilTodoState, value);
}
});
selector의 구성 요소는 다음과 같습니다.
- key
내부적으로 selector를 식별하는 고유한 값. - get
파생된 상태를 반환.
atom이나 selector 또는 Promise를 반환. - set
쓰기 가능한 상태를 반환.
atom이나 다른 selector의 상태를 업데이트.
(자기 자신의 selector를 업데이트하면 무한 루프가 돌게 됨)
selector를 구성할 때 get 함수만 설정하면 읽기만 가능한 RecoilValueReadOnly 객체를 반환하고 userRecoilValue Hook으로만 상태 조회가 가능합니다. 반면에 get과 set 함수를 모두 설정하면 읽기/쓰기가 가능한 RecoilState 객체를 반환하고 useRecoilState Hook을 사용할 수 있습니다.
selector 구성을 위해 states/recoilCounterState.ts, states/recoilTodoState.ts 파일에 아래 코드를 각각 추가해줍니다.
/* states/recoilCounterState.ts */
export const recoilCounterSelector = selector({
key: 'recoilCounterSelector',
get: ({ get }) => {
return get(recoilCounterState);
},
set: ({ set }, value: CommonState)=> {
set(recoilCounterState, value);
}
});
/* states/recoilTodoState.ts */
export const recoilTodoSelector = selector({
key: 'recoilTodoSelector',
get: ({ get }) => {
return get(recoilTodoState);
},
set: ({ set }, value: CommonState)=> {
set(recoilTodoState, value);
}
});
다음으로 screens/RecoilCounter.tsx, screens/RecoilTodo.tsx 파일에서 useRecoilState 함수의 파라미터를 atom에서 selector로 변경해줍니다.
/* screens/RecoilCounter.tsx */
import { recoilCounterSelector } from '../states/recoilCounterState';
const [recoilCounter, setRecoilCounter] = useRecoilState(recoilCounterSelector);
/* screens/RecoilTodo.tsx */
import { recoilTodoSelector } from '../states/recoilTodoState';
const [recoilTodo, setRecoilTodo] = useRecoilState(recoilTodoSelector);
여기까지 구성하고 애플리케이션을 실행하면 Recoil이 사용된 예제 코드가 동작하는 것을 확인할 수 있습니다.
이상으로 React에서 Recoil을 사용하는 방법에 대해 알아봤습니다.
※ Reference
- recoiljs.org, Getting Started, https://recoiljs.org/docs/introduction/getting-started
- ajdkfl6445.gitbook.io, Recoil 기초 개념 및 사용법, https://ajdkfl6445.gitbook.io/study/react/recoil
- velog.io/@juno7803, [Recoil] Recoil 200% 활용하기, https://velog.io/@juno7803/Recoil-Recoil-200-%ED%99%9C%EC%9A%A9%ED%95%98%EA%B8%B0
- ui.toast.com, Recoil - 또 다른 React 상태 관리 라이브러리?, https://ui.toast.com/weekly-pick/ko_20200616
- blog.woolta.com, react의 새로운 상태관리 라이브러리 recoil 에 대해 알아보기 - atom, selector, https://blog.woolta.com/categories/1/posts/209