본문 바로가기

FrontEnd

Recoil로 Todo List 만들기.

반응형

https://recoiljs.org/ko/docs/basic-tutorial/intro

 

도입부 | Recoil

이 섹션은 Recoil과 React를 설치했다고 가정한다. Recoil과 React를 처음부터 시작하는 방법은 Getting Started를 보면된다. 앞으로의 섹션의 컴포넌트들은 부모트리에 ``가 있다고 가정한다.

recoiljs.org

recoil

리코일 공식 문서를 그대로 따라한 코드이다.

배울만한 점을 복기해본다.

1. TodoList

가장 최상단의 컴포넌트이다.

todoListState Atom을 갖고 있다.

또한 List+Filter Atom을 조합해 파생한 filteredTodoListState를 화면에 보여준다.

즉 사용자가 보는 데이터는 filtered된 리스트 데이터지만, 해당 컴포넌트는 필터 이전의 List를 갖고 있다.

또한 todoListState 혹은 todoListFilterState가 변경되면 해당 컴포넌트는 리렌더링 된다.

export interface Todo {
  id: number;
  text: string;
  isComplete: boolean;
}
export const todoListState = atom<Todo[]>({
  key: "todoListState",
  default: []
});

const filteredTodoListState = selector({
  key: "FilteredTodoList",
  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 default function TodoList() {
  // 동시에 읽고 쓰기 위함.
  const todoList = useRecoilValue(filteredTodoListState);

  return (
    <>
      <TodoListStats />
      <TodoListFilters />
      <TodoItemCreator />

      {todoList.map((todoItem) => (
        <TodoItem key={todoItem.id} item={todoItem} />
      ))}
    </>
  );
}

2. TodoListStats

TodoList 컴포넌트에 있던 todoListState를 이용해 통계 데이터를 조합하여 화면에 뿌려준다.

해당 컴포넌트는 todoListState에 의존하여 해당 데이터가 변경되면 자동으로 갱신된다.

export const todoListStatsState = selector({
  key: "TodoListStats",
  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) * 100;

    return {
      totalNum,
      totalCompletedNum,
      totalUncompletedNum,
      percentCompleted
    };
  }
});
/**
 * Todo list statistics: derived from the complete todo list
 * by calculating useful attributes of the list,
 * such as the total number of items in the list,
 * the number of completed items, and the percentage of items that are completed.
 */
export default function TodoListStats() {
  const {
    totalNum,
    totalCompletedNum,
    totalUncompletedNum,
    percentCompleted
  } = useRecoilValue(todoListStatsState);

  const formattedPercentCompleted = Math.round(percentCompleted);

  return (
    <ul>
      <li>Total items: {totalNum}</li>
      <li>Items completed: {totalCompletedNum}</li>
      <li>Items not completed: {totalUncompletedNum}</li>
      <li>Percent completed: {formattedPercentCompleted}</li>
    </ul>
  );
}

3. TodoListFilter

필터를 변경하면, 해당 필터를 구독하는 1.TodoList는 filteredTodoListState에 의해 리렌더링된다.

type FilterState = "Show Completed" | "Show All" | "Show Uncompleted";
export const todoListFilterState = atom<FilterState>({
  key: "todoListFilterState",
  default: "Show All"
});

export default function TodoListFilters() {
  const [filter, setFilter] = useRecoilState(todoListFilterState);
  const updateFilter: React.ChangeEventHandler<HTMLSelectElement> = ({
    target: { value }
  }) => {
    // upcasting always typesafe
    setFilter(value as FilterState);
  };

  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>
    </>
  );
}

4. TodoItemCreator

해당 컴포넌트는 todoListState 아톰을 useSetRecoilState 만을 이용하여 접근한다.

이는 todoListState가 변경되어도 해당 컴포넌트가 리렌더링 되지 않음을 의미한다.

(React.memo 등은 고려해야 한다)

// 고유한 Id 생성을 위한 유틸리티
let id = 0;
function getId() {
  return id++;
}

export default function TodoItemCreator() {
  const [inputValue, setInputValue] = useState("");
  // useSetRecoilState()을 사용하는 것은 컴포넌트가 값이 바뀔 때 리렌더링을 하기 위해 컴포넌트를 구독하지 않고도 값을 설정하게 해줍니다.
  const setTodoList = useSetRecoilState(todoListState);

  const addItem = () => {
    // 기존 todo 리스트를 기반으로 새 todo 리스트를 만들 수 있도록 setter 함수의 updater 형식을 사용한다는 점에 유의해야 한다.
    setTodoList((oldTodoList) => [
      ...oldTodoList,
      {
        id: getId(),
        text: inputValue,
        isComplete: false
      }
    ]);
    setInputValue("");
  };

  const onChange: React.ChangeEventHandler<HTMLInputElement> = ({
    target: { value }
  }) => {
    setInputValue(value);
  };

  return (
    <div>
      <input type="text" value={inputValue} onChange={onChange} />
      <button onClick={addItem}>Add</button>
    </div>
  );
}

5. TodoItem

todoListState의 Update, Read, Delete 책임을 갖고 있는 컴포넌트다. 

const replaceItemAtIndex = (
  arr: Todo[],
  index: number,
  newValue: Todo
): Todo[] => {
  return [...arr.slice(0, index), newValue, ...arr.slice(index + 1)];
};

const removeItemAtIndex = (arr: Todo[], index: number): Todo[] => {
  return [...arr.slice(0, index), ...arr.slice(index + 1)];
};

export default function TodoItem({ item }: { item: Todo }) {
  const [todoList, setTodoList] = useRecoilState(todoListState);
  const index = todoList.findIndex((listItem) => listItem === item);

  const editItemText: React.ChangeEventHandler<HTMLInputElement> = ({
    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>
  );
}

 

요약 :

Read 갱신이 필요 없는 컴포넌트의 경우 useSetRecoilState 훅을 사용한다.

작은 Atom를 조합하여 큰 Selector를 만든다.

반응형