JavaScript 이벤트 자세히 알아보기

preventDefaultstopPropagation: 각 메서드의 역할을 하는 시점과 정확히 그 역할을 합니다.

스티븐 슈투르
Stephen Stchur

Event.stopPropagation() 및 Event.preventDefault()

자바스크립트 이벤트 처리는 간단한 경우가 많습니다. 특히 간단한 (비교적 플랫) HTML 구조를 다룰 때는 더욱 그렇습니다. 이벤트가 요소의 계층 구조를 통해 이동 (또는 전파)될 때는 작업이 좀 더 복잡해집니다. 이는 일반적으로 개발자가 발생한 문제를 해결하기 위해 stopPropagation() 또는 preventDefault()에 도달하는 경우입니다. 'preventDefault()을(를) 사용해 봤는데 효과가 없으면 stopPropagation()을(를) 시도해 보고 그래도 문제가 해결되지 않으면 둘 다 시도해 보세요.'라고 생각한 적이 있다면 이 도움말을 참고하세요. 각 방법의 기능과 언제 어떤 메서드를 사용해야 하는지 정확하게 설명하고 개발자가 살펴볼 수 있는 다양한 작업 예시를 제공합니다. 제 목표는 고객님의 혼란을 완전히 끝내는 것입니다.

하지만 자세히 살펴보기 전에 자바스크립트에서 가능한 두 가지 이벤트 처리의 두 가지 종류에 관해 간단히 알아보겠습니다. 모든 최신 브라우저에서는 버전 9 이전의 Internet Explorer에서는 이벤트 캡처를 전혀 지원하지 않았습니다.

이벤트 스타일 (캡처 및 버블링)

모든 최신 브라우저는 이벤트 캡처를 지원하지만 개발자가 거의 사용하지 않습니다. 흥미롭게도, 원래 Netscape가 지원했던 유일한 이벤트 형태였습니다. Netscape의 가장 큰 라이벌인 Microsoft Internet Explorer는 이벤트 캡처를 전혀 지원하지 않고 이벤트 버블링이라는 또 다른 스타일의 이벤트만 지원했습니다. W3C가 형성되었을 때 W3C는 두 이벤트 스타일 모두에서 이점을 확인했으며 addEventListener 메서드에 대한 세 번째 매개변수를 통해 브라우저가 두 이벤트를 모두 지원해야 한다고 선언했습니다. 원래 이 매개변수는 부울이었지만 모든 최신 브라우저는 options 객체를 세 번째 매개변수로 지원합니다. 이 객체를 사용하면 이벤트 캡처 사용 여부를 지정할 수 있습니다.

someElement.addEventListener('click', myClickHandler, { capture: true | false });

options 객체와 capture 속성은 선택사항입니다. 둘 중 하나를 생략하는 경우 capture의 기본값은 false입니다. 즉, 이벤트 버블링이 사용됩니다.

이벤트 캡처

이벤트 핸들러가 '캡처 단계에서 리슨' 중이라는 것은 무엇을 의미하나요? 이를 이해하려면 이벤트가 어떻게 발생하고 어떻게 이동하는지를 알아야 합니다. 개발자가 활용하지 않거나 관심을 두지 않으며 생각하지 않더라도 다음은 모든 이벤트에 해당됩니다.

모든 이벤트는 해당 창에서 시작되어 먼저 캡처 단계를 거칩니다. 즉, 이벤트가 전달되면 이벤트가 시작되고 먼저 타겟 요소를 향해 '아래로' 이동합니다. 버블링 단계에서만 음악을 듣는 경우에도 마찬가지입니다. 다음 마크업과 자바스크립트의 예를 참고하세요.

<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가 클릭됨'을 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> 요소 + '클릭' 이벤트: 이 조합에서 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()도 호출합니다. 따라서 마우스 휠 스크롤, 키보드 스크롤 또는 강조표시 또는 탭, 링크 클릭, 컨텍스트 메뉴 표시 등의 모든 기본 작업이 모두 차단되므로 페이지가 상당히 쓸모없는 상태가 됩니다.

라이브 데모

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

감사의 말

UnsplashTom Wilson의 히어로 이미지