[React] Recoil - Todo 만들기

2022. 11. 11. 12:35·React/실험실
반응형

들어가며

이번에는 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
'React/실험실' 카테고리의 다른 글
  • [React] 벨로퍼트와 함께하는 React Testing - 개요
  • [React] Test Coverage
  • [React] Recoil - 사용하기
  • [React] Recoil - 시작하기
잉여개발자
잉여개발자
풀스택 개발자를 목표로 잉여롭게 개발 공부도 하면서 다양한 취미 생활도 즐기고 있는 잉여 개발자입니다.
  • 잉여개발자
    잉여로운 개발일지
    잉여개발자
    • 분류 전체보기 (789)
      • 개발정보 (36)
      • 개발환경 (7)
      • 개발생활 (19)
      • React (141)
        • 이론 (23)
        • 기능 (12)
        • 실험실 (88)
        • 버그 (6)
        • 패스트캠퍼스 (9)
        • Npm (3)
      • React Native (28)
        • 공통 (6)
        • TypeScript (3)
        • JavaScript (18)
        • 버그 (1)
      • Next.js (30)
        • 이론 (13)
        • 실험실 (13)
        • 버그 (3)
      • Web (35)
      • 알고리즘 (202)
        • 풀이 힌트 (39)
      • JavaScript (47)
      • TypeScript (29)
        • 기초 (27)
        • 실험실 (2)
      • Node.js (13)
        • 이론 (0)
        • 기능 (3)
        • 실험실 (9)
        • 버그 (1)
      • 도커 (4)
      • CCNA (22)
        • 이론 (4)
        • 문제 (18)
      • 취미생활 (167)
        • 잉여로운 칵테일 (2)
        • 잉여의 식물키우기 (130)
        • 잉여로운 여행기 (11)
        • 잉여의 제2외국어 (21)
        • 잉여로운 책장 (2)
      • Java (1)
        • Java의 정석 (1)
      • 꿀팁 공유 (3)
  • 태그

    Babel
    Docker
    next.js
    리액트
    타입스크립트
    영어회화
    react
    typescript
    리얼클래스
    redux
    자바스크립트
    바질
    javascript
    영어독학
    덤프
    리얼학습일기
    타일러영어
    CCNA
    알고리즘
    프로그래머스
    바질 키우기
    CSS
    ChatGPT
    다이소
    식물
    네이버 부스트캠프
    네트워크
    Node.js
    ReactNative
    webpack
  • hELLO· Designed By정상우.v4.10.1
잉여개발자
[React] Recoil - Todo 만들기
상단으로

티스토리툴바