Recoil을 도입한 이유
contextAPI에 비해서 무엇이 좋은가? 어떤 이유로 Recoil을 도입하게 되었는가?
- contextAPI가 가진 단점
- 렌더링 문제
- 코드가 너무 많아진다.
- 등등
장점
- React에 최적화된 상태관리 라이브러리
- 난이도가 쉬움
- Re-Render 최소화
- TypeScript를 기본적으로 지원 (의존 모듈 설치x)
한편으로, Recoil은 비동기적인 데이터를 처리하는 데 다소 문제가 있음을 발견하였다. 이 때문에 Recoil은 동기적인 전역 상태의 관리에 한정해 사용하고, 비동기적인 전역 상태의 관리는 useSWR을 사용할 계획이다.
useSWR에 관해서는 아래를 참고해주시기 바란다.
useSWR 도입 논의Atoms
컴포넌트가 구독할 수 있는 상태의 단위.
동일한 atom이 여러 컴포넌트에서 사용되는 경우 모든 컴포넌트는 상태를 공유한다.
atom의 키값은 전역적으로 고유해야 한다. 기본값도 갖는다.
const todoListState = atom({ key: 'todoListState', default: [], });
Hooks
- useRecoilState : 읽기, 쓰기
- useRecoilValue : 읽기 전용
- useSetRecoilState : 쓰기 전용
function TodoItem({item}) { const [todoList, setTodoList] = useRecoilState(todoListState); const index = todoList.findIndex((listItem) => listItem === item); const editItemText = ({target: {value}}) => { const newList = replaceItemAtIndex(todoList, index, { ...item, text: value, }); setTodoList(newList); }; const toggleItemCompletion = () => { const newList = replaceItemAtIndex(todoList, index, { ...item, isComplete: !item.isComplete, }); setTodoList(newList); }; const deleteItem = () => { const newList = removeItemAtIndex(todoList, index); setTodoList(newList); }; return ( <div> <input type="text" value={item.text} onChange={editItemText} /> <input type="checkbox" checked={item.isComplete} onChange={toggleItemCompletion} /> <button onClick={deleteItem}>X</button> </div> ); } function replaceItemAtIndex(arr, index, newValue) { return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)]; } function removeItemAtIndex(arr, index) { return [...arr.slice(0, index), ...arr.slice(index + 1)]; }
인상깊은 코드
replaceItemAtIndex, removeItemAtIndex
배열의 요소를 업데이트(수정, 삭제)하는 로직을 함수로 추상화한 것이 흥미롭다.
이 로직은 자주 사용된다. 유틸 함수로 두어서 전역적으로 사용해도 좋을 것 같다.
Selectors
Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수다.
상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다.
컴포넌트들은 selectors를 atoms처럼 구독할 수 있으며,
selectors가 변경되면 컴포넌트들도 다시 렌더링된다.
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다.
최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생 데이터는
selectors에 명시한 함수를 통해 효율적으로 계산함으로써 쓸모없는 상태의 보존을 방지한다.
Selectors는 어떤 컴포넌트가 자신을 필요로하는지,
또 자신은 어떤 상태에 의존하는지를 추적한다.
컴포넌트의 관점에서 보면
selectors와 atoms는 동일한 인터페이스를 가지므로 서로 대체할 수 있다.
useRecoilValue를 사용해 읽을 수 있다. 읽기 전용.
모든 atom은 쓰기 가능 상태지만
selector는 일부만 쓰기 가능한 상태(get과 set 속성을 둘 다 가지고 있는 selector)로 간주된다.
const todoListState = atom({ key: 'todoListState', default: [], }); const todoListFilterState = atom({ key: 'todoListFilterState', default: 'Show All', })
const filteredTodoListState = selector({ key: 'filteredTodoListState', get: ({ get }) => { const filter = get(todoListFilterState); const list = get(todoListState); switch (filter) { case 'Show Completed': return list.filter((item) => item.isComplete); case 'Show Uncompleted': return list.filter((item) => !item.isComplete); default: return list; } }, });
function TodoList() { const todoList = useRecoilValue(filteredTodoListState); return ( {todoList.map((todoItem) => ( <TodoItem item={todoItem} key={todoItem.id} /> ))} ); }
필터를 변경하려면 우리는
TodoListFilter
컴포넌트를 구현해야 한다.function TodoListFilters() { const [filter, setFilter] = useRecoilState(todoListFilterState); const updateFilter = ({target: {value}}) => { setFilter(value); }; return ( <> Filter: <select value={filter} onChange={updateFilter}> <option value="Show All">All</option> <option value="Show Completed">Completed</option> <option value="Show Uncompleted">Uncompleted</option> </select> </> ); }
통계 표시하기
const todoListStatsState = selector({ key: 'todoListStatsState', get: ({get}) => { const todoList = get(todoListState); const totalNum = todoList.length; const totalCompletedNum = todoList.filter((item) => item.isComplete).length; const totalUncompletedNum = totalNum - totalCompletedNum; const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum; return { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, }; }, });
function TodoListStats() { const { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted, } = useRecoilValue(todoListStatsState); const formattedPercentCompleted = Math.round(percentCompleted * 100); return ( <ul> <li>Total items: {totalNum}</li> <li>Items completed: {totalCompletedNum}</li> <li>Items not completed: {totalUncompletedNum}</li> <li>Percent completed: {formattedPercentCompleted}</li> </ul> ); }
atoms와 selectors의 용도를 잘 구분해서 사용하는 것이 핵심일 것 같다.
다음 사실을 기억하자.
최소한의 상태 집합만 atoms에 저장하고, 다른 모든 파생 데이터는 selectors에 명시한 함수를 통해 계산한다. Selectors는 자신이 의존하는 atoms나 selectors의 상태가 변경되면 재실행된다.
기타
- atoms, selector의 키 값은 이름과 동일하게 사용한다.
- 공식문서에서는 atoms, selectors에 접미사로 state를 붙인다.
- 통계(statistics)를 보통 stats라는 약어로 표현한다.