본문 바로가기

React/실험실

[React] Recoil - Todo 만들기

들어가며

이번에는 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