2024.03.07 TIL - 트러블슈팅(tanstack-query mutation queryKey부분 오류, new Date type오류), 캐싱된 query 사용하기, custom hook생성

Innes·2024년 3월 7일
1

TIL(Today I Learned)

목록 보기
84/147
post-thumbnail

🏹 트러블슈팅

mutation에서 queryKey부분 오류

  • 오류 메시지 : "todos"유형에 'InvalidateQueryFilters'유형과 공통적인 속성이 없습니다.

  • react-query 3.xx 버전 쓰는 다른 분과 비교해보니 그 분은 이런 Type에러가 안생기고 있었다...! 뭔가 이상하다

  • 문제의 코드

// InputBox.tsx
 const queryClient = useQueryClient();
 const mutation = useMutation({
    mutationFn: (newTodo: Todo): Promise<void> => addTodo(newTodo),
    onSuccess: () => {
      // ⭐️⭐️ invalidateQueries안에 ""형식으로 queryKey 넣어줌
      queryClient.invalidateQueries("todos");
    },
  });

// TodoItem.tsx

 const queryClient = useQueryClient();

  const { mutate: deleteTodoItem } = useMutation({
    mutationFn: (id: string): Promise<void> => deleteTodo(id),
    onSuccess: () => {
      // ⭐️⭐️ 위와 마찬가지 1
      queryClient.invalidateQueries("todos");
    },
  });

  const { mutate: toggleTodoItem } = useMutation({
    mutationFn: (todo: Todo): Promise<void> => toggleTodo(todo),
    onSuccess: () => {
      // ⭐️⭐️ 위와 마찬가지 2
      queryClient.invalidateQueries("todos");
    },
  });
  • 원인 : tanstack-query에서는 invalidateQueries에 queryKey를 넣는 문법이 달라졌던 것!!!

  • 해결 : queryKey 넣어줄때 {queryKey: ["todos"]} 형태로 넣어주기!

// TodoLists.tsx

 const {
    data: todos,
    isLoading,
    isError,
   // ⭐️ useQuery에서 queryKey 생성
  }: TodosType = useQuery({
    queryKey: ["todos"],
    queryFn: getTodos,
  });

// InputBox.tsx

  const queryClient = useQueryClient();

  const mutation = useMutation({
    mutationFn: (newTodo: Todo): Promise<void> => addTodo(newTodo),
    onSuccess: () => {
      queryClient.invalidateQueries({
        // ⭐️⭐️⭐️⭐️⭐️ tanstack-query에서 바뀐 문법!!
        queryKey: ["todos"],
      });
    },
  });

// 
  • "todos"로 작성했을때도 비록 type에러는 났어도 동작은 잘 했었다?!
    -> tanstack: "그렇게 쓰는 형식은 이전 버전의 형식이어서, 동작하는 것 정도는 지원해주겠는데 type은 틀렸다고 에러를 보여줄거야."

  • 확인 방법 1. 공식 문서에도 바뀌어있다.
    https://tanstack.com/query/latest/docs/framework/react/guides/query-invalidation

  • 확인 방법 2. typescript는 vscode에서 ctrl(command) + 클릭 시 지원하는 타입을 확인할 수 있다.

    • InvalidateQueryFilters유형이 뭐지? -> invalidateQueries ctrl + 클릭

    • invalidateQueries 안에 InvalidateQueryFilters가 있구나! -> ctrl + 클릭

    • InvalidateQueryFilters는 QueryFilters로부터 나왔구나! -> QueryFilters 클릭

    • QueryFilters안에 queryKey라는 속성이 있고, 걔는 QueryKey를 받는구나 -> QueryKey 클릭

    • QueryKey에는 Array(배열)가 들어오는구나.

      => queryKey: [ ]가 들어온다는걸 알 수 있다!


new Date: string으로 정렬시 Type오류

  • 문제 : typescript에서 new Date를 사용하는 경우 종종 type오류가 생긴다고 한다.
// 문제의 코드
const sortedTodos = todos
    ? [...todos].sort((a, b) => {
        if (sortOrder === "asc") {
          return new Date(b.deadline) - new Date(a.deadline);
        }
        return new Date(a.deadline) - new Date(b.deadline);
      })
    : [];
  • 해결 : new Date를 인위적으로 계산가능한 숫자처럼 형식을 바꿔주는 과정이 필요!
// TodoLists.tsx

  const sortedTodos = todos
    ? [...todos].sort((a, b) => {
        // ⭐️ new Date를 변수로 선언
        const deadlineA = new Date(a.deadline);
        const deadlineB = new Date(b.deadline);
      
        // ⭐️ 변수 앞에 + 붙여주기! -> 계산 가능한 숫자형식으로 인식함
        if (sortOrder === "asc") {
          return +deadlineB - +deadlineA;
        }
        return +deadlineA - +deadlineB;
      })
    : [];

미리 캐싱된 query를 사용해보자

  • 캐싱된 데이터 가져오기!! getQueryData(["queryKey"])
  • 반환값이 [queryKey: QueryKey, data: TQueryFnData | undefined][]이렇게 특이한 형식으로 나와서 배열 2개를 한번에 구조분해할당 해야됨...
import { useParams } from "react-router-dom";
import { useQueryClient } from "@tanstack/react-query"; // queryClient를 가져옵니다.

const Detail = () => {
  const queryClient = useQueryClient(); // queryClient를 가져옵니다.
  const { id } = useParams<{ id: string }>(); // URL 파라미터로부터 id를 가져옵니다.

  // queryClient에서 Todos 데이터를 가져옵니다.
  // ⭐️⭐️⭐️ 캐싱된 데이터 가져오기!! getQueryData(queryKey: ["queryKey"])
  // 배열 2개 구조분해할당 한 이유 : getQueriesData의 반환값이 queryKey배열, data배열이기때문
  const [[queryKey, todos]] = queryClient.getQueriesData({
    queryKey: ["todos"],
  });

  // 해당 ID에 해당하는 Todo를 찾습니다.
  const todoItem = todosData?.find(todo => todo.id === id);

  if (!todoItem) {
    return <div>Todo가 없습니다.</div>;
  }

  return (
    <>
      <St.TodoList>
        <St.TodoListBody>
          <St.Span>{todoItem.title}</St.Span>
          <p>{todoItem.content}</p>
          <St.Time>
            {new Date(todoItem.deadline).toLocaleDateString("ko-KR", {
              year: "numeric",
              month: "long", // "long"을 사용하면 월 이름이 됨
              day: "numeric",
            })}
            까지
          </St.Time>
        </St.TodoListBody>
      </St.TodoList>
    </>
  );
};

export default Detail;
  • 캐싱된 query를 사용하는 것의 문제점
    • home에서 useQuery로 캐싱해놓은 데이터라서, detail페이지에서 그 데이터를 사용할 경우 detail페이지를 새로고침하면 에러발생
    • why?! : home에서 detail로 넘어오는게 아니라 처음부터 detail에서 페이지를 렌더링하는 경우, 캐싱해놓은 데이터가 없다고 인식하니까
  • 따라서, 차라리 detail페이지에서 useQuery로 todos를 한번더 api통신으로 가져오는게 낫다는 결론... 다시 바꾸자... ^^;

custom hook 생성하기

  • 여러 컴포넌트에서 공통되는 로직이 반복된다면, 하나의 hook으로 묶은 다음 그걸 가져다 쓸 수 있다.
  • 코드의 재사용성, 간결한 코드 작성에 효과적이다!
  • 형식
// custom hook 만들기
export const hook이름 = () => {
	const 실행로직 = blah blah
    return { 실행로직 }
    // 혹은 로직 실행 후의 값 등등 컴포넌트에서 가져다 쓸 값을 return으로 반환해주기
}

// 컴포넌트에서 사용하기
const { 실행로직 } = hook이름();
// '실행로직' 혹은 다른 return 반환값 중 사용할 데이터를 가져다 쓰면 끝!
  • 예시
// 🧡 기존 - useQuery로 todos 데이터 가져오기

import { useQuery } from "@tanstack/react-query";
import { getTodos } from "../api/todos-api";
import { Todo } from "../types/Todos";

interface TodosType {
  data: Todo[] | undefined;
  isLoading: boolean;
  isError: boolean;
}

// .. 생략
 const {
    data: todos,
    isLoading,
    isError,
  }: TodosType = useQuery({
    queryKey: ["todos"],
    queryFn: getTodos,
  });

  if (isLoading) {
    return <div>Loading...</div>;
  }
  if (isError) {
    return <div>Error</div>;
  }
// ..생략
// 💚 custom hook 사용

// hook - useTodoQuery()
import { useQuery } from "@tanstack/react-query";
import { getTodos } from "../api/todos-api";
import { Todo } from "../types/Todos";

export interface TodosType {
  data: Todo[] | undefined;
  isLoading: boolean;
  isError: boolean;
}

// useTodoQuery 생성 시작
export const useTodoQuery = () => {
  const {
    data: todos,
    isLoading,
    isError,
  }: TodosType = useQuery({
    queryKey: ["todos"],
    queryFn: getTodos,
  });

  // 컴포넌트에서 가져다 쓸 값들 반환해주기
  return { todos, isLoading, isError };
};


// 💚 컴포넌트에서 사용하기 (TodoList.tsx)
  const { todos, isLoading, isError } = useTodoQuery();
  • 예시 2
// 🧡 기존 - 연달아 작성된 여러개의 useMutation

import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteTodo, toggleTodo, updateTodo } from "../api/todos-api";

   // .. 생략
   const queryClient = useQueryClient();

    const { mutate: deleteTodoItem } = useMutation({
      mutationFn: (id: string): Promise<void> => deleteTodo(id),
      onSuccess: () => {
        queryClient.invalidateQueries({
          queryKey: ["todos"],
        });
      },
    });

    const { mutate: toggleTodoItem } = useMutation({
      mutationFn: (todo: Todo): Promise<void> => toggleTodo(todo),
      onSuccess: () => {
        queryClient.invalidateQueries({
          queryKey: ["todos"],
        });
      },
    });

    const { mutate: updateTodoItem } = useMutation({
      mutationFn: ({
        todo,
        newTitle,
        newContent,
      }: {
        todo: Todo;
        newTitle: string;
        newContent: string;
      }): Promise<void> => updateTodo(todo, newTitle, newContent),
      onSuccess: () => {
        queryClient.invalidateQueries({
          queryKey: ["todos"],
        });
      },
    });

  // ..생략
  
const removeHandler = (id: string): void => {
    deleteTodoItem(id);
  };

const reLocateHandler = (todo: Todo): void => {
    toggleTodoItem(todo);
  };

const onEditHandler = (e: React.FormEvent<HTMLFormElement>): void => {
    updateTodoItem({ todo, newTitle, newContent });
  };
// 💚 custom hook 사용

// hook - useTodoMutation()
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteTodo, toggleTodo, updateTodo } from "../api/todos-api";
import { Todo } from "../types/Todos";

interface UpdateTodoType {
  todo: Todo;
  newTitle: string;
  newContent: string;
}

// useTodoMutation 생성 시작
export const useTodoMutation = () => {
  const queryClient = useQueryClient();

  const { mutate: deleteTodoItem } = useMutation({
    mutationFn: (id: string): Promise<void> => deleteTodo(id),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  const { mutate: toggleTodoItem } = useMutation({
    mutationFn: (todo: Todo): Promise<void> => toggleTodo(todo),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  const { mutate: updateTodoItem } = useMutation({
    mutationFn: ({
      todo,
      newTitle,
      newContent,
    }: UpdateTodoType): Promise<void> => updateTodo(todo, newTitle, newContent),
    onSuccess: () => {
      queryClient.invalidateQueries({
        queryKey: ["todos"],
      });
    },
  });

  // 컴포넌트에서 가져다 쓸 데이터 반환해주기
  return { deleteTodoItem, toggleTodoItem, updateTodoItem };
};



// 💚 컴포넌트에서 사용하기 (Detail.tsx)
  const { deleteTodoItem, toggleTodoItem, updateTodoItem } = useTodoMutation();

    // ..생략
  
    const removeHandler = (id: string): void => {
    deleteTodoItem(id);
  };

    const reLocateHandler = (todo: Todo): void => {
    toggleTodoItem(todo);
  };

    const onEditHandler = (e: React.FormEvent<HTMLFormElement>): void => {
    updateTodoItem({ todo, newTitle, newContent });
  };
profile
꾸준히 성장하는 우상향 개발자

0개의 댓글