[ ᴊᴀᴠᴀꜱᴄʀɪᴘᴛ ] 이벤트 처리: DOM과 브라우저 상호작용

NewHa·2025년 1월 14일
post-thumbnail

JavaScript의 Event

JavaScript에서 이벤트는 웹 페이지와 상호작용하는 가장 기본적인 방식입니다. 사용자가 웹 페이지에서 마우스를 클릭하거나, 키보드를 입력하는 등의 동작이 이벤트로 표현되며, 이를 통해 웹 페이지는 동적으로 반응할 수 있습니다.

이벤트란?

이벤트는 사용자가 웹 페이지에서 발생시키는 모든 동작(예: 클릭, 입력, 스크롤 등)입니다. 특정 DOM 요소에서 이벤트가 발생하고 브라우저는 이런 이벤트를 감지하고, 이벤트를 처리하는 코드를 실행해 사용자와 상호작용을 가능하게 만듭니다.

이벤트 종류

이벤트는 다양한 종류가 있으며, 주요 이벤트는 다음과 같습니다.

🔗

자세한 이벤트 목록은 MDN Event reference 페이지를 참고하세요.

분류EventDescription
Mouseclick마우스 버튼을 클릭했을 때
Keyboardkeydown키를 누르고 있을 때
Keyboardkeyup누르고 있던 키에서 손을 뗄 때
Formfocus요소가 포커스를 얻었을 때
Formblur요소가 포커스를 잃었을 때
Forminputinput 또는 textarea 요소의 값이 변경되었을 때
Formchangeselect, checkbox, radio button의 상태가 변경되었을 때
Formsubmitform을 submit할 때
Windowload웹 페이지의 로드가 완료되었을 때
Windowerror브라우저가 자바스크립트 오류를 만났거나 요청한 자원이 존재하지 않는 경우
Windowresize브라우저 창의 크기를 조절했을 때
Windowscroll사용자가 페이지를 스크롤할 때

📍

Keyboard Event(키보드 이벤트)

키보드 입력과 관련된 이벤트로는 keydownkeyup이 있습니다.
우선 키보드를 입력할 때 keydownkeyup 두 이벤트가 모두 발생합니다. 키보드를 누를 때 keydown이벤트가 발생하고 뗄 때 keyup이벤트가 발생합니다.

keydown vs keyup

  • keydown: 키를 누르는 순간부터 이벤트가 발생하며, 키를 누르고 있는 동안 일정 간격으로 이벤트가 반복해서 발생됩니다. 지속적인 입력이 필요한 게임 이동키와, 즉각적으로 동작을 수행해야 하는 경우에 적합합니다.
document.addEventListener('keydown', (e) => {
  console.log(`e.type: ${event.type}`);
});
  • keyup: 키를 눌렀다가 완전히 손을 뗀 순간한 번만 이벤트가 발생합니다. 사용자 입력이 완료되었을 때 유효성 검사를 진행하는 것과 같이 입력 완료 시 동작을 처리할 때 유용합니다.
document.addEventListener('keyup', (e) => {
  console.log(`e.type: ${event.type}`);
});

event handler(이벤트 핸들러)

이벤트 핸들러는 이벤트가 발생했을 때 실행되는 함수입니다.
이벤트를 처리할 때, 우선 JavaScript를 사용해 이벤트 핸들러를 붙일 DOM 요소를 선택합니다. 선택된 해당 요소에 이벤트 핸들러를 등록해서 동작을 정의합니다. 이제 사용자가 해당 요소와 상호작용을 하여 이벤트를 트리거(발동)시키면 등록된 동작이 실행되는 것입니다.

💡

뜬금없지만, 여기서 자바스크립트에서는 해당 DOM 요소에 직접 이벤트 핸들러를 등록한다는 점은 React와 비교되는 점입니다. React는 해당 요소가 아니라 모든 이벤트 핸들러가 root DOM container에 등록됩니다. 자세한 내용은 해당 게시글을 참고해주세요.

DOM 요소에 이벤트 핸들러를 등록하는 데는 3가지 방법이 알려져있습니다. 하지만 addEventListener 방법이 권장됩니다. 메모리 관리면에서 또, 하나의 컴포넌트에 여러 핸들러를 등록하는 게 가능하기 때문입니다.

1) HTML 속성을 사용
HTML 태그에 직접 이벤트 핸들러를 추가하는 방식입니다. 이 방법은 간단하지만, HTML과 JavaScript 코드가 섞여 관리가 어렵고 유지보수성이 떨어집니다.

<button onclick="alert('Button clicked!')">Click Me</button>

2) DOM 요소의 이벤트 프로퍼티를 사용
JavaScript에서 DOM 요소의 이벤트 프로퍼티(onclick 등)를 설정하여 이벤트 핸들러를 등록합니다. 코드 분리가 가능하고 가독성이 좋지만, 단일 이벤트 핸들러만 등록 가능하고 기존 핸들러를 덮어씁니다.

<button id="myButton">Click Me</button>
<script>
  const button = document.getElementById('myButton');
  button.onclick = () => {
    alert('Button clicked!');
  };
</script>

3) addEventListener 메서드를 사용
상대적으로 코드가 길지만 가장 권장되는 방식으로, 하나의 요소에 여러 개의 이벤트 핸들러를 등록할 수 있습니다.

<button id="myButton">Click Me</button>
<script>
  const button = document.getElementById('myButton');
  button.addEventListener('click', () => {
  console.log('Button clicked!');
  });
</script>

이벤트 리스너를 등록할 때, 옵션을 활용해 이벤트 전파 옵션(capture, bubbling)도 설정 가능합니다.

  • capture: 이벤트를 캡처링 단계에서 처리할지 여부.
  • once: 이벤트가 한 번만 실행되고 제거되도록 설정.
  • passive: 기본 동작(예: 스크롤)을 차단하지 않겠다고 명시.
// 버튼을 한 번 클릭하면 이벤트 핸들러가 자동으로 제거되도록 옵션 추가
button.addEventListener('click', () => console.log('Clicked'), { once: true });

등록한 이벤트 리스너를 관리하지 않으면, DOM 요소가 제거되었더라도 메모리가 해제되지 않아 메모리 누수가 발생할 수 있습니다. remove()메서드로 요소를 제거할 수 있습니다.

const button = document.getElementById('button');
button.addEventListener('click', () => {
  console.log('Button clicked');
});
// 요소 제거
button.remove();

요소를 제거해도 이벤트 리스너가 여전히 존재할 수 있습니다. 필요하지 않으면
removeEventListener를 사용해 제거합니다. 이 때, 이벤트를 제거하려면 같은 이벤트 함수 참조가 필요하므로 이벤트를 변수에 저장하여 사용해야 합니다.

const handler = () => console.log('Clicked');
button.addEventListener('click', handler);
button.removeEventListener('click', handler);

브라우저 동작과 이벤트 흐름

다음 코드의 실행 동작을 예를 들어 설명합니다.

console.log("code start");

const handleClick = () => {
    console.log("event handler start");

    Promise.resolve().then(() => {
      console.log("Promise1 실행");
    });

    setTimeout(() => {
      console.log("setTimeout 실행");
    }, 0);

    Promise.resolve().then(() => {
      console.log("Promise2 실행");
    });

    console.log("event handler end");
  }

  console.log("code end");

  // <button id="button">클릭!</button>
  const button = document.getElementById("button");
  button.addEventListener("click", handleClick);

브라우저 동작

  1. 브라우저가 JavaScript를 위에서 아래로 실행하기 시작합니다. console.log('code start')가 스택에 등록되고 실행되어 콘솔에 출력하고 스택에서 제거됩니다.

📍

Call Stack (콜 스택)

함수가 호출되면 요청된 작업은 순차적으로 콜 스택에 쌓여서 실행됩니다. 자바스크립트는 하나의 콜 스택을 사용해 한 번에 한 가지의 작업만 수행할 수 있습니다.

  1. console.log('code end')가 스택에 등록되고 실행되어 콘솔에 출력하고 스택에서 제거됩니다.

  2. HTML 요소를 선택해 버튼 DOM 요소가 생성되고 addEventListener로 handleClick 함수가 버튼의 클릭 이벤트 핸들러로 등록됩니다. 이제 브라우저는 클릭 이벤트가 발생하면 실행할 함수를 기억합니다.

이벤트 핸들러 함수 실행

  1. 버튼(HTML 요소)을 클릭하면 이벤트가 발생하고, 브라우저의 네이티브 이벤트 시스템이 click 이벤트를 감지하고 이벤트 객체를 생성합니다.

📍

이벤트 객체 (Event Object)

이벤트를 발생시킨 요소와 발생한 이벤트에 대한 유용한 정보를 제공하고, 이벤트를 처리할 수 있는 이벤트 핸들러에 인자로 암묵적으로 전달됩니다.

  • 이벤트 객체는 다음과 같은 프로퍼티들을 가진 객체입니다.

    🔗

    이벤트 프로퍼티

    이벤트 객체의 프로퍼티로 event.target과 event.currentTarget이 제공됩니다.
    이와 관련해서는 event.target 과 event.currentTarget의 차이 게시글을 참고하세요.

  1. 생성된 네이티브 이벤트 객체가 DOM 트리를 따라 캡처링타깃버블링의 순서로 전파됩니다.

📍

이벤트 전파 (Event Propagation)

DOM에서 이벤트가 발생하면 브라우저는 이를 처리하기 위해 DOM 트리로 이벤트 전파 과정을 거칩니다. 이 때 전파 방향에 따라 버블링과 캡처링으로 구분됩니다.

  • 캡처링 단계 (Capturing Phase): 이벤트가 최상위 요소(document)에서 시작해 타깃인 자식 요소(button)까지 내려가는 과정입니다.
  • 타깃 단계 (Target Phase): 이벤트가 실제로 발생한 타깃 요소(button)에서 실행됩니다.
  • 버블링 단계 (Bubbling Phase): 이벤트가 타깃인 자식 요소에서 최상위 요소까지 전파되면서, 상위 요소의 이벤트를 실행됩니다(이벤트 버블링).

이러한 이벤트 전파를 제어하는 메소드가 있습니다.

  • stopPropagation(): 이벤트 전파를 중단합니다.
  • stopImmediatePropagation(): 같은 요소의 다른 핸들러도 실행되지 않도록 차단합니다.
  1. 타깃에서 등록된 핸들러 함수를 호출하고, 호출 스택에 추가하여 실행합니다. 함수 내부의 코드가 순차적으로 실행됩니다. console.log('event handler start')가 실행되어 콘솔에 출력하고 제거됩니다.
  2. 다음으로 Promise.resolve().then이 콜 스택에 등록 되고 호출되면 Web API로 전달됩니다. Web API는 Promise를 처리하고 내부 콜백 함수(() => console.log('promise1 실행'))는 Microtask Queue 콜백에 추가됩니다.

📍

Task Queue

비동기 처리 함수의 콜백 함수, 비동기식 이벤트 핸들러, Timer 함수의 콜백 함수가 보관되는 영역입니다. 이벤트 루프에 의해 콜 스택이 비어졌을 때 순차적으로 콜 스택으로 이동되어 실행됩니다.

  1. setTimeout이 콜스택에 등록 되어 호출되고 Web API로 전달됩니다. Web API는 setTimeout을 실행하고 Event Queue 콜백에 콜백 함수(() => console.log('setTimeout 실행'))를 추가합니다.

  2. 그 다음 Promise.resolve().then이 콜스택에 등록되고 호출되어 Web API로 전달됩니다. Web API는 Promise를 처리하고 내부 콜백 함수(() => console.log('promise2 실행'))는 Microtask Queue 콜백에 추가됩니다.

  3. 마지막으로 console.log('event handler end') 가 실행되고 콘솔에 출력되고 콜스택에서 handleClick 함수가 실행 종료되어 제거됩니다.

  4. 이제 콜 스택이 비었고, 이벤트 루프는 우선 Microtask Queue 에서 작업을 가져와 처리하기 시작합니다. 첫 번째 Promise 콜백을 가져와 실행합니다. console.log('promise1 실행')이 실행되어 콘솔에 출력하고 제거됩니다.

📍

Event Loop(이벤트 루프)

브라우저는 단일 쓰레드에서 동작합니다. 따라서 한 번에 하나의 작업만 처리할 수 있습니다. 하지만 이벤트 루프를 통해 한 번에 여러 작업이 처리되는 것 처럼 동작할 수 있습니다.

  • 이벤트 루프는 브라우저 내에서 콜 스택 내에서 현재 실행 중인 작업이 있는 지, 마이크로태스크 큐와 이벤트 큐에 작업이 있는 지 반복해서 확인합니다.
  • 콜 스택이 비어있으면 마이크로태스크 큐 내의 작업이 이동되어 실행됩니다.
  • 마이크로태스크 큐도 비어있으면 이벤트 큐 내의 작업이 이동되어 실행됩니다.
  1. 그 댜음 Microtask Queue에 남아있는 두 번째 Promise 콜백을 가져와 실행합니다. console.log('promise2 실행')이 실행되어 콘솔에 출력하고 제거됩니다.

  2. 호출 스택이 비어있고, Microtask Queue에 더 이상 작업이 없으면 Event Queue 에서 작업을 가져와 처리하기 시작합니다. setTimeout의 콜백을 실행해 console.log('setTimeout 실행')이 실행되고 콘솔에 출력하고 제거됩니다.

이러한 과정을 거쳐 콘솔에는 다음과 같이 출력됩니다.

code start
code end
event handler start
event handler end
Promise1 실행
Promise2 실행
setTimeout 실행

주요 이벤트 메서드

1) stopPropagation()

이벤트 전파를 중단하여 상위 요소로 이벤트가 전달되지 않도록 합니다

document.getElementById("child").addEventListener("click", (event) => {
  event.stopPropagation();
  console.log("Child clicked");
});

2) preventDefault()

이벤트의 기본 동작을 취소합니다.

document.querySelector("a").addEventListener("click", (event) => {
  event.preventDefault();
  console.log("Link click prevented");
});

3) stopImmediatePropagation()

같은 요소에 등록된 다른 이벤트 핸들러의 실행도 중단합니다

button.addEventListener('click', () => console.log('First handler'));
button.addEventListener('click', (e) => {
  e.stopImmediatePropagation();
  console.log('Second handler');
});
button.addEventListener('click', () => console.log('Third handler'));

이벤트 활용

1) 이벤트 위임(Event Delegation)

많은 자식 요소에 각각 이벤트 리스너를 추가하는 대신, 부모 요소에서 한 번만 이벤트를 처리합니다:

document.getElementById("parent").addEventListener("click", (event) => {
  if (event.target.tagName === "BUTTON") {
    console.log("Button clicked:", event.target.textContent);
  }
});

2) 스크롤 이벤트

페이지 하단에 도달했을 때 데이터를 추가 로드하는 무한 스크롤 구현:

window.addEventListener("scroll", () => {
  if (window.innerHeight + window.scrollY >= document.body.offsetHeight) {
    console.log("Load more data");
  }
});

📍

스크롤 이벤트 성능 최적화

1) Debouncing
이벤트가 너무 자주 발생할 때, 마지막 이벤트 이후 일정 시간 동안 실행을 지연합니다.

const debounce = (func, delay) => {
  let timer;
  return (...args) => {
    clearTimeout(timer);
    timer = setTimeout(() => func(...args), delay);
  };
};
window.addEventListener('resize', debounce(() => {
  console.log('Resized');
}, 300));

2) Throttling
일정 시간 동안 이벤트가 여러 번 발생하더라도 한 번만 실행합니다.

const throttle = (func, delay) => {
  let lastTime = 0;
  return (...args) => {
    const now = Date.now();
    if (now - lastTime >= delay) {
      lastTime = now;
      func(...args);
    }
  };
};
window.addEventListener('scroll', throttle(() => {
  console.log('Scrolled');
}, 300));
profile
백 번을 보면 한 가지는 안다 👀

0개의 댓글