들어가며
이번에는 Todo 리스트 애플리케이션을 만드려고 한다.
만들 기능으로는
▶ Todo 아이템 추가
▶ Todo 아이템 수정
▶ Todo 아이템 삭제
▶ Todo 아이템 필터링
▶ 통계 표시
Todo를 만들면서 Recoil의 atoms와 selectors, atom families와 Hook 그리고 최적화를 다루게 될 것이다.
Atoms
TodoList 컴포넌트
import { atom, useRecoilValue } from "recoil";
export const todoListState = atom({
key: "todoListState",
default: [],
});
const TodoList = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
{todoList.map((todoItem) => (
<div>{todoItem}</div>
))}
</>
);
};
export default TodoList;
export const todoListState = atom({
key: "todoListState",
default: [],
});
atom을 사용해서 todoListState 만들었다.
Todo를 작성하면 default의 배열에 값이 추가되는 방식이다.
const TodoList = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
{todoList.map((todoItem) => (
<div>{todoItem}</div>
))}
</>
);
};
useRecoilValue를 사용해서 todoListState의 default (값)만 가져왔다.
그리고 map을 사용해서 todoItem을 화면에 출력했다.
아직 특별한 작업을 한 것이 아닌, atom을 사용해서 state를 만들고 useRecoilValue를 사용해서
화면에 출력시키는 작업만 했다.
TodoItemCreator 컴포넌트
import { useState } from "react";
import { useSetRecoilState } from "recoil";
import { todoListState } from "./TodoList";
const TodoItemCreator = () => {
const [inputValue, setInputValue] = useState("");
const setTodoList = useSetRecoilState(todoListState);
const handleItemAdd = () => {
setTodoList((prev) => [
...prev,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue("");
};
const handleInputChange = ({ target: { value } }) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={handleInputChange} />
<button onClick={handleItemAdd}>Add</button>
</div>
);
};
let id = 0;
function getId() {
return id++;
}
export default TodoItemCreator;
Todo Item을 만들어주는 컴포넌트를 만들었다.
const setTodoList = useSetRecoilState(todoListState);
const handleItemAdd = () => {
setTodoList((prev) => [
...prev,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue("");
};
// ...
let id = 0;
function getId() {
return id++;
}
useSetRecoilState를 사용해서 업데이트 함수만 받아와서 handleItemAdd 함수를 만들었다.
useState에서 setState와 마찬가지로 prev 값을 가져올 수 있어서 기존 Todo를 기반으로
새로운 배열을 만들어서 추가해줬다.
// ...
const TodoList = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<div>{todoItem}</div>
))}
</>
);
};
Todo List 컴포넌트에 TodoItemCreator를 추가해줬다.
TodoItem 컴포넌트
import { useRecoilState } from "recoil";
import { todoListState } from "./TodoList";
const 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>
);
};
const replaceItemAtIndex = (arr, index, newValue) => {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
};
const removeItemAtIndex = (arr, index) => {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
};
export default TodoItem;
수정이 되거나 토글, 삭제에 대한 처리를 포함하는 TodoItem 컴포넌트이다.
useRecoilState를 사용하는 방식은 기존과 동일해서 특별한게 없다.
// ...
const TodoList = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem item={todoItem} key={todoItem.id} />
))}
</>
);
};
Todo List 컴포넌트에 TodoItem을 추가해줬다.
Selectors
// TodoList.jsx
export const todoListState = atom({
key: "todoListState",
default: [],
});
export const todoListFilterState = atom({
key: "todoListFilterState",
default: "Show All",
});
export const filterTodoListState = 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;
}
},
});
이번에는 필터링 기능을 구현하려고 한다.
export const todoListFilterState = atom({
key: "todoListFilterState",
default: "Show All",
});
필터링 기준을 선택하는 atom이다.
" Show All ", " Show Completed ", " Show Uncompleted " 를 옵션으로 가질 예정이며, 기본값은
Show All이다.
export const filterTodoListState = 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;
}
},
});
todoListFilterState와 todoListState를 사용해서 필터링 된 리스트를 결과물로 넘기는
filteredTodoListState이다.
todoListFilterState와 todoListState를 의존성으로 추적하기 때문에 둘 중 하나라도 변경되면 filteredTodoListState는 재실행된다.
TodoListFilter 컴포넌트
import { useSetRecoilState } from "recoil";
import { todoListFilterState } from "./TodoList";
const TodoListFilter = () => {
const setTodoListFilterState = useSetRecoilState(todoListFilterState);
const handleButtonClick = (value) => {
setTodoListFilterState(value);
};
return (
<div>
Filter :
<button onClick={() => handleButtonClick("Show All")}>Show All</button>
<button onClick={() => handleButtonClick("Show Completed")}>
Completed
</button>
<button onClick={() => handleButtonClick("Show Uncompleted")}>
Uncompleted
</button>
</div>
);
};
export default TodoListFilter;
todoListFilterState를 변경하는 컴포넌트이다.
useSetRecoilState를 사용해서 버튼을 클릭하면FilterState를 업데이트를 해주는 단순한 컴포넌트이다.
TodoList 컴포넌트
// ...
export const filterTodoListState = 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;
}
},
});
const TodoList = () => {
// const todoList = useRecoilValue(todoListState);
const todoList = useRecoilValue(filterTodoListState);
return (
<>
<TodoItemCreator />
<TodoListFilter />
{todoList.map((todoItem) => (
<TodoItem item={todoItem} key={todoItem.id} />
))}
</>
);
};
TodoListFilter 컴포넌트로 인해서 필터링 상태를 업데이트가 가능해졌기 때문에 todoListState Atom을
사용해서 화면에 TodoList를 렌더링하는 것이 아닌 filterTodoListState Atom을 사용해서
state를 가져와서 필터링 기능을 구현하였다.
TodoListStats 컴포넌트
import { selector, useRecoilValue } from "recoil";
import { todoListState } from "./TodoList";
const todoListStatsState = selector({
key: "todoListStatsState",
get: ({ get }) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((todo) => todo.isComplete).length;
const totalUncompletedNum = todoList.length - totalCompletedNum;
const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
};
},
});
const TodoListStats = () => {
const { totalNum, totalCompletedNum, totalUncompletedNum, percentCompleted } =
useRecoilValue(todoListStatsState);
const formattedPercentCompleted = Math.round(percentCompleted * 100);
return (
<ul>
<li>Total Item : {totalNum}</li>
<li>Total Completed : {totalCompletedNum}</li>
<li>Total Uncompleted : {totalUncompletedNum}</li>
<li>Persent Completed : {formattedPercentCompleted}</li>
</ul>
);
};
export default TodoListStats;
TodoList의 통계를 알기 위해서 TodoListStats 컴포넌트를 만들었다.
const todoListStatsState = selector({
key: "todoListStatsState",
get: ({ get }) => {
const todoList = get(todoListState);
const totalNum = todoList.length;
const totalCompletedNum = todoList.filter((todo) => todo.isComplete).length;
const totalUncompletedNum = todoList.length - totalCompletedNum;
const percentCompleted = totalNum === 0 ? 0 : totalCompletedNum / totalNum;
return {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
};
},
});
todoListState를 추적하기 때문에 변화가 생기면 todoListStatsState가 다시 실행된다.
그래서 filterTodoListState와 마찬가지로 최신의 값을 가질 수 있다.
확실히 Recoil을 사용하니 Redux에 비해서 구현하는 방식이 훨씬 간단한 것 같다.
지금까지는 Recoil을 사용하는 방법에 대해서 공부를 했다면 다음에는 프로젝트에서
Recoil을 잘 사용하는 방법에 대해서 공부를 해보겠다!
'React > 실험실' 카테고리의 다른 글
[React] 벨로퍼트와 함께하는 React Testing - 개요 (0) | 2022.11.16 |
---|---|
[React] Test Coverage (0) | 2022.11.15 |
[React] Recoil - 사용하기 (0) | 2022.11.10 |
[React] Recoil - 시작하기 (0) | 2022.11.09 |
[React] Context API 사용하기 (0) | 2022.11.05 |