브라우저는 다양한 이벤트에 대해 미리 정의된 기본 동작과 상호작용을 가지고 있습니다.
예를 들어, 사용자가 폼에서 "제출" 버튼을 클릭하면 브라우저는 기본적으로 해당 폼 데이터를 특정 URL로 전송합니다.
또한 어떤 요소의 자식 요소를 클릭하면, 그 자식이 컨테이너 내부에 있기 때문에 클릭 이벤트가 부모 요소에도 함께 발생합니다.
하지만 경우에 따라서는 이러한 기본 동작을 재정의하고 싶을 때가 있습니다. 이 글에서는 event.preventDefault()와 event.stopPropagation() 메서드가 각각 무엇인지, 그리고 이를 활용해 브라우저의 기본 동작을 어떻게 취소하는지 알아보겠습니다.
event.preventDefault()
event.preventDefault() 메서드는 이벤트가 발생했을 때 브라우저가 수행하는 기본 동작을 차단합니다.
웹페이지에서 흔히 볼 수 있는 기본 동작의 예시와, 이를 event.preventDefault()로 재정의하는 방법을 살펴보겠습니다.
폼 제출 기본 동작 재정의하기
사용자가 폼을 제출하면(제출 버튼 클릭), 폼의 기본 동작은 폼 데이터를 이를 처리할 URL로 전송하는 것입니다.
form 요소는 action과 method 속성을 가지며, 각각 폼을 전송할 대상 URL과 요청 방식(get, post 등)을 지정합니다.
만약 이 속성들이 지정되어 있지 않으면, 기본 URL은 현재 페이지의 URL이 되고 요청 방식은 get이 됩니다.
예를 들어 다음 코드를 보겠습니다:
<form>
<input name="email" />
<input name="password" />
<input type="submit" />
</form>
이 코드는 아래와 같은 페이지를 만듭니다:
입력값 "dillion"과 "password"를 넣고 폼을 제출하면, 다음과 같이 127.0.0.1:5500/index.html로 get 요청이 전송되는 것을 확인할 수 있습니다:
이것이 바로 브라우저가 폼을 처리하는 기본 방식입니다.
하지만 요청을 보내기 전에 데이터에 추가 작업을 수행하고 싶은 경우가 많습니다. 실제로 현대적인 폼 처리 방식에서는 이런 경우가 매우 흔합니다.
예를 들어 요청을 전송하기 전에 데이터 유효성 검사, 값 확인, 데이터 가공, 헤더 설정 등을 먼저 하고 싶을 수 있습니다.
이런 상황에서는 폼의 기본 동작을 막아야 합니다. 방법은 다음과 같습니다:
<form id='form'>
...
</form>
const form = document.getElementById('form')
form.addEventListener('submit', (event) => {
event.preventDefault()
// 데이터를 가공한 후 요청을 직접 전송
})
이렇게 하면 폼이 언제, 어떻게 제출될지를 개발자가 직접 제어할 수 있습니다.
링크 클릭 시 기본 동작 재정의하기
링크(href 속성이 있는 앵커 태그 a)를 클릭하면, 기본 동작은 브라우저가 해당 링크 주소로 이동(navigation)하는 것입니다.
그렇다면 이 동작을 가로채서, 페이지 이동 전에 다른 작업을 수행하려면 어떻게 해야 할까요? 예를 들어 사용자가 이동하려는 페이지에 대한 접근 권한이 있는지 먼저 확인하는 경우를 생각해 볼 수 있습니다. 구현 방법은 다음과 같습니다:
<a id="link" href="https://google.com">Google</a>
const link = document.getElementById("link")
link.addEventListener("click", event => {
event.preventDefault()
// 필요한 작업을 수행한 후 직접 이동 처리
})
직접 테스트해 보세요. "Google" 링크를 클릭해도 페이지 이동이 일어나지 않습니다. 기본 이동 동작을 차단했기 때문입니다. 이제 남은 이동 과정은 개발자가 직접 처리해야 합니다.
event.stopPropagation()
전파(propagation)란 무언가를 퍼뜨리는 행위를 의미하며, 여기서는 곧 '이벤트'를 말합니다. stopPropagation 메서드는 요소에서 이벤트가 발생했을 때, 그 이벤트가 다른 요소로 퍼져 나가는 것을 막는 데 사용됩니다.
JavaScript에서 어떤 요소에 이벤트가 발생하면, 그 이벤트는 DOM 트리를 따라 부모 및 조상 요소들로 버블링(bubbling)됩니다. 즉, 이벤트가 발생한 요소가 부모의 컨테이너 "안에" 위치해 있기 때문에, 부모 역시 동일한 이벤트를 수신하게 됩니다.
예시를 통해 좀 더 자세히 설명해 보겠습니다.
요소의 자식 클릭하기
다음과 같은 요소들이 있다고 가정해 봅시다:
<div>
<button>Click me</button>
</div>
button을 클릭하면, 버튼이 div 컨테이너 안에 있기 때문에 사실상 div도 함께 클릭한 것이 됩니다. 이 논리에 따르면 클릭 이벤트는 버튼에서 컨테이너로 전파되고, 최상위 루트(root)에 도달할 때까지 모든 조상 요소로 계속 퍼져 나갑니다.
이를 실제로 확인해 보기 위해 다음 코드로 동작을 살펴보겠습니다:
<div id="div">
<button id="button">Click me</button>
</div>
const div = document.getElementById('div')
const button = document.getElementById('button')
button.addEventListener('click', () => {
console.log('button clicked')
})
div.addEventListener('click', () => {
console.log('div container clicked')
})
브라우저에서 이 코드를 실행한 뒤 버튼을 클릭하면, 다음과 같은 결과를 얻게 됩니다:
div 컨테이너도 클릭 이벤트를 수신했기 때문에, div에 등록된 클릭 콜백 함수까지 함께 호출된 것입니다.
이처럼 이벤트 전파는 이벤트와 요소의 기본 동작이지만, 상황에 따라서는 원치 않는 동작이 될 수 있습니다. 수많은 예시 중 하나를 소개하겠습니다.
다음은 Gmail의 새 메시지(New Message) 팝업 창입니다:
팝업 상단에는 세 개의 액션 버튼이 있습니다. 하나는 팝업을 최소화하고, 하나는 전체 화면으로 전환하며, 하나는 팝업을 닫는 역할을 합니다.
그런데 "New Message" 텍스트가 있는 상단 바에도 클릭 핸들러가 등록되어 있어, 상단 바를 클릭하면 팝업이 최소화됩니다:
여기서 반드시 피해야 할 상황이 있습니다. 버튼을 클릭할 때 클릭 이벤트가 상단 바로 전파되어, 상단 바에 등록된 함수까지 실행되는 것입니다. 다시 말해, 예를 들어 닫기 버튼을 클릭했을 때 상단 바의 최소화 동작까지 함께 실행되지 않기를 원하는 것입니다.
이런 경우에는 이벤트 전파를 차단해야 합니다.
팝업이 다음과 같이 구성되어 있다고 가정해 봅시다:
<div id='top-bar'>
<!-- 메시지 요소 -->
<!-- 버튼들 -->
</div>
const topBar = document.getElementById('top-bar')
const closeButton = document.getElementById('close-btn')
topBar.addEventListener('click', () => {
// 팝업 최소화 또는 최대화
})
closeButton.addEventListener('click', () => {
// 팝업 닫기
})
이 상태에서는 버튼의 클릭 이벤트가 상단 바로 퍼질 수 있습니다. 이를 막으려면 버튼의 리스너에 stopPropagation 메서드를 추가해야 합니다. 버튼의 리스너를 다음과 같이 수정하세요:
closeButton.addEventListener('click', (event) => {
event.stopPropagation()
// 팝업 닫기
})
이렇게 처리하면 상단 바는 오직 자신이 직접 클릭되었을 때만 클릭 이벤트를 수신하게 됩니다.
마무리
event.preventDefault()와 event.stopPropagation()의 차이를 정리하면 다음과 같습니다. 전자는 브라우저가 수행하는 기본 동작(폼 제출, 링크 이동 등)을 막고, 후자는 이벤트 자체의 기본 동작(DOM 트리를 따라 위로 전파되는 버블링)을 막습니다.
이러한 기본 동작들은 잘못된 것이 아니며, 평소 코딩할 때 굳이 신경 쓰지 않아도 됩니다. 하지만 이 글의 예제에서 살펴본 것처럼, 이를 재정의해야 하는 상황은 분명히 존재합니다. 두 메서드의 차이를 정확히 이해해 두면, 실무에서 이벤트를 훨씬 유연하게 다룰 수 있습니다.