(이 글은 recoil 0.0.7 버전으로 작성되어있습니다.)
Recoil을 좀 더 알아보기 위해서 Recoil 튜토리얼에 있는 TodoList 만들기를 내 방식대로 조금씩 바꿔서 만들어 볼 것이다.
src 밑에 recoil이라는 폴더를 만들어서 그안에 todo.js를 만들어주었다. 그리고 우리가 사용할 state를 만들어주었다.
src/recoil/toto.js
import { atom } from 'recoil';
export const todoListState = atom({
key: 'todoListState',
default: [],
});
그리고 todo라는 폴더를 만들어서 그 안에 jsx들을 만들어주었다.
src/todo/TodoList.jsx
import React from 'react';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';
import { useRecoilValue } from 'recoil';
import { todoListState } from '../recoil/todo';
const TodoList = () => {
const todoList = useRecoilValue(todoListState);
return (
<>
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
};
export default TodoList;
useRecoilValue hook을 이용해서 todoListState에서 만든 state의 값만을 불러올 수 있다. 그 값으로 map을 이용해 각각의 Item들을 생성해주었다.
그리고 나서 item들을 추가 할 수 있는 TodoItemCreator Component를 만들었다.
src/todo/TodoItemCreator.jsx
import React, { useState } from 'react';
import { todoListState } from '../recoil/todo';
import { useSetRecoilState } from 'recoil';
const TodoItemCreator = () => {
const [inputValue, setInputValue] = useState('');
const setTodoList = useSetRecoilState(todoListState);
// useSetRecoilState hook을 사용해 set함수만 가져올 수도 있다.
const addItem = () => {
setTodoList((oldTodoList) => {
const id = oldTodoList.length
? oldTodoList[oldTodoList.length - 1].id + 1
: 0; // oldTodoList에 원소가 있으면 그 원소 id + 1을 id로 하고 없으면 0을 id로 한다.
return [
...oldTodoList,
{
id,
text: inputValue,
isComplete: false,
},
];
});
setInputValue('');
};
const onChange = ({ target: { value } }) => {
setInputValue(value);
};
return (
<div>
<input type='text' value={inputValue} onChange={onChange} />
<button onClick={addItem}>Add</button>
</div>
);
};
export default TodoItemCreator;
useSetRecoilState hook을 사용해 set함수만 가져올 수도 있다.
input에 text를 적은다음 버튼을 누르면 추가되는 todolist를 볼 수 있을 것이다. 물론 아직 TodoItem Component를 안 만들어서 안 된다. 만들어보도록 한다!
src/todo/TodoItem.jsx
import React from 'react';
import { useRecoilState } from 'recoil';
import { todoListState } from '../recoil/todo';
const TodoItem = ({ item }) => {
const [todoList, setTodoList] = useRecoilState(todoListState);
const editItemText = ({ target: { value } }) => {
const newList = todoList.map((listItem) =>
listItem.id === item.id ? { ...listItem, text: value } : listItem
); // id가 같은 것은 text를 업데이트하고 아닌 것은 그대로 넣은 list를 만들어 set해줌.
setTodoList(newList);
};
const toggleItemCompletion = () => {
const newList = todoList.map((listItem) =>
listItem.id === item.id
? { ...listItem, isComplete: !item.isComplete }
: listItem
);
// id가 같은 것은 isComplete를 업데이트하고 아닌 것은 그대로 넣은 list를 만들어 set해줌.
setTodoList(newList);
};
const deleteItem = () => {
const newList = todoList.filter((listItem) => listItem.id !== item.id);
// id가 다른 것들만 filter하여 set해준다.
setTodoList(newList);
};
return (
<div>
<input type='text' value={item.text} onChange={editItemText} />
<input
type='checkbox'
checked={item.isComple
te}
onChange={toggleItemCompletion}
/>
<button onClick={deleteItem}>X</button>
</div>
);
};
export default TodoItem;
useRecoilState hook을 사용하면 앞에서 해본 useRecoilValue, useSetRecoilState를 같이 쓴 효과를 볼 수 있다. (뭔가 어떤건 setRecoil, 어떤건 value 하니깐 헷갈리는 감이...) useState를 사용하듯이 value와 setter를 받을 수 있다.
이렇게까지 작성해주면 기본적인 기능을 하는 TodoList를 만들었다!
하지만 atom 말고 selector라는 것을 사용해보기 위해 다른 기능도 한번 추가해보려고 한다!
selector는 state를 통해 도출된 값이라고 볼 수 있다. 예를들어 state의 길이를 구하는 함수를 만들어놓고 함수를 필요할 때 마다 state처럼 사용할 수 있는 것이다.
한번 만들어보면서 이해해보도록 한다!
src/recoil/todo.js
import { atom, selector } from 'recoil';
export const todoListState = atom({
key: 'todoListState',
default: [],
});
export const todoListFilterState = atom({
key: 'todoListFilterState',
default: 'Show All',
}); // 어떻게 필터할지 정하는 state
export const filteredTodoListState = selector({
key: 'filteredTodoListState',
// get에는 객체 안에 get 함수가 들어있는 파라미터를 받는다.
// get을 사용하여 state들을 불러올 수 있다. 어떤기준으로
// filter를 할지 state와 todoList state를 받아 기준에 따라 filter한다.
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;
}
},
}); // 필터 된 todoList를 반환해주는 selector
export const todoListStatsState = selector({
key: 'todoListStatsState',
get: ({ get }) => {
const todoList = get(filteredTodoListState);
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,
};
},
}); // todoList의 상태들을 계산해주는 selector
이런식으로 state들을 가공하는 selector를 만든다던지 state를 활용해 여러가지의 개수, 퍼센트를 구하는 selector를 만들어 사용할 수가 있다.
그럼 이 selector들을 활용해본다. filteredTodoListState로 화면이 보이게 하기 위해서 TodoList Component를 좀 바꿔주어야 한다.
src/todo/TodoList.jsx
import React from 'react';
import TodoListStats from './TodoListStats';
import TodoListFilters from './TodoListFilters';
import TodoItemCreator from './TodoItemCreator';
import TodoItem from './TodoItem';
import { useRecoilValue } from 'recoil';
import { filteredTodoListState } from '../recoil/todo';
const TodoList = () => {
const todoList = useRecoilValue(filteredTodoListState); // 필터된 state로 보이게한다!
return (
<>
<TodoListStats /> // 상태를 보여줄 컴포넌트
<TodoListFilters /> // 필터할 컴포넌트
<TodoItemCreator />
{todoList.map((todoItem) => (
<TodoItem key={todoItem.id} item={todoItem} />
))}
</>
);
};
export default TodoList;
그러면 TodoListStats와 TodoListFilters를 Component를 만들면 끝이난다.
src/todo/TodoListStats.jsx
import React from 'react';
import { useRecoilValue } from 'recoil';
import { todoListStatsState } from '../recoil/todo';
const TodoListStats = () => {
const {
totalNum,
totalCompletedNum,
totalUncompletedNum,
percentCompleted,
} = useRecoilValue(todoListStatsState);
let 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>
);
};
export default TodoListStats;
src/todo/TodoListFilters.jsx
import React from 'react';
import { useRecoilState } from 'recoil';
import { todoListFilterState } from '../recoil/todo';
const 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>
</>
);
};
export default TodoListFilters;
필요에 따라 useRecoilState 또는 useRecoilValue를 사용하여 불러와서 사용하였다. 그러면 이제
이렇게 잘 작동되는 TodoList를 볼 수 있을 것이다. 그런데 여기에 문제가 있다. 아직 Recoil이 나온지 얼마 되지 않아서 selector에 오류가 있는것 같다.
분명 튜토리얼 대로 따라했는데..? 뭘 잘 못했는지 몰라 구글링을 해보니 나랑 같은 현상을 겪는 분이 stackoverflow에 질문을 해놓았다.
https://github.com/facebookexperimental/Recoil/issues/12
답글에는 아마 recoil 개발자들이 답변을 해놓은 것 같다. 신고해줘서 고맙고 이를 확인 해보겠다고 한다. 고쳐지면 다시 한번 수정해서 작성해볼 것이다!
Redux 세팅이 귀찮아서 useContext 쓰고 있었는데 Recoil이 더 편하네요 useState같이 쓰는 느낌
튜토리얼 감사합니다