React에서 버튼 중복 클릭을 방지하는 방법

사용자가 버튼을 빠르게 연속 클릭하여 비동기 API가 여러 번 호출되는 문제를 해결해보자

2025.05.23

15분 소요

글을 시작하며

프론트엔드 개발을 하다 보면 사용자가 버튼을 여러 번 빠르게 클릭하면서 동일한 API가 중복 호출되는 문제를 한 번쯤은 경험했을 것입니다.
이런 중복 호출은 사용자 경험을 해칠 뿐 아니라 서버에 불필요한 부담 및 에러를 발생 시키기 때문에 적절한 방어 코드가 꼭 필요합니다.

이번 글에서는 제가 TanStack Query의 비동기 상태를 사용하여 이 문제를 어떻게 해결했는지 공유해보고자 합니다.

문제 상황

  • 게시판에서 할 일을 선택하고, "삭제" 버튼을 누르면 API를 통해 서버에 삭제 요청이 전달됩니다.
  • 그런데 사용자가 버튼을 빠르게 두 번 클릭하게 되면 요청이 중복으로 발생하게 되고, 의도치 않은 결과를 초래할 수 있습니다.
<Button
  onClick={() => deleteTask()}
  startIcon={<DeleteIcon />}
>
  삭제
</Button>

이런 경우, 클릭 이벤트가 끝나기 전에 다시 버튼을 클릭할 수 있기 때문에 deleteTask()가 중복 실행됩니다.
저의 경우에는 이미 삭제된 할 일을 또 삭제하는 요청으로 인해 에러가 발생했었습니다..

Debounce 방식의 한계

버튼 중복 클릭 방지를 검색하면 Debounce를 제안하는 글을 여러개 볼 수 있습니다.
여기서 고민이 되는 부분은 시간을 얼마로 설정하냐는 것입니다. API latency는 서버, DB 상황에 따라서 언제나 달라질 수 있기 때문입니다.

예를 들어, 500ms로 debounce를 설정했는데 실제 API 응답은 700ms가 걸리는 상황이라면 어떻게 될까요?

사용자는 여전히 "응답이 오기 전에" 버튼을 다시 클릭할 수 있고, 이로 인해 중복 호출이 발생할 수 있습니다.

또한 다음과 같은 근본적인 한계도 존재합니다

  • 시간 기반 제어는 네트워크 상황에 따라 항상 적절한 값이 될 수 없습니다.
  • debounce는 요청 자체를 "지연"시킬 뿐, 요청이 끝났는지를 기준으로 제어하진 않습니다.
  • debounce는 무조건 일정 시간 기다렸다 실행하기 때문에, 요청이 빠르게 끝날 수 있는 경우조차 의도치 않게 늦춰질 수 있다는 단점이 있습니다.

결국 debounce는 "클릭 이벤트를 지연"시키는 것이지, API 요청의 상태를 기준으로 제어하는 방식이 아닙니다.

해결 방법: isPending으로 상태 제어

사용자의 클릭을 “시간”이 아닌 실제 요청 상태를 기준으로 제어해야 합니다.
TanStack Query를 사용하는 프로젝트라면, 비동기 작업의 상태를 받아올 수 있습니다.
저는 useMutationisPending 상태를 활용하여 버튼의 disabled 상태를 컨트롤했고 아주 간단하게 중복 클릭을 막을 수 있었습니다.

const [{ mutate: deleteTask, isPending }] = useMutation({ ... });

return (
  <Button
    disabled={isPending}
    onClick={() => deleteTask()}
    startIcon={<DeleteIcon />}
  >
    삭제
  </Button>
);

deleteTask는 비동기 mutation 함수이며 isPending은 현재 요청이 진행 중인지를 나타내는 상태입니다.
버튼에 disabled={isPending}를 주면, 요청이 끝날 때까지 클릭이 불가능해져 중복 클릭을 방지할 수 있습니다.

  • 버튼 disabled 외에도, 로딩 스피너나 로딩 문구를 추가해 사용자에게 피드백을 제공할 수 있습니다.

결론

버튼의 중복 클릭 문제는 사소해 보일 수 있지만, 서비스의 신뢰성과 사용자 경험에 직접적인 영향을 주는 중요한 이슈입니다.
이 문제를 해결하는 것은 단순한 기능 개선을 넘어, UX 향상은 물론 중복 데이터 생성과 서버 과부하를 방지하는 데 필수적인 요소입니다.
이번 글을 참고하여 한층 더 완성도 높은 서비스를 구현해보세요!

React비동기 처리버튼 클릭 방지중복 요청 방지