BLOGPOSTSABOUT

WIL | 블로그 댓글 기능 추가

2025-08-09

Giscus로 블로그 댓글 기능 추가하기

사용자의 api 중복 호출은 UI 단에서 isLoading을 통해 처리할 수 있지만 스크립트 등을 통한 중복 요청이 들어오는 경우 해당 횟수만큼 api가 호출됩니다. 이를 방지하기 위해 React Query 기반의 커스텀 훅을 구현했습니다.


⁉️ 기존 방식(ref, throttle)의 문제점

  1. ref를 통해 isDisabled를 관리
    mutation을 사용하는 곳마다 코드를 작성해주어야함

  2. throttle이나 debounce로 컨트롤
    일정 시간동안 api 호출을 막는 것이기 때문에 지정해둔 시간이 되기 전까지 추가적인 기능이 불가능함

🚦 중요 포인트

  • 요청 중복을 식별할 수 있는 키(mutationKey)를 사용
  • 요청 중이라면 무시하고, 완료되면 다시 요청 가능
  • 중복 호출된 경우 onSuccessonSettled 등의 콜백이 실행되지 않도록 처리

📖 코드 정리

1. 타입 정의

interface UsePreventDuplicateMutationProps<TData, TError, TVariables> {
  mutationKey: string; // 중복 여부를 판단하기 위한 key
  mutationFn: (variables: TVariables) => Promise<TData>; // 실제 api 함수
  // 해당 함수를 사용하는 곳에서 onSuccess, onError 등을 사용하기 위해 설정
  options?: Omit<
    UseMutationOptions<TData, TError, TVariables>,
    "mutationFn" | "mutationKey"
  >;
}
  • 중복 요청을 식별하기 위한 key
  • 실제 API 요청 함수와 React query 옵션 일부를 가져옴

2. 요청 추적을 위한 Set 선언

const activeMutations = new Set<string>();
  • 요청 중인 mutationKey를 저장하는 Set
  • 중복 판단에 사용됨

3. 커스텀 훅 정의

const usePreventDuplicateMutation = <TData, TError, TVariables>({...}) => {
  return useMutation<TData | undefined, TError, TVariables>({
    ...
}
  • useMutation을 감싸는 형태
  • 반환 타입은 중복 시 undefined가 되므로 TData | undefined 처리

4. mutationFn 로직

mutationFn: async (variables) => {
  // 1. 요청을 보내기 전에 Set에 해당 키가 있는지 확인
  if (activeMutations.has(mutationKey)) {
    console.warn("중복 호출 무시!");
    // 2. 해당 키가 있으면 중복으로 판단하여 경고 출력 + undefined 반환
    return Promise.resolve(undefined);
  }
  // 3. 중복이 아니면 Set에 키를 추가하여 진행
  activeMutations.add(mutationKey);
 
  try {
    // 4. 실제 api 요청
    return await mutationFn(variables);
  } finally {
    // 5. 요청이 끝나면 Set에서 해당 키 제거
    activeMutations.delete(mutationKey);
  }
};
  • 중복 여부를 Set으로 판단
  • 요청 완료 후에는 Set에서 해당 key 제거

5. 사용 예시

export const useUpdate = (id: string) =>
  usePreventDuplicateMutation({
    mutationKey: `update-${id}`,
    mutationFn: () => updateAPI(id),
  });
  • id 기반으로 mutationKey를 고정
  • 같은 id로 중복 요청 시 호출 무시

💛 전체 코드

import type { UseMutationOptions } from "@tanstack/react-query";
import { useMutation } from "@tanstack/react-query";
 
interface UsePreventDuplicateMutationProps<TData, TError, TVariables> {
  mutationKey: string;
  mutationFn: (variables: TVariables) => Promise<TData>;
  options?: Omit<
    UseMutationOptions<TData, TError, TVariables>,
    "mutationFn" | "mutationKey"
  >;
}
 
const activeMutations = new Set<string>();
 
const usePreventDuplicateMutation = <TData, TError, TVariables>({
  mutationKey,
  mutationFn,
  options,
}: UsePreventDuplicateMutationProps<TData, TError, TVariables>) => {
  return useMutation<TData | undefined, TError, TVariables>({
    mutationFn: async (variables: TVariables): Promise<TData | undefined> => {
      if (activeMutations.has(mutationKey)) {
        console.warn("Duplicate request ignored");
        return Promise.resolve(undefined);
      }
      activeMutations.add(mutationKey);
 
      try {
        return await mutationFn(variables);
      } finally {
        activeMutations.delete(mutationKey);
      }
    },
    ...options,
    onSuccess: (data, variables, context) => {
      if (data === undefined) return; //NOTE: 미설정 시 중복 호출 횟수만큼 동작
 
      options?.onSuccess?.(data, variables, context);
    },
    onError: (error, variables, context) => {
      options?.onError?.(error, variables, context);
    },
    onSettled: (data, error, variables, context) => {
      if (data === undefined) return; //NOTE: 미설정 시 중복 호출 횟수만큼 동작
      options?.onSettled?.(data, error, variables, context);
    },
  });
};
 
export default usePreventDuplicateMutation;

참고

참고한 블로그 Giscus 사이트