Giscus로 블로그 댓글 기능 추가하기
사용자의 api 중복 호출은 UI 단에서 isLoading
을 통해 처리할 수 있지만 스크립트 등을 통한 중복 요청이 들어오는 경우 해당 횟수만큼 api가 호출됩니다.
이를 방지하기 위해 React Query 기반의 커스텀 훅을 구현했습니다.
⁉️ 기존 방식(ref, throttle)의 문제점
-
ref
를 통해isDisabled
를 관리
mutation
을 사용하는 곳마다 코드를 작성해주어야함 -
throttle
이나debounce
로 컨트롤
일정 시간동안 api 호출을 막는 것이기 때문에 지정해둔 시간이 되기 전까지 추가적인 기능이 불가능함
🚦 중요 포인트
- 요청 중복을 식별할 수 있는 키(
mutationKey
)를 사용 - 요청 중이라면 무시하고, 완료되면 다시 요청 가능
- 중복 호출된 경우
onSuccess
나onSettled
등의 콜백이 실행되지 않도록 처리
📖 코드 정리
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;
참고