Recoil 레퍼런스를 토대로 진행했습니다.
Recoil
A state management library for React.
recoiljs.org
리코일 사용해보기 전편에 이어서..
리코일 사용해보기 - 1 [React/Recoil/typescript]
Recoil 레퍼런스를 토대로 진행했습니다. Recoil A state management library for React. recoiljs.org 리코일(Recoil) 호환성 및 단순함을 이유로 외부의 글로벌 상태관리 라이브러리보다는 React 자체에 내장된 상태
woongtech.tistory.com
이번에는 간단한 todo 리스트 애플리케이션을 제작할 예정이다. 애플리케이션은 다음 기능을 수행한다.
- todo 아이템 추가
- todo 아이템 수정
- todo 아이템 삭제
- todo 아이템 렌더링
- 유용한 통계 표시
이 과정에서, Recoil API에 노출된 atoms, selectors, atom families와 hook을 다룰 예정이다. 최적화 또한 진행한다.
모든 코드는 App.tsx에 작성된다.
Atoms
아톰은 애플리케이션 상태의 source of truth를 갖는다. todo 리스트에서 source of truth는 todo 아이템을 나타내는 객체로 이루어진 배열이 된다.
const todoListState = atom<ITodo[]>({
key: "todoListState",
default: [],
});
아톰 리스트를 todoListState
라고 했다. 아톰에 고유한 키를 설정하고 빈 배열을 default로 설정했다. 이 항목을 읽기 위해, useRecoilValue 훅을 TodoList
컴포넌트에서 사용할 수 있다.
function TodoList() {
const todoList = useRecoilValue(todoListState);
return (
<>
{/*<TodoListStats />*/}
{/*<TodoListFilters />*/}
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
}
TodoItemCreator
새로운 todo 아이템을 구현하기 위해 todoListState의 내용을 업데이트하는 setter 함수에 접근해야한다. TodoItemCreator
컴포넌트의 setter 함수를 얻기 위해 useSetRecoilState
훅을 사용할 수 있다.
interface ITodo {
id: number;
text: string;
isComplete: boolean;
}
우선, ITodo라는 인터페이스를 정의해주었다. 이 인터페이스에서는 Todo item의 id, text, isComplete 값을 가지고 있다.
function TodoItemCreator() {
const [inputValue, setInputValue] = useState("");
const setTodoList = useSetRecoilState<ITodo[]>(todoListState);
const addItem = () => {
setTodoList((oldTodoList) => [
...oldTodoList,
{
id: getId(),
text: inputValue,
isComplete: false,
},
]);
setInputValue("");
};
const onChange = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
setInputValue(value);
};
return (
<div>
<input type="text" value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
}
let id = 0;
function getId() {
return id++;
}
TodoItemCreator
컴포넌트는 TodoItem을 추가하기 위한 입력창과 버튼이 있는 컴포넌트이다. useState 훅을 사용해 inputValue state를 생성해주고, 이 state를 입력창의 value 값으로 설정한다.
그리고, useSetRecoilStore
를 생성해 todoListState
의 state를 업데이트할 수 있는 setTodoList 함수를 가져온다.
addItem
함수는 입력창에 입력된 값을 todo 리스트에 추가하는 함수이다. 이 때, setTodoList
함수를 사용해 todo 리스트의 상태를 업데이트하고, 입력창의 값을 초기화해준다.
onChange
함수는 입력창에 값이 입력될 때마다 호출되는 함수이다. target으로 설정된 값을 가져와서 setInputValue
함수를 이용해 inputValue
state를 업데이트한다.
마지막으로, getId
함수는 todo item의 id를 생성해주는 함수이다. 초기값으로 0을 가지는 id 변수를 사용해 새로운 값을 생성해준다.
이렇게 구현된 TodoItemCreator
컴포넌트는 todo 리스트에 새로운 item을 추가하는 기능을 제공한다. 이것을 TodoList
컴포넌트에서 렌더링하고, todoListState
의 상태를 가져와서 Todo list를 렌더링한다. 기존 todo 리스트를 기반으로 새 todo 리스트를 만들 수 있도록 setter 함수의 updater 형식을 사용한다는 점에 유의해야 한다.
TodoItem
TodoItem
컴포넌트는 todo 리스트의 값을 표시하는 동시에 텍스트를 변경하고 항목을 삭제할 수 있다. todoListState
를 읽고 항목 텍스트를 업데이트하고, 완료된 것으로 표시하고, 삭제하는 데 사용하는 setter 함수를 얻기 위해 useRecoilState
를 사용한다.
function TodoItem({ item }: { item: ITodo }) {
const [todoList, setTodoList] = useRecoilState(todoListState);
const index = todoList.findIndex((listItem) => listItem === item);
const editItemText = ({
target: { value },
}: React.ChangeEvent<HTMLInputElement>) => {
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: ITodo[], index: number, newValue: ITodo) {
return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
}
function removeItemAtIndex(arr: ITodo[], index: number) {
return [...arr.slice(0, index), ...arr.slice(index + 1)];
}
위 코드에서 TodoItem 컴포넌트는 ITodo 인터페이스를 가진 item 프로퍼티를 받아서, 이를 화면에 보여주고, 해당 TodoItem을 수정하고나 완료 처리하거나 삭제할 수 있는 기능을 제공한다. 이 때, useRecoilState를 사용해 전역 상태인 todoListState를 관리한다.
replaceItemAtIndex 함수는 주어진 배열에서 주어진 인덱스에 해당하는 요소를 새로운 값으로 대체하는 함수이다.
removeItemAtIndex 함수는 배열에서 주어진 인덱스에 해당하는 요소를 제거하는 함수이다.
여기까지 작성했다면, 화면을 렌더링해보자. 지금까지 구현했던 주요 기능들이 정상적으로 동작하고 있음을 확인할 수 있다.
Selectors
Selector는 파생된 상태의 일부를 나타낸다. 파생된 상태를 어떤 방법으로든 주어진 상태를 수정하는 순수 함수에 전달된 상태의 결과물로 생각할 수 있다.
파생된 상태는 다른 데이터에 의존하는 동적인 데이터를 만들 수 있기 때문에 강력한 개념이다. todo 리스트 애플리케이션에서는 다음과 같은 것들이 파생된 상태로 간주된다.
- 필터링 된 todo 리스트 : 전체 todo 리스트에서 일부 기준에 따라 특정 항목이 필터링 된 새 리스트가 생성되어 파생된다.
- Todo 리스트 통계 : 전체 todo 리스트에서 목록의 총 항목 수, 완료된 항목 수, 완료된 항목의 백분율 같은 리스트의 유용한 속성을 계산하여 파생된다.
TodoListFilters
우선, 필터링 된 todo 리스트를 구현하기 위해서, atom에 저장될 수 있는 필터 기준을 선택해야 한다. 우리가 사용하게 될 필터 옵션은 "Show All", "Show Completed" 와 "Show Uncompleted"가 있다. 기본값은 "Show All"이 될 것이다.
const todoListFilterState = atom({
key: "todoListFilterState",
default: "Show All",
});
todoListFilterState
와 todoListState
를 사용해 우리는 필터링 된 리스트를 파생하는 filteredTodoListState
selector를 구성할 수 있다.
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;
}
},
});
filteredTodoListState
는 내부적으로 2개의 의존성 todoListFilterState
와 todoListState
을 추적한다. 그래서 둘 중 하나라도 변하면 filteredTodoListState는 리렌더링된다.
컴포넌트 관점에서 보면 selector는 아톰을 읽을 때 사용하는 같은 훅을 사용해서 읽을 수 있다. 그러나 특정한 훅은 쓰기 가능 상태 (즉, useRecoilState())에서만 작동하는 점을 유의해야 한다. 모든 아톰은 쓰기 가능 상태지만 selector는 일부만 쓰기 가능한 상태(get과 set 속성을 둘 다 가지고 있는 selector)로 간주된다.
그리고나서 필터링 된 todo 리스트를 표시하기 위해 TodoList 컴포넌트에서 한 줄을 수정해준다.
// const todoList = useRecoilValue(todoListState);
const todoList = useRecoilValue(filteredTodoListState);
그리고나서 TodoListFilters
컴포넌트를 구현해야한다.
function TodoListFilters() {
const [filter, setFilter] = useRecoilState(todoListFilterState);
const updateFilter = ({
target: { value },
}: React.ChangeEvent<HTMLSelectElement>) => {
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>
</>
);
}
컴포넌트에서 리코일 상태를 사용하기 위해 useRecoilState
훅을 사용한다. todoListFilterState
를 인자로 전달해 필터 상태를 가져오고, 상태를 업데이트 하는 데 사용할 setFilter
함수를 반환받는다.
UpdateFilter
함수는 드롭다운 선택 요소에서 발생하는 onChange
이벤트를 처리하기 위한 이벤트 핸들러이다. 이 함수는 React.ChangeEvent<HTMLSelectElement>
형식의 이벤트 객체를 인자로 받아온다. 이벤트 객체에서 value를 추출한 후, setFilter 함수를 호출하여 Recoil 상태를 업데이트한다.
TodoListFilters
컴포넌트는 렌더링할 때 드롭다운 선택 요소를 표시한다. 현재 선택된 필터 값을 value 속성에 바인딩하고, onChange 이벤트를 처리하기 위해 updateFilter 함수를 연결한다. 선택 요소에는 세 가지 옵션이 있다: "Show All" (모든 할 일 표시), "Show Completed" (완료된 할 일 표시), "Show Uncompleted" (미완료된 할 일 표시).
그리고나서 화면을 다시 렌더링해보면 필터링 기능이 표시됨을 확인할 수 있다.
TodoListStats
다음은 TodoListStats
컴포넌트를 구현하기 위해 동일한 개념을 사용할 것이다. 우리는 다음 통계를 표시할 것이다.
- todo 항목들의 총 개수
- 완료된 todo 항목들의 총 개수
- 완료되지 않은 todo 항목들의 총 개수
- 완료된 항목의 백분율
각 통계에 대해 selector를 만들 수 있지만, 필요한 데이터를 포함하는 객체를 반환하는 selector 하나를 만드는 것이 더 쉬운 방법일 것이다. 우리는 이 selector를 todoListStatsState
라고 부를 것이다.
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>
);
}
여기까지 작성하고 화면을 다시 렌더링해보면 다음과 같이 통계도 표시되는것을 확인할 수 있다.
'WEB > Recoil' 카테고리의 다른 글
리코일 사용해보기 - 1 [React/Recoil/typescript] (0) | 2023.03.29 |
---|