recoil의 atom과 selector에 대해 조금 헷갈려서 정리해보았다. atom은 그 자체로 단일 데이터를 저장하고, selector는 atom을 통해 계산하여 얻어야 하는 데이터가 있을 때 정의하면 되는 것 같다.
호환성이나 단순함을 위해 외부 전역 상태를 사용하는 것보다, React에서 이미 만들어진(built-in) 상태(state) 관리 기능을 사용하는 것이 좋다.
컴포넌트 상태는 공통 조상 컴포넌트까지 올라가서야 공유가 가능하지만, 그렇게 되면 해당 전역 상태를 사용하는 모든 트리들에 대한 리렌더링이 필요하다. Context는 단일 값으로만 저장이 가능하고, 스스로 consumer를 가지지 못했다. 이것이 전역 상태를 선언한 트리와 해당 상태를 사용하는 트리 간의 분리가 불가능하게 했다.
React의 의미와 동작, API를 따르면서 해당 문제를 개선하고자 한 것이 recoil이다. Recoil은 React의 트리에 연결된 고유하면서 직교적인 단방향 그래프를 정의한다. 상태는 순수 함수(selector를 호출하는 곳)을 통해 해당 그래프의 루트(atom을 호출하는 곳)에서 흐름을 변경하고 컴포넌트에 영향을 준다.
- 공유된 상태가 단순한, React 지역 상태와도 비슷한 get/set 인터페이스를 보일러 플레이트가 필요하지 않은 API를 제공함
- 동시 모드(Concurrent Mode)와 같은 다른 React 기능들과도 호환이 가능
- 상태 정의는 코드 분할이 가능하도록 점진적이고, 분산적인 성질을 가짐
- 사용하는 컴포넌트를 수정하지 않고서도 파생된 데이터를 상태로서 사용할 수 있음
- 파생된 데이터는 해당 데이터를 사용하는 컴포넌트의 수정 없이 동기/비동기 적으로 이동이 가능
- navigation을 일급 객체로 취급할 수 있고, 링크 내에서 상태 값 전환의 인코딩 또한 가능
- 모든 애플리케이션의 상태를 하위 호환의 상태로 유지하기 쉬워서, 애플리케이션의 변화에도 상태값이 유지될 수 있음
recoil은 크게 Atom과 Selector라는 개념을 갖는다.
1. Atom
애플리케이션 내에서 사용하는 상태의 source of truth(모든 데이터는 하나의 출처만을 사용하는 것)을 가진다. 아래 예시에서의 source of truth는 객체의 배열이다.
const todoListState = atom({
key: 'todoListState',
default: [],
});
Atom은 고유 키 값(key)과, 기본 값(default)을 가진다. 해당 값을 읽기 위해선, useRecoilValue() 훅을 사용할 수 있다.
function TodoList() {
const todoList = useRecoilValue(todoListState);
// ...
}
값의 변경을 위해선 setter 함수에 접근해 데이터를 업데이트 할 수 있다. setter 함수는 useSetRecoilState() 훅을 사용할 수 있다.
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
값과, setter 함수를 동시에 가져오지 위해선 useRecoilState() 훅을 사용한다.
const [todoList, setTodoList] = useRecoilState(todoListState);
2. Selectors
파생된 상태의 일부로써 표현된다. 파생된 상태는 기존의 상태에서 새 값을 파생시키는 순수 함수에 상태 값을 넘겨줌으로서 얻는 상태 값이라고 생각할 수도 있다. Selectors는 다른 데이터에게 의존하는 동적인 데이터를 생성해주기 때문에, 매우 유용한 기능이다. 만약 투두리스트 프로젝트를 진행한다면, 다음 데이터를 Selectors를 통해 얻을 수 있다.
- 필터된 투두 리스트
- 투두 리스트 통계: 모든 투두의 갯수, 완료한 투두의 총 갯수, 투두 완료 비율 등
해당 기능을 구현하기 위해선 Atom으로 저장 가능한 '필터 조건 값'을 생성한다.
const todoListFilterState = atom({
key: 'TodoListFilter',
default: 'Show All', // 'Show All', 'Show Completed', "Show Uncompleted"
});
그리고 위에서 정의한 todoListState와 todoListFilterState 두 개를 사용하여 filteredTodoListState select를 생성할 수 있다.
const filteredTodoListState = selector({
key: 'FilteredTodoList',
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() {
// changed from todoListState to filteredTodoListState
const todoList = useRecoilValue(filteredTodoListState);
return (
<>
<TodoListStats />
<TodoListFilters />
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem item={todoItem} key={todoItem.id} />
))}
</>
);
}
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: 'TodoListStats',
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 * 100; // 투두 완료 비율
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
}; //객체로 반환할 수 있다.
},
});
function TodoListStats() {
const {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
} = useRecoilValue(todoListStatsState);
const formattedPercentCompleted = Math.round(percentCompleted);
return (
<ul>
<li>Total items: {totalNum}</li>
<li>Items completed: {totalCompletedNum}</li>
<li>Items not completed: {totalUncompletedNum}</li>
<li>Percent completed: {formattedPercentCompleted}</li>
</ul>
);
}
#엘리스트랙 #엘리스트랙후기 #리액트네이티브강좌 #온라인코딩부트캠프 #온라인코딩학원 #프론트엔드학원 #개발자국비지원 #개발자부트캠프 #국비지원부트캠프 #프론트엔드국비지원 #React #Styledcomponent #React Router Dom #Redux #Typescript #Javascript