
Recoil의 핵심 컨셉
- 오직 React만을 위해 React처럼
- React 내부상태만 이용
- 작은 Atom 단위로 관리
- 순수함수 Selector
- Re-Render 최소화
- 데이터 흐름을 따라서
- 곧 새로운 React 기능과 호환성
- TypeScript를 기본적으로 지원
초기 설계 시 고려할 점
- 꼭 전역으로 관리되어야만 하는가?
간단한 건 Hooks와 Props로 충분
- 여러 상태값을 사용 용도에 따라 분류
UI 전용 상태 / 폼 데이터 처리 상태 / 서버 데이터 조회용 상태
- 어떤 데이터를 캐싱하고, 실시간으로 반영되야 하는지 파악
- 데이터가 어느 시점에서 변경되고 어떤 부분에 영향을 주는지 예측
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
Selectors는 상태를 기반으로 하는 파생 데이터를 계산하는 데 사용된다.
최소한의 상태 집합만 atoms에 저장하고 다른 모든 파생 데이터는
selectors에 명시한 함수를 통해 효율적으로 계산할 수 있다.
Selector는 atoms나 다른 selectors를 입력으로 받아들이는 순수 함수다.
상위의 atoms 또는 selectors가 업데이트되면 하위의 selector 함수도 다시 실행된다.
컴포넌트들은 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라는 약어로 표현한다.