프로젝트 이모저모

[투두리스트 버그] 전역 상태 값이 바로 변경되어 렌더링 되지 않음

Ella Seon 2023. 8. 1. 18:11

0. 버그 현황

- 공유하기 버튼을 누르면 내가 한 일들만 필터링이 돼서 목록이 뜬다. 이 목록들을 친구에게 카톡으로 공유할 수 있다.

- 하지만, 뒤로가기 버튼을 누르고 안 한 일들 나머지를 다시 체크하고 공유버튼을 눌렀더니 상태가 그대로 되어있다..

- 상태값이 바로바로 변경되어서 렌더링이 되지 않고있다. 

 

1. 문제 코드

- todo 컴포넌트에서 todoItem 을 리코일을 이용해서 전역상태로 관리했다.

- todoItem 빈 배열에서 get API 를 불러와서 setTodoItem 으로 상태를 업데이트 해주었다. 

function Todo() {
  const [todoItem, setTodoItem] = useRecoilState<TodoItem[]>(todoItemState);
  const [isLoading, setIsLoading] = useState(true);
  const setToken = useSetRecoilState(tokenState);

  const navigate = useNavigate();


  const getTodo = async () => {
    try {
      const todoRes = await customAuthAxios.get("todos");
      if (todoRes) {
        setTodoItem(todoRes.data);
        setIsLoading(false);
      }
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    getTodo();
  }, []);


  if (isLoading) {
    return <Loading />;
  } else {
    return (
      <div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
        <aside className="w-98 text-right mr-5 mb-5">
          <KakaoShare />
        </aside>
        <section className="bg-main_bg_cloud max-w-7xl w-98 rounded-xl h-600 relative">
          <div className="sticky top-0 pb-5 rounded-t-xl bg-main_bg_cloud ">
            <h1
              className="font-mono pl-10 pt-9 text-3xl font-semibold cursor-pointer"
              onClick={handleRefresh}
            >
              Today
            </h1>
            <p className="font-mono  pl-10 pt-3 text-sm">
              What are you working on today?
            </p>
          </div>
          <ul className="h-fit max-h-450 pt-5 pb-5 pr-10 pl-10 grid grid-cols-2 gap-4 overflow-y-scroll">
            {todoItem.map((postIt) => {
              return (
                <PostItem
                  key={postIt.id}
                  todoId={postIt.id}
                  todoList={todoItem}
                  setTodoList={setTodoItem}
                  isCompleted={postIt.isCompleted}
                >
                  {postIt.todo}
                </PostItem>
              );
            })}
          </ul>
        </section>
     
      </div>
    );
  }
}

export default Todo;
// atoms.ts

export const todoItemState = atom<TodoItem[]>({
  key: "todoItemState",
  default: [],
  effects_UNSTABLE: [persistAtom],
});

 

- Todo 컴포넌트에서 공유하기 버튼을 Result 컴포넌트 페이지로 이동하는데, 마찬가지로 리코일에서 todoItem 을 불러와서 이 todoItem 에서 filter 를 적용해서 보여주는 것이다. 헌데, handleMovePreviousPage 이벤트 핸들러가 발동되어 이전 페이지에 가서 다시 isCompleted 를 false 를 하고 다시 공유버튼을 눌렀을 경우, completedTodos 에서 filter 가 제대로 되지 않고 있었다. 

function Result() {
  const navigate = useNavigate();
  const [todoItem] = useRecoilState<TodoItem[]>(todoItemState);

  const completedTodos = todoItem.filter((item) => {
    return item.isCompleted === true;
  });


  const handleMovePreviousPage = () => {
    navigate(-1);
  };

  return (
    <div className="bg-main_skyblue flex flex-col justify-center items-center h-screen">
      <section className="bg-main_bg_cloud max-w-7xl w-98 rounded-xl h-600 relative">
        <div className="sticky top-0 pb-5 rounded-t-xl bg-main_bg_cloud ">
          <h1 className=" text-center pt-9 text-3xl font-semibold ">
            오늘 한 일 목록 공유하기
          </h1>
        </div>
        <ul className="h-fit max-h-450 pt-11 pb-5 pr-10 pl-10 grid grid-cols-2 gap-4 overflow-y-scroll">
          {completedTodos.map((postIt) => {
            return (
              <ResultPostItem key={postIt.id} timeTypes={postIt.todo.slice(-1)}>
                {postIt.todo.slice(0, -1)}
              </ResultPostItem>
            );
          })}
        </ul>
      </section>
      <div className="flex flex-col gap-y-3 mt-5">
        <Button size="large" onClick={handleShareKaKao}>
          카카오톡으로 공유하기
        </Button>
        <Button size="large" onClick={handleMovePreviousPage}>
          뒤로가기
        </Button>
      </div>
    </div>
  );
}

 

2. 해결 방법

- Result 컴포넌트에서 console.log(todoItem) 을 해서 살펴보았다.

- 한개만 true 로 만들었다. 

- 다시 뒤로가기 버튼을 누른 후 모든 todoItem 의 isCompleted를 true 로 변경하였다. 

useRecoilState를 사용해서 상태를 가지고 오고있는데 Result 컴포넌트가 리코일 상태 변경을 바로바로 감지하지 못하고 있었다.

 

 

이유는 각각의 todoItem은 get API 를 불러와야지 setTodoItem 이 발동되어서 상태가 변경이 된다.

Result 컴포넌트에서 뒤로가기를 누르고 Todo 컴포넌트 페이지에서 다시 isCompleted 상태 변경을 하고 handleMoveToResultPage 이벤트 핸들러가 발동하면 get API 가 불러와지지 않았기 때문에 상태 변경이 되지않는다. 

 

get API 를 불러올 때 상태 변경이 되는게 아니라 isCompleted 만 바꿨을 때도 상태 변경이 되게 해야한다.

isCompleted 는 postItem 컴포넌트에서 변경하고 있다.

 

handleTodoCompleted 함수가 실행이 될 때 todoItem 전역 상태의 isCompleted 도 상태 변경이 되게 하였다. 

function PostItem({
  children,
  todoId,
  todoList,
  setTodoList,
  isCompleted,
}: PostItemProps) {
  const len = children.length;
  const timeType = children.slice(-1);
  const content = children.slice(0, len - 1);

  const [, setTodoItem] = useRecoilState(todoItemState);
  const [updateToggle, setUpdateToggle] = useState(false);
  const [updatedContent, setUpdatedContent] = useState(content);
  const [isCompletedTodo, setIsCompletedTodo] = useState(isCompleted);
  const todoInput = useRef<HTMLTextAreaElement>(null);

  const handleTodoDelete = () => {
    const deleteConfirm = confirm("정말로 삭제하시겠습니까?");

    if (deleteConfirm) {
      const deleteTodo = async () => {
        try {
          await customAuthAxios.delete(`todos/${todoId}`);
          setTodoList(
            [...todoList].filter((todoItem) => todoItem.id !== todoId)
          );
        } catch (error) {
          console.log(error);
        }
      };
      deleteTodo();
    }
  };

  const handleTodoUpdate = async () => {
    if (updateToggle) {
      const updateData = {
        todo: updatedContent + timeType,
        isCompleted: isCompletedTodo,
      };

      try {
        await customAuthAxios.put(`todos/${todoId}`, updateData);
      } catch (error) {
        console.log(error);
      }
    }
    setUpdateToggle((prev) => !prev);
  };

  const handleTodoCompleted = async () => {
    const updateData = {
      todo: updatedContent + timeType,
      isCompleted: !isCompleted,
    };
    try {
      await customAuthAxios.put(`todos/${todoId}`, updateData);
      setIsCompletedTodo((prev) => !prev);
      setTodoItem((prevTodos) =>
        prevTodos.map((todo) =>
          todo.id === todoId ? { ...todo, isCompleted: !isCompleted } : todo
        )
      );
    } catch (error) {
      console.log(error);
    }
  };

  useEffect(() => {
    todoInput.current?.focus();
  }, [updateToggle]);

  return (
    <li className="font-semibold tracking-widest relative h-fit">
      <div
        className={`${
          timeType === "1"
            ? "bg-post_red"
            : timeType === "2"
            ? "bg-post_yellow"
            : "bg-post_blue"
        } w-50 h-32 p-1 shadow shadow-black break-all`}
      >
        {!updateToggle ? (
          <p
            className={`${isCompletedTodo ? "line-through" : ""} h-24`}
            onClick={handleTodoCompleted}
          >
            {updatedContent}
          </p>
        ) : (
          <textarea
            ref={todoInput}
            className="w-32 h-24 bg-transparent tracking-widest resize-none"
            value={updatedContent}
            onChange={(e) => {
              setUpdatedContent(e.target.value);
            }}
            maxLength={30}
          />
        )}
        <button
          onClick={handleTodoDelete}
          className="absolute right-2 bottom-1"
        >
          <FontAwesomeIcon
            icon={faTrashCan}
            className="cursor-pointer hover:opacity-80"
            opacity={0.2}
          />
        </button>
        <button onClick={handleTodoUpdate} className="absolute left-2 bottom-1">
          <FontAwesomeIcon
            icon={!updateToggle ? faPenToSquare : faCheck}
            className="cursor-pointer hover:opacity-80"
            opacity={0.2}
          />
        </button>
      </div>
    </li>
  );
}

export default PostItem;