Next.js와 react-query로 todo앱 만들기 (useMutation)

jonyChoiGenius·2023년 4월 27일
0

React.js 치트 시트

목록 보기
19/22

useMutation는 훅이기 때문에 항상 함수형 컴포넌트에서 호출되어야 한다. 다른 함수 안에서 호출되는 것은 불가능하다.
useQuery도 마찬가지이지만,
useMutation는 특히 게시글의 생성, 수정, 삭제에서 사용하기 때문에 useEffect나 다른 이벤트 핸들러 안에서 사용하는 경우가 많은데...이렇게 되면 invald Hook을 만나게 된다.

useMutation에서 자주 쓰이는 메서드는 mutate이므로, useMutation으로부터 mutate를 반환하는 커스텀 훅을 만들어 보았다.

그리고, 해당 훅도 훅이기 때문에 함수형 컴포넌트에서 선언해서 mutate를 먼저 추출하고, 그 이후에 mutate를 다른 함수 안에서 쓰도록 유의해야 한다.

커스텀 훅

export const useCreateTodoMutate = () => {
  const mutationFn = (text: string) => TodosApi.post("", { text });
  const queryKey = QueryKeys.todos.list();
  
  const mutation = useMutation(mutationFn, {
    onSuccess: () => {
      useInvalidate(queryKey);
    },
  });
  return mutation.mutate;
};

여기서 사용된 useInvalidate는 아래와 같은 함수이다

export const useInvalidate = (queryKey: QueryKey, options = {}) =>
  queryClient.invalidateQueries(queryKey, options);

결과적으로 api를 실행하고, invalidate로서 업데이트하는 것을 한 세트로 묶은 Mutation을 실행하는 mutate를 반환하는 커스텀 훅인 것이다.

아래의 부분이 반복된다.

  const mutation = useMutation(mutationFn, {
    onSuccess: () => {
      useInvalidate(queryKey);
    },
  });
  return mutation.mutate;

이 부분을 역시나 services\index.ts로 따로 빼자
이때 mutationFn이 어떤 파라미터를 받는 함수인지 모르기 때문에 (...args: any[])로 파라미터 타입을 설정해준다.

export const getMutate = (
  queryKey: QueryKey,
  mutationFn: (...args: any[]) => Promise<AxiosResponse<any, any>>,
) => {
  const mutation = useMutation(mutationFn, {
    onSuccess: () => {
      useInvalidate(queryKey);
    },
  });
  return mutation.mutate;
};

사용

해당 커스텀 훅을 컴포넌트 내부에서 사용할 때에는 일단 커스텀 훅을 실행하여 mutate를 반환받고,
해당 mutate를 조건에 맞게 실행하는 방식을 사용한다.

  const createTodoMutate = useCreateTodoMutate();
  const onCreate = () => {
    createTodoMutate(input);
  };

Update

export const useUpdateTodoMutate = () => {
  const mutationFn = (todo: Todo) => TodosApi.put("", { ...todo });
  const queryKey = QueryKeys.todos.list();

  return getMutate(queryKey, mutationFn);
};
  const updateTodoMutate = useUpdateTodoMutate();
...
  <div
onClick={() => {
  updateTodoMutate({ ...e, completed: !e.completed });
}}
  >
    완료하기
  </div>

delete

export const useDestoryTodoMutate = () => {
  const mutationFn = (id: number) => TodosApi.delete(`?id=${id}`);
  const queryKey = QueryKeys.todos.list();

  return getMutate(queryKey, mutationFn);
};
const destroyTodoMutate = useDestoryTodoMutate();
...
  <div
onClick={() => {
  destroyTodoMutate(e.id);
}}
  >
    삭제하기
</div>

이렇게 리액트 쿼리 기본 세팅이 끝났다.

보일러 플레이트 정리

지금껏 만든 리액트 보일러 플레이트의 사용법을 정리하는 문서.
해당 문서를 정리하면서 일부 리팩토링을 했다.

서비스 폴더의 폴더 구조는 아래와 같다.

services
- index.ts : 쿼리 인스턴스와 유틸함수가 작성된 파일
- QueryKeys.ts : 쿼리 키를 만드는 팩토리
- todos
  -index.ts : todos에 사용되는 쿼리와 뮤테이션을 가진 파일

쿼리키 작성

services\QueryKeys.ts 파일은 리액트의 쿼리 키를 저장하는 팩토리이다.

const QueryKeys = {
  todos: {
    list: () => ["todo-list"] as const,
    retrieve: (TodoId: number) => [...QueryKeys.todos.list(), TodoId] as const,
  },
  musics: {
    list: () => ["movies"] as const,
    retrieve: (musicId: number) =>
      [...QueryKeys.musics.list(), musicId] as const,
    tracks: {
      list: () => [...QueryKeys.musics.list(), "tracks"] as const,
    },
  },
};

export default QueryKeys;
  1. QueryKeys라는 객체 안에, 조회하고자 하는 관심사명으로 객체를 만든다.

    가령 todos라는 객체를 만든다.

  1. 해당 객체에 메서드를 선언한다. 해당 메서드는 필요한 파라미터를 받은 후, 쿼리키 배열을 반환한다.

    가령 QueryKeys.todos.retrieve는 TodoId라는 숫자를 입력받아 [”todo-list”, 숫자]를 반환한다.

  1. 메서드에서 배열을 반환할 때, 상위의 쿼리키가 있다면 해당 쿼리키를 반환하는 메서드를 가져다 쓴다.

    가령 QueryKeys.todos.retrieve가 [”todo-list”, 숫자]를 반환할 때, [...QueryKeys.todos.list(), TodoId]와 같이 메서드를 호출하여 [”todo-list”, 숫자]를 만든다.

  1. 하위에 또 다른 관심사가 있는 경우, 하위에 또 다른 객체를 만든다. 가령 musics라는 객체가 tracks라는 프로퍼티들을 가지고 있다면, musics : { tracks : {} } 와 같은 형식으로 만든다.

위와 같은 방식을 통해 쿼리키들간의 계층 관계를 추론할 수 있게 된다.

리액트 쿼리 인스턴스 / 커스텀 훅

services\index.ts에는 리액트 쿼리 인스턴스와 공용으로 쓰일 수 있는 유틸 함수들이 저장되어 있다.

  1. queryClient
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: Infinity,
    },
  },
});

쿼리 클라이언트에서 기본 설정을 변경할 수 있다.

데이터가 캐싱되고 유지되도록 staleTime을 Infinity로 하였다.

  1. useInvalidate
export const useInvalidate = (queryKey: QueryKey, options = {}) =>
  queryClient.invalidateQueries(queryKey, options);

useInvalidate는 쿼리키를 입력받으면 해당 쿼리를 즉시 invalidated 상태로 만드는 커스텀 훅이다.

  1. useQueryResult
export const useQueryResult= (
  queryKey,
  queryFn,
  options={},
): CustomQueryHookReturnType => {
  const query = useQuery(queryKey, queryFn, options);
  return [query.data?.data, query.refetch, query];
};

useQueryResult는 쿼리키쿼리함수를 입력받아 useQuery를 생성하는 함수이다.

반환값은 쿼리객체에서 data와 refetch를 별도로 분리하여 [data, refetch, 쿼리객체]의 배열 형식으로 반환한다.

해당 커스텀 훅이 없을 때에는

const { data: moviesData, refetch: moviesRefetch }= useQuery(queryKey, queryFn, options)

위와 같이 쿼리 객체를 구조분해 할당을 해야 하지만,

커스텀 훅을 사용하면

const [moviesData, moviesRefetch] = useQueryResult(queryKey, queryFn, options)

위와 같이 배열을 구조분해할당하여 사용할 수 있다.

  1. useCustomMutate
export const useCustomMutate = (
  queryKey,
  mutationFn,
) => {
  const mutation = useMutation(mutationFn, {
    onSuccess: () => {
      useInvalidate(queryKey);
    },
  });
  return mutation.mutate;
};

useCustomMutate는 쿼리키쿼리 함수를 입력받아 useMutation을 생성한다.

반환값은 mutate이다.

컴포넌트 내부에서는 아래와 같이 사용한다.

const retriveMovies = useCustomMutate(쿼리키, 쿼리함수)
const onClick = ()=> retriveMovies()

쿼리 목록

services\todos\index.ts 는 todos에 쓰이는 쿼리 목록을 저장해둔 것이다. 이와 같이 services 폴더 하위에 관심사에 따라 폴더를 만들어 작성한다.]

아래는 투두 리스트에서 하나의 값을 가져오는 쿼리이다.

export const retrieveTodoQuery: CustomQueryHookType<number, Todo> = (
  selectedId,
  options = {},
) => {
	const queryKey = QueryKeys.todos.retrieve(selectedId);
  const queryFn = () => TodosApi.get(`?id=${selectedId}`);

  return useQueryResult(queryKey, getQueryFn(selectedId), options);
};

retrieveTodoQuery는 selectedId와 options를 입력받아 useQueryResult의 결과를 반환한다.

CustomQueryHookType<입력값타입, 반환값타입>은 retrieveTodoQuery의 입력값과 반환값의 타입을 지정할 수 있다. 입력값 타입은 파라미터의 첫번째 인자의 타입이고, 반환값타입은 반환되는 배열의 첫번째 요소의 타입이다.

getQueryFn은 파라미터를 입력받아 해당 파라미터에 맞는 쿼리함수를 생성해주는 함수이다.

아래는 새로운 투두를 생성하는 뮤테이션이다.

export const useCreateTodoMutate = () => {
  const mutationFn = (text: string) => TodosApi.post("", { text });
  const queryKey = QueryKeys.todos.list();

  return useCustomMutate(queryKey, mutationFn);
};

해당 뮤테이션이 그 자체로 뮤테이트를 실행하지 않고, 뮤테이트를 실행할 수 있는 함수를 반환한다는 점을 강조하기 위해 ‘use’라는 접두사를 붙여주었다.

사용하기

컴포넌트 내부에서 사용할 때에는 아래와 같다.

//useQuery로 전체 데이터 불러오기
const [todoListData, todoListDataRefetch] = listTodoQuery();

//선택된 데이터가 바뀔 때마다 useQuery로 하나씩 불러오기
const [selectedId, setSelectedId] = useState(0);
const [TodoData] = retrieveTodoQuery(selectedId, {
  keepPreviousData: true,
});

//클릭 이벤트가 발생할 때 useMutation으로 실행하기
const createTodoMutate = useCreateTodoMutate();
const onCreate = () => {
  createTodoMutate(input);
};
profile
천재가 되어버린 박제를 아시오?

0개의 댓글