preventDefault
및 stopPropagation
: 각 메서드의 용도와 정확한 기능
Event.stopPropagation() 및 Event.preventDefault()
JavaScript 이벤트 처리는 대개 간단합니다. 이는 특히 단순한(상대적으로 평면적인) HTML 구조를 처리할 때 중요합니다. 이벤트가 요소의 계층 구조를 통해 이동 (또는 전파)할 때는 좀 더 복잡해집니다. 일반적으로 개발자가 stopPropagation()
또는 preventDefault()
에 문의하여 발생한 문제를 해결하려고 할 때입니다. 'preventDefault()
를 시도해 보고, 그래도 안 되면 stopPropagation()
를 시도해 보고, 그래도 안 되면 둘 다 시도해 보자'라고 생각한 적이 있다면 이 도움말이 도움이 될 것입니다. 각 메서드의 작동 방식과 사용 시기, 살펴볼 수 있는 다양한 작동 예시를 정확하게 설명해 드리겠습니다. 저희의 목표는 고객님의 혼란을 완전히 종식시키는 것입니다.
너무 깊이 들어가기 전에 JavaScript에서 가능한 두 가지 이벤트 처리 유형을 간단히 살펴보는 것이 중요합니다. 즉, 모든 최신 브라우저에서(버전 9 이전의 Internet Explorer는 이벤트 캡처를 전혀 지원하지 않음)
이벤트 스타일(캡처 및 버블링)
모든 최신 브라우저는 이벤트 캡처를 지원하지만 개발자가 거의 사용하지 않습니다.
흥미롭게도 Netscape에서 원래 지원한 유일한 이벤트 형식이었습니다. Netscape의 가장 큰 경쟁사인 Microsoft Internet Explorer는 이벤트 캡처를 전혀 지원하지 않았으며, 이벤트 버블링이라는 다른 스타일의 이벤트만 지원했습니다. W3C가 만들어졌을 때 두 이벤트 스타일의 장점을 발견했으며, addEventListener
메서드에 대한 세 번째 매개변수를 통해 브라우저가 두 이벤트를 모두 지원해야 한다고 선언했습니다. 원래 이 매개변수는 불리언이었지만 모든 최신 브라우저는 options
객체를 세 번째 매개변수로 지원합니다. 이 객체를 사용하면 이벤트 캡처를 사용할지 여부 등을 지정할 수 있습니다.
someElement.addEventListener('click', myClickHandler, { capture: true | false });
options
객체는 capture
속성과 마찬가지로 선택사항입니다. 둘 중 하나를 생략하면 capture
의 기본값은 false
이며, 즉 이벤트 대화창이 사용됩니다.
이벤트 캡처
이벤트 핸들러가 '캡처 단계에서 수신 대기'하는 것은 무엇을 의미하나요? 이를 이해하려면 이벤트가 발생하는 방식과 이동하는 방식을 알아야 합니다. 다음은 개발자가 이를 활용하거나 신경 쓰지 않거나 고려하지 않더라도 모든 이벤트에 적용됩니다.
모든 이벤트는 창에서 시작하여 먼저 캡처 단계를 거칩니다. 즉, 이벤트가 전달되면 창이 시작되고 먼저 타겟 요소를 향해 '아래쪽'으로 이동합니다. 이는 버블링 단계에서만 리슨하는 경우에도 발생합니다. 다음 마크업 및 JavaScript 예시를 살펴보세요.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('#C was clicked');
},
true,
);
사용자가 요소 #C
를 클릭하면 window
에서 시작된 이벤트가 전달됩니다. 그러면 이 이벤트는 다음과 같이 하위 요소를 통해 전파됩니다.
window
=> document
=> <html>
=> <body>
=> 등은 타겟에 도달할 때까지 계속됩니다.
window
, document
, <html>
요소 또는 <body>
요소 (또는 타겟으로 이동하는 다른 요소)에서 클릭 이벤트를 수신 대기하고 있지 않은지 여부는 중요하지 않습니다. 이벤트는 여전히 window
에서 시작되며 방금 설명한 대로 여정을 시작합니다.
이 예에서 클릭 이벤트는 window
과 #C
사이의 모든 요소를 통해 window
에서 타겟 요소(여기서는 #C
)로 전파됩니다(stopPropagation()
메서드의 작동 방식과 직접 연결되므로 중요한 단어이며 이 문서의 뒷부분에서 설명됨).
즉, 클릭 이벤트가 window
에 시작되고 브라우저에 다음과 같은 질문이 표시됩니다.
'캡처 단계에서 window
의 클릭 이벤트를 수신 대기하는 항목이 있나요?' 이 경우 적절한 이벤트 핸들러가 실행됩니다. 이 예에서는 아무것도 실행되지 않으므로 핸들러가 실행되지 않습니다.
그런 다음 이벤트가 document
에 전파되고 브라우저는 '캡처 단계에서 document
의 클릭 이벤트를 리슨하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 <html>
요소에 전파되고 브라우저가 '캡처 단계에서 <html>
요소 클릭을 수신 대기하는 것이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 <body>
요소로 전파되고 브라우저는 '캡처 단계에서 <body>
요소의 클릭 이벤트를 리슨하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 #A
요소로 전파됩니다. 브라우저는 다시 '캡처 단계에서 #A
의 클릭 이벤트를 리슨하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 #B
요소에 전파되고 동일한 질문이 표시됩니다.
마지막으로 이벤트가 대상에 도달하면 브라우저에서 '캡처 단계에서 #C
요소의 클릭 이벤트를 수신 대기하는 항목이 있나요?'라고 묻습니다. 이번에는 '예'입니다. 이벤트가 타겟에 있는 이 짧은 시간 간격을 '타겟 단계'라고 합니다. 이 시점에서 이벤트 핸들러가 실행되고 브라우저에서 console.log '#C was click'을 실행하면 됩니다. 그렇죠?
틀렸습니다. 아직 끝난 것이 아닙니다. 프로세스는 계속되지만 이제 버블링 단계로 변경됩니다.
일정 대화창
브라우저에서 다음을 묻습니다.
'버블링 단계에서 #C
의 클릭 이벤트를 수신하는 항목이 있나요?' 이 부분에 주의를 기울이세요.
캡처 단계 및 버블링 단계 모두에서 클릭 (또는 모든 이벤트 유형)을 수신 대기하는 것이 완벽하게 가능합니다. 그리고 두 단계 모두에서 이벤트 핸들러를 연결했다면(예: .addEventListener()
를 capture = true
로 한 번, capture = false
로 한 번 호출하여 두 번 연결) 두 이벤트 핸들러가 동일한 요소에 대해 실행됩니다. 하지만 두 이벤트가 서로 다른 단계(캡처 단계에서 하나, 버블링 단계에서 하나)에서 실행된다는 점도 중요합니다.
그런 다음 이벤트가 상위 요소인 #B
로 전파됩니다(이벤트가 DOM 트리를 '위로' 이동하는 것처럼 보이므로 더 일반적으로 '버블링'이라고 함). 그러면 브라우저가 '버블링 단계에서 #B
의 클릭 이벤트를 리슨하는 항목이 있나요?'라고 묻습니다. 이 예에서는 아무것도 없으므로 핸들러가 실행되지 않습니다.
이어서 이벤트가 #A
로 표시되고 브라우저가 '버블링 단계에서 #A
의 클릭 이벤트를 수신 대기하는 것이 있나요?'라고 묻습니다.
그러면 이벤트가 <body>
로 표시됩니다. '버블링 단계에서 <body>
요소의 클릭 이벤트를 수신 대기하는 항목이 있나요?'
다음으로 <html>
요소는 다음과 같습니다. '버블링 단계에서 <html>
요소의 클릭 이벤트를 수신 대기하는 것이 있나요?
다음은 document
입니다. '버블링 단계에서 document
의 클릭 이벤트를 수신하는 항목이 있나요?'
마지막으로 window
: '버블링 단계에서 창에서 클릭 이벤트를 수신 대기하는 것이 있나요?'
다양한 혜택이 마음에 드셨나요? 긴 여정이었으며 이제는 이벤트가 매우 지쳐 있을 수도 있지만, 믿기지 않겠지만 모든 이벤트가 거치는 여정입니다. 개발자는 일반적으로 한 이벤트 단계에만 관심이 있으므로(일반적으로 버블링 단계) 대부분 이러한 현상은 눈에 띄지 않습니다.
이벤트 캡처 및 이벤트 버블링을 사용해 보고 핸들러가 실행될 때 콘솔에 메모를 로깅해 보세요. 이벤트가 이동하는 경로를 보는 것은 매우 유용한 정보를 제공합니다. 다음은 두 단계에서 모든 요소를 수신 대기하는 예입니다.
<html>
<body>
<div id="A">
<div id="B">
<div id="C"></div>
</div>
</div>
</body>
</html>
document.addEventListener(
'click',
function (e) {
console.log('click on document in capturing phase');
},
true,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in capturing phase');
},
true,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in capturing phase');
},
true,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in capturing phase');
},
true,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in capturing phase');
},
true,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in capturing phase');
},
true,
);
document.addEventListener(
'click',
function (e) {
console.log('click on document in bubbling phase');
},
false,
);
// document.documentElement == <html>
document.documentElement.addEventListener(
'click',
function (e) {
console.log('click on <html> in bubbling phase');
},
false,
);
document.body.addEventListener(
'click',
function (e) {
console.log('click on <body> in bubbling phase');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('click on #A in bubbling phase');
},
false,
);
document.getElementById('B').addEventListener(
'click',
function (e) {
console.log('click on #B in bubbling phase');
},
false,
);
document.getElementById('C').addEventListener(
'click',
function (e) {
console.log('click on #C in bubbling phase');
},
false,
);
콘솔 출력은 클릭하는 요소에 따라 다릅니다. DOM 트리의 '가장 깊은' 요소(#C
요소)를 클릭하면 이러한 이벤트 핸들러가 모두 실행됩니다. 어떤 요소가 어떤 요소인지 더 명확하게 하기 위해 약간의 CSS 스타일을 적용한 콘솔 출력 #C
요소(스크린샷 포함)는 다음과 같습니다.
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
"click on <body> in bubbling phase"
"click on <html> in bubbling phase"
"click on document in bubbling phase"
아래의 실시간 데모에서 이 기능을 대화식으로 사용해 볼 수 있습니다. #C
요소를 클릭하고 콘솔 출력을 확인합니다.
event.stopPropagation()
이벤트가 발생하는 위치와 캡처 단계와 버블링 단계 모두에서 DOM을 통해 이동(즉, 전파)하는 방식을 이해했으므로 이제 event.stopPropagation()
에 집중할 수 있습니다.
stopPropagation()
메서드는 대부분의 네이티브 DOM 이벤트에서 호출할 수 있습니다. '대부분'이라고 말하는 이유는 이 메서드를 호출해도 아무 일도 일어나지 않는 경우가 몇 개 있기 때문입니다(이벤트가 처음부터 전파되지 않기 때문). focus
, blur
, load
, scroll
등의 이벤트가 이 카테고리에 속합니다. stopPropagation()
를 호출할 수 있지만 이러한 이벤트는 전파되지 않으므로 아무 일도 일어나지 않습니다.
stopPropagation
는 무엇을 하나요?
말 그대로입니다. 이 메서드를 호출하면 그 시점부터 이벤트가 전파되지 않습니다. 이는 양방향(캡처 및 버블링) 모두에 적용됩니다. 따라서 캡처 단계의 어느 곳에서든 stopPropagation()
를 호출하면 이벤트가 타겟 단계 또는 버블링 단계로 전달되지 않습니다. 버블링 단계에서 호출하면 이미 캡처 단계를 거쳤지만 호출한 지점부터 '버블링'이 중단됩니다.
동일한 마크업 예시로 돌아가서 캡처 단계에서 #B
요소에서 stopPropagation()
를 호출하면 어떻게 될까요?
다음과 같은 출력이 생성됩니다.
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
아래의 실시간 데모에서 이 기능을 대화식으로 사용해 볼 수 있습니다. 실시간 데모에서 #C
요소를 클릭하고 콘솔 출력을 확인합니다.
버블링 단계에서 #A
에서 전파를 중지하는 것은 어떨까요? 그러면 다음과 같은 출력이 생성됩니다.
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
"click on #C in bubbling phase"
"click on #B in bubbling phase"
"click on #A in bubbling phase"
아래의 라이브 데모를 통해 양방향 방식으로 플레이해 볼 수 있습니다. 실시간 데모에서 #C
요소를 클릭하고 콘솔 출력을 확인합니다.
재미로 한 번 더 해 보겠습니다. #C
의 타겟 단계에서 stopPropagation()
를 호출하면 어떻게 되나요?
'타겟 단계'는 이벤트가 타겟에 있는 시간 기간에 주어진 이름입니다. 다음과 같은 출력이 생성됩니다.
"click on document in capturing phase"
"click on <html> in capturing phase"
"click on <body> in capturing phase"
"click on #A in capturing phase"
"click on #B in capturing phase"
"click on #C in capturing phase"
'캡처 단계에서 #C 클릭'을 로깅하는 #C
의 이벤트 핸들러는 계속 실행되지만 '버블링 단계에서 #C 클릭'을 로깅하는 핸들러는 실행되지 않습니다. 이치야말로
이해하실 수 있을 겁니다. 앞의 에서 stopPropagation()
를 호출했으므로 이 지점에서 이벤트 전파가 중단됩니다.
아래의 라이브 데모를 통해 양방향 방식으로 플레이해 볼 수 있습니다. 라이브 데모에서 #C
요소를 클릭하고 콘솔 출력을 관찰합니다.
이러한 실시간 데모에서 자유롭게 살펴보시기 바랍니다. #A
요소만 클릭하거나 body
요소만 클릭해 봅니다. 어떤 일이 일어날지 예측한 다음 맞는지 관찰해 보세요. 이 시점에서 상당히 정확하게 예측할 수 있습니다.
event.stopImmediatePropagation()
이 이상하고 자주 사용되지 않는 방법은 무엇인가요? stopPropagation
와 비슷하지만 이벤트가 하위 요소로 이동 (캡처)하거나 상위 요소로 이동 (버블링)하지 않도록 중지하는 대신 이 메서드는 단일 요소에 연결된 이벤트 핸들러가 둘 이상 있는 경우에만 적용됩니다. addEventListener()
는 멀티캐스트 스타일의 이벤트 처리를 지원하므로 이벤트 핸들러를 단일 요소에 두 번 이상 연결할 수 있습니다. 이 경우 (대부분의 브라우저에서) 이벤트 핸들러는 연결된 순서대로 실행됩니다. stopImmediatePropagation()
를 호출하면 후속 핸들러가 실행되지 않습니다. 다음 예를 참고하세요.
<html>
<body>
<div id="A">I am the #A element</div>
</body>
</html>
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run first!');
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I shall run second!');
e.stopImmediatePropagation();
},
false,
);
document.getElementById('A').addEventListener(
'click',
function (e) {
console.log('When #A is clicked, I would have run third, if not for stopImmediatePropagation');
},
false,
);
위 예시에서는 다음과 같은 콘솔 출력이 표시됩니다.
"When #A is clicked, I shall run first!"
"When #A is clicked, I shall run second!"
두 번째 이벤트 핸들러가 e.stopImmediatePropagation()
를 호출하므로 세 번째 이벤트 핸들러는 실행되지 않습니다. 대신 e.stopPropagation()
를 호출했다면 세 번째 핸들러가 계속 실행됩니다.
event.preventDefault()
stopPropagation()
가 이벤트가 '아래쪽'(캡처) 또는 '위쪽'(버블링)으로 이동하는 것을 방지하는 경우 preventDefault()
는 어떻게 하나요? 비슷한 기능을 하는 것 같습니다. 그렇나요?
아니요 이 두 가지는 종종 혼동되지만 실제로는 서로 관련이 없습니다.
preventDefault()
가 표시되면 머릿속으로 '작업'이라는 단어를 추가합니다. '기본 작업을 방지'하는 것을 생각해 보세요.
요청할 수 있는 기본 작업은 무엇인가요? 안타깝게도 이 질문에 대한 답변은 요소 + 이벤트 조합에 따라 크게 달라지므로 명확하지 않습니다. 문제를 더욱 혼란스럽게 만들기 위해 기본 작업이 전혀 없는 경우도 있습니다.
이해하기 위해 매우 간단한 예를 시작하겠습니다. 웹페이지에서 링크를 클릭하면 어떻게 되나요? 브라우저가 해당 링크에 지정된 URL로 이동할 것으로 예상됩니다.
이 경우 요소는 앵커 태그이고 이벤트는 클릭 이벤트입니다. 이 조합(<a>
+
click
)에는 링크의 href로 이동하는 '기본 작업'이 있습니다. 브라우저가 이러한 기본 작업을 실행하지 못하도록 방지하려면 어떻게 해야 하나요? 즉, 브라우저가 <a>
요소의 href
속성으로 지정된 URL로 이동하지 못하도록 하려면 어떻게 해야 하나요? preventDefault()
는 다음과 같은 작업을 실행합니다. 다음 예를 살펴보세요.
<a id="avett" href="https://www.theavettbrothers.com/welcome">The Avett Brothers</a>
document.getElementById('avett').addEventListener(
'click',
function (e) {
e.preventDefault();
console.log('Maybe we should just play some of their music right here instead?');
},
false,
);
아래의 실시간 데모에서 이 기능을 대화식으로 사용해 볼 수 있습니다. The Avett Brothers 링크를 클릭하고 콘솔 출력과 Avett Brothers 웹사이트로 리디렉션되지 않는 것을 확인합니다.
일반적으로 The Avett Brothers라는 링크를 클릭하면 www.theavettbrothers.com
로 이동합니다. 하지만 이 경우 클릭 이벤트 핸들러를 <a>
요소에 연결하고 기본 작업을 방지해야 한다고 지정했습니다. 따라서 사용자가 이 링크를 클릭해도 아무 곳으로도 이동하지 않으며 대신 콘솔에 '여기에서 음악을 재생하는 것이 좋을까요?'라는 로그가 기록됩니다.
기본 작업을 방지할 수 있는 다른 요소/이벤트 조합은 무엇인가요? 모든 방법을 나열할 수는 없으며, 때로는 실험을 통해 확인해야 합니다. 간단히 몇 가지를 소개해 드리자면 다음과 같습니다.
<form>
요소 + '제출' 이벤트: 이 조합의preventDefault()
는 양식이 제출되지 않도록 합니다. 이는 유효성 검사를 실행하려는 경우 유용합니다. 실패하면 조건부로 preventDefault를 호출하여 양식 제출을 중지할 수 있습니다.<a>
요소 + 'click' 이벤트: 이 조합의preventDefault()
는 브라우저가<a>
요소의 href 속성에 지정된 URL로 이동하지 못하도록 합니다.document
+ 'mousewheel' 이벤트: 이 조합의preventDefault()
는 마우스 휠을 사용한 페이지 스크롤을 방지합니다(키보드로 스크롤하는 것은 계속 작동함).
↜{ passive: false }
를 사용하여addEventListener()
를 호출해야 합니다.document
+ 'keydown' 이벤트: 이 조합의preventDefault()
는 치명적입니다. 페이지를 거의 사용할 수 없게 렌더링하여 키보드 스크롤, 탭 이동, 키보드 강조 표시를 방지합니다.document
+ 'mousedown' 이벤트: 이 조합의preventDefault()
는 마우스로 텍스트를 강조 표시하는 작업과 마우스를 누른 상태에서 호출하는 다른 모든 '기본' 작업을 방지합니다.<input>
요소 + 'keypress' 이벤트: 이 조합의preventDefault()
는 사용자가 입력한 문자가 입력 요소에 도달하지 못하게 합니다. 하지만 이렇게 하면 안 됩니다. 타당한 이유가 있는 경우는 거의 없습니다.document
+ 'contextmenu' 이벤트: 이 조합의preventDefault()
는 사용자가 마우스 오른쪽 버튼을 클릭하거나 길게 누르면(또는 컨텍스트 메뉴가 표시될 수 있는 다른 방법으로) 기본 브라우저 컨텍스트 메뉴가 표시되지 않도록 합니다.
이 목록은 preventDefault()
의 사용 사례를 모두 보여주지는 않지만 preventDefault()
를 사용하는 방법을 파악하는 데 도움이 되기를 바랍니다.
재미있는 장난?
문서에서 시작하여 캡처 단계에서 stopPropagation()
및 preventDefault()
하면 어떻게 되나요? 유쾌함은 계속됩니다! 다음 코드 스니펫은 웹페이지를 거의 완전히 무용하게 렌더링합니다.
function preventEverything(e) {
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
}
document.addEventListener('click', preventEverything, true);
document.addEventListener('keydown', preventEverything, true);
document.addEventListener('mousedown', preventEverything, true);
document.addEventListener('contextmenu', preventEverything, true);
document.addEventListener('mousewheel', preventEverything, { capture: true, passive: false });
왜 이런 작업을 하고 싶어 하는지 잘 모르겠습니다(누군가에게 장난을 치려는 경우는 예외). 하지만 여기에서 어떤 일이 일어나고 있는지 생각해 보고 왜 이런 상황이 발생하는지 파악하는 것이 좋습니다.
모든 이벤트는 window
에서 시작되므로 이 스니펫에서는 click
, keydown
, mousedown
, contextmenu
, mousewheel
이벤트가 이를 리슨하는 요소에 도달하지 못하도록 완전히 중지합니다. 또한 이 핸들러 이후에 문서에 연결된 모든 핸들러도 차단되도록 stopImmediatePropagation
를 호출합니다.
stopPropagation()
및 stopImmediatePropagation()
는 페이지를 무용하게 만드는 요소가 아닙니다(적어도 대부분은 아닙니다). 단순히 이벤트가 다른 곳으로 이동하지 못하도록 합니다.
하지만 preventDefault()
도 호출합니다. preventDefault()
는 기본 작업을 방지하는 것으로 기억할 수 있습니다. 따라서 모든 기본 작업(예: 마우스 휠 스크롤, 키보드 스크롤 또는 강조 표시 또는 탭 이동, 링크 클릭, 컨텍스트 메뉴 표시 등)이 모두 차단되어 페이지가 상당히 무용지물이 됩니다.
실시간 데모
이 도움말의 모든 예시를 한곳에서 다시 살펴보려면 아래에 삽입된 데모를 확인하세요.