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
에서 시작되고 위에서 설명한 대로 여정을 시작합니다.
이 예에서 클릭 이벤트는 전파됩니다 (stopPropagation()
메서드의 작동 방식과 직접 관련이 있는 중요한 단어이며 이 문서의 뒷부분에서 설명함). window
에서 window
와 #C
사이의 모든 요소를 통해 타겟 요소 (이 경우 #C
)로 전파됩니다.
즉, 클릭 이벤트는 window
에서 시작되고 브라우저에서 다음 질문을 합니다.
'캡처 단계에서 window
의 클릭 이벤트를 수신 대기하는 항목이 있나요?' 이 경우 적절한 이벤트 핸들러가 실행됩니다. 이 예에서는 아무것도 없으므로 핸들러가 실행되지 않습니다.
그런 다음 이벤트가 document
로 전파되고 브라우저에서 '캡처 단계에서 document
의 클릭 이벤트를 수신하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 <html>
요소로 전파되고 브라우저에서 '캡처 단계에서 <html>
요소의 클릭을 수신하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 <body>
요소로 전파되고 브라우저에서 '캡처 단계에서 <body>
요소의 클릭 이벤트를 수신하는 항목이 있나요?'라고 묻습니다. 이 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 #A
요소로 전파됩니다. 브라우저에서 다시 '캡처 단계에서 #A
의 클릭 이벤트를 수신 대기하는 항목이 있나요?'라고 묻고, 있는 경우 적절한 이벤트 핸들러가 실행됩니다.
그런 다음 이벤트가 #B
요소로 전파되고 동일한 질문이 표시됩니다.
마지막으로 이벤트가 타겟에 도달하고 브라우저에서 '캡처 단계에서 #C
요소의 클릭 이벤트를 수신하는 항목이 있나요?'라고 묻습니다. 이번 대답은 '예'입니다. 이벤트가 타겟에 도달하는 짧은 기간을 '타겟 단계'라고 합니다. 이 시점에서 이벤트 핸들러가 실행되고 브라우저가 '#C was clicked'를 console.log한 다음 완료됩니다.
틀렸습니다. 아직 끝난 것이 아닙니다. 프로세스가 계속되지만 이제 버블링 단계로 변경됩니다.
이벤트 버블링
브라우저에서 다음을 묻습니다.
'버블링 단계에서 #C
의 클릭 이벤트를 수신하는 항목이 있나요?' 여기에서 주의를 기울여야 합니다.
캡처 단계와 버블링 단계 모두에서 클릭 (또는 모든 이벤트 유형)을 수신 대기할 수 있습니다. 두 단계에서 모두 이벤트 핸들러를 연결한 경우 (예: capture = true
으로 한 번, capture = false
로 한 번 .addEventListener()
를 두 번 호출) 동일한 요소에 대해 두 이벤트 핸들러가 모두 실행됩니다. 하지만 서로 다른 단계 (캡처링 단계와 버블링 단계)에서 실행된다는 점도 유의해야 합니다.
다음으로 이벤트가 상위 요소인 #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"
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"
버블링 단계에서 #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
의 타겟 단계에서 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()
을 from으로 호출했으므로 이벤트 전파가 중지되는 지점입니다.
이러한 라이브 데모에서 자유롭게 실험해 보세요. #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'라는 라벨이 지정된 링크를 클릭하면 www.theavettbrothers.com
로 이동합니다. 하지만 이 경우 클릭 이벤트 핸들러를 <a>
요소에 연결하고 기본 작업을 방지해야 한다고 지정했습니다. 따라서 사용자가 이 링크를 클릭해도 어디로도 이동하지 않으며 콘솔에는 '여기에서 음악을 재생하는 게 좋을까요?'라는 메시지만 기록됩니다.
기본 작업을 방지할 수 있는 다른 요소/이벤트 조합은 무엇인가요? 모든 것을 나열할 수는 없으며 때로는 직접 실험해 봐야 합니다. 간단히 말해 다음과 같은 몇 가지가 있습니다.
<form>
요소 + 'submit' 이벤트: 이 조합의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()
를 어떻게 사용할 수 있는지 파악하는 데 도움이 될 것입니다.
재미있는 장난?
문서에서 시작하여 캡처 단계에서 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()
도 호출합니다. 따라서 마우스 휠 스크롤, 키보드 스크롤 또는 강조 표시 또는 탭 이동, 링크 클릭, 컨텍스트 메뉴 표시 등 모든 기본 작업이 방지되므로 페이지가 상당히 쓸모없는 상태로 남게 됩니다.