JavaScript 이벤트 자세히 알아보기

preventDefaultstopPropagation: 언제 어떤 메서드를 사용해야 하는지, 각 메서드가 정확히 무엇을 하는지

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 요소의 클릭 이벤트를 수신 대기하는 항목이 있나요?'라고 묻습니다. 이번에는 '예'입니다. 이벤트가 타겟에 있는 이 짧은 시간 간격을 '타겟 단계'라고 합니다. 이 시점에서 이벤트 핸들러가 실행되고 브라우저가 '#C was clicked'을 console.log하고 완료됩니다. 맞나요? 틀렸습니다. 아직 끝난 것이 아닙니다. 프로세스는 계속되지만 이제 버블링 단계로 변경됩니다.

이벤트 버블링

브라우저에서 다음을 묻습니다.

'버블링 단계에서 #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()는 앞서 언급한 대로 기본 작업을 방지합니다. 따라서 모든 기본 작업 (예: 마우스 휠 스크롤, 키보드 스크롤 또는 강조 표시 또는 탭 이동, 링크 클릭, 컨텍스트 메뉴 표시 등)이 모두 차단되어 페이지가 상당히 무용지물이 됩니다.

실시간 데모

이 도움말의 모든 예시를 한곳에서 다시 살펴보려면 아래에 삽입된 데모를 확인하세요.

감사의 말씀

Unsplash톰 윌슨님이 제공한 히어로 이미지