2026. 7. 18.
React onClick 한 번인데 왜 요청은 두 개일까? handler count로 경로 찾기
React 버튼 한 번에 로그나 요청이 두 번 보일 때 handler 횟수와 Network를 먼저 측정하고, 버블링·form submit·native listener·Effect를 분리하는 순서다.

저장 버튼을 한 번 눌렀는데 Console에 같은 로그가 두 줄 찍히고 Network에도 요청이 두 개 생겼다. 이때 Strict Mode를 끄거나 debounce부터 넣으면 두 번째 실행 경로가 남는다. 실제 onClick handler 첫 줄에서 횟수를 세고, 같은 클릭의 요청 수와 나란히 봐야 한다.
async function handleSave() {
console.count('save-click')
await saveDraft()
}
한 번 클릭했을 때 save-click: 1인데 요청이 두 개라면 onClick은 한 번 실행됐다. 저장 함수 내부, useEffect, form 제출처럼 handler 뒤의 경로를 찾아야 한다. save-click: 2라면 이벤트 연결, 부모 handler, native listener처럼 handler에 들어오기 전 경로부터 본다.
handler 안에서 센 숫자가 첫 갈림길이다
React 이벤트 공식 문서는 handler 함수를 호출하지 말고 전달하라고 안내한다. 아래 두 코드는 비슷해 보이지만 실행 시점이 다르다.
// 클릭할 때 실행
<button onClick={handleSave}>저장</button>
// 렌더링할 때 바로 실행
<button onClick={handleSave()}>저장</button>
AI가 만든 코드에서 onClick={handleSave()}가 보이면 먼저 onClick={handleSave} 또는 onClick={() => handleSave()}로 바꾼다. 렌더 로그와 클릭 로그를 같은 위치에 두지 않는다. 컴포넌트 본문에 있는 console.log는 렌더 횟수를 세고, handler 안의 console.count는 클릭 경로 진입 횟수를 센다.
커스텀 Button을 쓴다면 prop 전달도 한 번만 일어나는지 본다. 바깥 wrapper와 안쪽 <button>이 모두 같은 함수를 호출하거나, 내부 handler가 prop을 두 번 실행하면 한 번의 클릭이 두 호출로 이어진다. 기본 버튼은 되는데 커스텀 버튼만 다르다면 커스텀 Button의 onClick 전달 두 곳을 확인하는 순서로 범위를 더 좁힐 수 있다.
판단 기준은 단순하다. handler count가 2면 클릭이 들어오는 길을 보고, count가 1이면 handler 뒤의 작업을 본다.
부모 이벤트·form·native listener를 한 줄씩 끊어 본다
두 번째 경로를 찾을 때 여러 코드를 한꺼번에 지우지 않는다. 아래 순서로 한 갈래씩 확인한다.
자식과 부모에 같은 동작이 연결됐는지 본다
클릭 이벤트는 부모 요소로 전파된다. 자식 버튼과 부모 카드에 onClick이 모두 있고 두 handler가 같은 저장 함수를 호출하면 둘 다 실행될 수 있다. React 문서와 MDN 이벤트 버블링 설명을 기준으로, 부모 동작이 필요 없다면 부모 handler를 제거한다. 자식에서만 끝내야 하는 명확한 UI라면 event.stopPropagation()을 검토한다.
function SaveButton() {
return (
<button
type="button"
onClick={(event) => {
event.stopPropagation()
handleSave()
}}
>
저장
</button>
)
}
stopPropagation()을 모든 버튼에 습관적으로 붙이지 않는다. 부모 클릭도 제품 동작에 필요한지 먼저 확인한다.
form 안에서는 click과 submit을 중복 호출하지 않는다
<form onSubmit={handleSave}> 안의 버튼에 다시 onClick={handleSave}를 두면 click 경로와 submit 경로가 같은 함수를 호출할 수 있다. 제출 버튼이라면 저장은 onSubmit 한 곳에 둔다. 제출 목적이 아닌 버튼에는 type="button"을 쓴다. MDN button 문서에 따르면 form과 연결된 button은 type을 지정하지 않으면 submit이 기본 동작이다.
페이지 새로고침까지 함께 보인다면 React 버튼의 type과 submit을 확인하는 글로 form 기본 동작을 먼저 고친다.
addEventListener에는 같은 cleanup이 있어야 한다
React prop과 별도로 window.addEventListener('click', handleSave)를 등록했다면 native listener가 하나 더 있다. Effect가 다시 실행될 때 기존 listener를 제거하지 않으면 등록이 쌓일 수 있다.
useEffect(() => {
window.addEventListener('click', handleWindowClick)
return () => {
window.removeEventListener('click', handleWindowClick)
}
}, [])
추가와 제거에는 같은 event type과 같은 함수 참조를 사용한다. 익명 함수를 각각 새로 만들면 제거할 listener를 가리키지 못한다. 이 패턴은 React useEffect 문서와 MDN addEventListener 문서의 setup·cleanup 경계에 맞는다.
Strict Mode는 click handler보다 render와 Effect를 먼저 의심하게 한다
Strict Mode가 켜진 개발 화면에서 로그 두 줄이 보인다는 사실만으로 click handler가 두 번 호출됐다고 결론 내리면 안 된다. React StrictMode 문서는 개발 중 컴포넌트 본문을 추가 렌더링하지만, 이 목록에서 event handler 내부 코드는 제외한다고 명시한다. 반면 Effect는 cleanup 누락을 찾기 위해 setup과 cleanup을 한 번 더 실행한다.
따라서 handler 안의 save-click은 1인데 Effect 안의 로그가 두 번이라면 클릭 문제가 아니다. 클릭으로 state가 바뀌고, 그 state를 감시하는 Effect가 요청을 보내는 구조인지 본다.
function handleSave() {
console.count('save-click')
setShouldSave(true)
}
useEffect(() => {
if (!shouldSave) return
saveDraft()
}, [shouldSave])
사용자 클릭이 곧 저장 이유라면 요청을 handler에 두는 편이 경로를 읽기 쉽다. 외부 시스템 동기화가 목적이라 Effect를 유지해야 한다면 cleanup과 의존성을 고친다. 개발 중 setup → cleanup → setup을 구분하는 자세한 절차는 React useEffect 두 번 실행과 cleanup 점검에서 이어서 확인할 수 있다.
Strict Mode를 끄는 것은 원인 확인이 아니다. 개발 검사를 없애기 전에 handler, Effect, Network 세 위치의 숫자가 무엇을 뜻하는지 분리한다.
한 번 클릭해 숫자 세 개가 맞으면 수정이 끝난다
수정 뒤에는 Console을 지우고 Network의 Preserve log를 끈 상태에서 버튼을 한 번 누른다. 아래 세 결과를 같은 재현에서 확인한다.
- Console의
save-click이 1이다. - Network의 의도한 저장 요청이 1개다.
- 부모 카드 클릭, form submit, Effect setup 중 의도하지 않은 두 번째 경로가 실행되지 않는다.
개발 모드에서만 Effect setup 로그가 추가로 보이더라도 handler와 요청이 각각 한 번이면 문제를 섞지 않는다. 반대로 handler는 한 번인데 요청이 두 개면 요청 함수 내부 재시도, 클라이언트 라이브러리 설정, 서버 리다이렉트 같은 다음 경계로 넘긴다. 이 글의 완료 조건은 클릭 한 번에서 프런트엔드가 시작한 저장 경로가 하나라는 증거를 남기는 것이다.
실제 결제, 주문, 권한 변경처럼 중복 처리 비용이 큰 작업은 프런트엔드 수정만으로 안전을 보장하지 않는다. 서버에서도 같은 요청을 중복 처리하지 않도록 별도 정책을 설계해야 한다. 지금은 console.count 한 줄을 실제 handler에 넣고, 클릭 한 번의 Console과 Network를 다시 확인한다.
참고 출처
- Responding to Events (React)
- StrictMode (React)
- useEffect (React)
- Synchronizing with Effects (React)
- Event bubbling (MDN)
- button element (MDN)
- addEventListener (MDN)
이 글은 AI 코딩과 개발 학습의 일반 정보 제공 목적입니다. 도구, 모델, 커리큘럼, 요금은 버전과 시점에 따라 달라질 수 있으므로 실습이나 도입 전 공식 문서와 최신 릴리스 노트를 확인하세요.
다음으로 읽을 기사
같은 흐름으로 이어 읽기 좋은 기사만 추려 보여줍니다.
첫 번째 댓글을 남겨보세요
여러분의 생각이 다른 독자에게 도움이 됩니다.
댓글 0
이 글을 읽은 독자들의 생각을 나눠보세요.