Recoil - TodoList 만들어보기

IT공부중·2020년 5월 22일
7

Recoil

목록 보기
2/2

(이 글은 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 개발자들이 답변을 해놓은 것 같다. 신고해줘서 고맙고 이를 확인 해보겠다고 한다. 고쳐지면 다시 한번 수정해서 작성해볼 것이다!

요약

  • atom은 기본적인 global state 값을 생성 가능.
  • selector로 atom으로 생성한 state값들로 도출되는 값들을 state로 생성 가능.
  • useRecoilState, useSetRecoilState, useRecoilValue hook으로 state들을 가지고와 사용하는 것이 가능하다.
profile
4년차 프론트엔드 개발자 문건우입니다.

8개의 댓글

comment-user-thumbnail
2020년 6월 17일

Redux 세팅이 귀찮아서 useContext 쓰고 있었는데 Recoil이 더 편하네요 useState같이 쓰는 느낌
튜토리얼 감사합니다

1개의 답글
comment-user-thumbnail
2020년 8월 29일

export import를 통해 한 recoil state를 전역적으로 사용할 수도 있어서 redux보다 더 편한거 같네요.. 심지어 비동기까지!

답글 달기
comment-user-thumbnail
2022년 10월 25일

input의 value를 ref를 쓰면 타이핑할때마다 리렌더링 안시켜도 되는데 혹시 상태값으로 한 이유가 있는지궁금합니당

1개의 답글