‘길게 눌러야 실행’이라고 적힌 버튼을 아주 짧게 누르고 뗐는데 잠시 뒤 작업이 실행된다. 누르기 시작할 때 setTimeout을 예약해 놓고, 손을 뗄 때 그 예약을 취소하지 않았기 때문이다. 길게 누르기는 타이머를 시작하는 코드와 취소하는 코드가 한 쌍이어야 한다.
setTimeout은 누르기가 끝나도 자동으로 사라지지 않는다
도식의 타이머에는 정상적으로 손을 뗀 경로와 브라우저가 입력을 취소한 경로가 모두 연결된다. 제한 시간이 되기 전에 어느 쪽이 와도 같은 정리 함수에서 예약을 해제해야 오래 누르기 동작이 뒤늦게 실행되지 않는다.
setTimeout은 지정한 시간이 지난 뒤 함수를 한 번 실행하도록 예약하고 타이머 ID를 돌려준다. 누르고 있는 동안에만 예약이 유효해야 한다면, 손을 떼거나 입력이 취소될 때 clearTimeout(ID)를 호출한다. 다음 예제는 실제 삭제나 결제를 하지 않고 상태 문구만 바꾸는 설명용 버튼이다.
<button type="button" id="hold">길게 눌러 확인</button>
<p id="result" aria-live="polite">대기 중</p>
<script>
const button = document.querySelector('#hold');
const result = document.querySelector('#result');
let timerId = null;
function cancelHold() {
if (timerId === null) return;
clearTimeout(timerId);
timerId = null;
}
button.addEventListener('pointerdown', () => {
cancelHold();
result.textContent = '누르는 중';
timerId = setTimeout(() => {
timerId = null;
result.textContent = '확인 완료';
}, 800);
});
button.addEventListener('pointerup', cancelHold);
button.addEventListener('pointercancel', cancelHold);
button.addEventListener('pointerleave', cancelHold);
</script>800밀리초는 동작을 설명하기 위한 예시일 뿐 보편적인 UX 기준은 아니다. 짧게 눌렀다 떼면 예약된 함수를 취소한다. 충분히 누르면 콜백이 먼저 실행되고 timerId가 다시 null이 된다. 누르는 중 다른 곳으로 포인터가 이동하거나 입력 자체가 취소돼도 정리한다. 누르기를 여러 번 시작했을 때 이전 예약이 남지 않도록 pointerdown에서 먼저 취소하는 이유도 여기에 있다.
타이머가 남는지 어떻게 재현하고 테스트할까?
먼저 짧게 누르고 800밀리초보다 길게 기다려 확인 완료가 나오지 않는지 본다. 다음에는 충분히 누른 뒤 한 번만 완료되는지 확인한다. 누르는 중 포인터를 버튼 밖으로 옮기거나 터치 입력이 취소될 때도 결과가 뒤늦게 바뀌지 않아야 한다. 느린 컴퓨터나 백그라운드 탭에서는 타이머 시각이 정확히 맞지 않을 수 있으므로 밀리초 단위의 정밀한 시계로 취급하지 않는다.
이 최소 예제는 포인터 입력만 보여 준다. 키보드 이용자를 위한 별도 조작이나 동등한 확인 수단 없이 실제 중요한 작업의 유일한 관문으로 사용하면 안 된다. 특히 계정 삭제나 결제처럼 되돌리기 어려운 작업을 클라이언트의 타이머만으로 승인하지 않는다. 서버 쪽 권한 확인과 별도의 확인 절차가 필요하다.
핵심 요약: 예약한 타이머의 종료 경로를 함께 설계한다
setTimeout으로 길게 누르기를 예약했다면 pointerup, pointercancel, 이탈처럼 누르기가 끝나는 경로에서 clearTimeout을 호출한다. 짧은 누르기, 긴 누르기, 입력 취소를 각각 테스트하고, 중요한 작업은 프런트엔드 타이머만 믿지 않는다.
작성자
기초 개념을 구현과 검증, 실제 운영 판단까지 연결해 기록합니다.

