Skip to content

03. 이벤트 시스템과 리스너 옵션

이벤트 처리는 "클릭하면 함수 실행"보다 훨씬 넓다. 어떤 타이밍에, 어떤 옵션으로, 어떤 생명주기로 리스너를 붙이고 떼는지가 품질을 결정한다.

학습 목표

  1. addEventListener를 현대적인 기본 이벤트 등록 방식으로 설명할 수 있다.
  2. capture, once, passive, signal 옵션의 의미를 이해한다.
  3. removeEventListenerAbortController 기반 정리 패턴 차이를 설명할 수 있다.
  4. 인라인 핸들러와 프로퍼티 핸들러를 왜 본문 예시에서 밀어내는지 이해한다.

1. 이벤트를 등록하는 세 가지 방식

원문에도 나오듯 이벤트를 붙이는 방식은 크게 세 가지다.

방식예시현재 권장도
HTML 속성에 직접 작성<button onclick="save()">낮음
프로퍼티에 함수 할당button.onclick = save보조적
리스너 등록button.addEventListener('click', save)기본

현대 문서에서 addEventListener를 기본으로 두는 이유는 단순하다.

  • 같은 이벤트에 여러 리스너를 붙일 수 있다
  • 옵션 객체로 동작을 세밀하게 제어할 수 있다
  • 인라인 핸들러보다 CSP와 보안 정책에 더 잘 맞는다

2. 왜 인라인 핸들러를 권장하지 않을까?

onclick="..."는 동작 자체는 가능하지만, 현재는 주력 패턴이 아니다.

  • 마크업과 로직이 강하게 섞인다
  • 재사용과 테스트가 어렵다
  • CSP를 강하게 적용하는 환경과 잘 맞지 않는다
  • 문자열 기반 코드는 XSS 관점에서도 실수를 유도한다

따라서 "이런 방식도 있었다"는 역사적 맥락 정도로만 짚고, 본문 예시는 addEventListener 위주로 두는 것이 낫다.


3. addEventListener의 기본 형태

js
const controller = new AbortController()

button.addEventListener('click', handleSave, {
  once: false,
  passive: true,
  signal: controller.signal,
})

세 번째 인자는 과거에는 true/false 정도로만 배웠지만, 지금은 옵션 객체로 읽는 것이 기본이다.

옵션의미언제 유용한가
capture캡처 단계에서 실행상위에서 먼저 가로채야 할 때
once한 번 실행 후 자동 해제첫 클릭만 처리하는 온보딩, 1회성 트래킹
passivepreventDefault()를 호출하지 않겠다고 약속스크롤, 터치, 휠 성능 최적화
signalAbortController로 리스너 생명주기 제어컴포넌트 cleanup, 다중 리스너 일괄 해제

4. passive는 성능 옵션이다

스크롤과 관련된 이벤트에서 브라우저는 "이 리스너가 기본 스크롤을 막을지"를 기다릴 수 있다.
passive: true는 "막지 않을 테니 스크롤을 바로 진행해도 된다"는 힌트다.

그래서 아래처럼 정리하면 된다.

  • touchstart, touchmove, wheel 등에서는 성능에 영향이 크다
  • passive: true인 리스너 안에서는 preventDefault()를 기대하면 안 된다
  • 기본 동작을 정말 막아야 한다면 passive를 신중하게 꺼야 한다

즉, 이 옵션은 단순 문법이 아니라 브라우저 렌더링 성능과 연결된다.


5. once는 의도 표현에도 좋다

once: true는 단순히 코드를 줄이는 기능이 아니다.
이 리스너가 "재사용되지 않는 일회성 처리"라는 의도를 드러낸다.

  • 첫 인터랙션만 추적
  • 첫 열기 애니메이션 이후 정리
  • 사용자 동의 배너의 최초 확인 처리

수동으로 내부에서 플래그를 두는 것보다 훨씬 읽기 쉽다.


6. signalAbortController가 중요한 이유

원문은 이벤트 제거를 위해 같은 타깃, 타입, 콜백을 맞추는 과정을 자세히 설명한다. 그 원리는 여전히 중요하다.
다만 현재는 AbortController를 함께 설명해야 실무와 맞다.

js
const controller = new AbortController()

window.addEventListener('resize', updateLayout, { signal: controller.signal })
document.addEventListener('visibilitychange', syncWhenVisible, { signal: controller.signal })

// cleanup
controller.abort()

장점은 분명하다.

  • 리스너 여러 개를 한 번에 정리할 수 있다
  • fetch 취소와 비슷한 모델로 이해할 수 있다
  • 컴포넌트 언마운트나 페이지 전환 cleanup과 잘 맞는다

7. removeEventListener에서 정말 중요한 일치 조건

MDN 기준으로 제거 시 핵심은 같은 타입, 같은 리스너, 같은 캡처 여부다.
옵션 객체를 썼더라도 capture가 아닌 다른 값은 제거 일치 조건의 핵심이 아니다.

즉, 아래 포인트를 기억하면 된다.

  • 콜백 참조는 같아야 한다
  • capture: true로 붙였으면 제거할 때도 캡처 여부가 맞아야 한다
  • 익명 함수를 바로 넣으면 나중에 제거가 어려워진다
  • 그래서 장기 생명주기에서는 signal 패턴이 더 편해진다

8. 콜백 함수와 this 바인딩

원문처럼 객체의 handleEvent 패턴이나 bind(this) 예제도 DOM 이벤트 문맥에서는 여전히 의미가 있다.
다만 학습 우선순위는 아래가 더 높다.

  • 화살표 함수로 주변 스코프를 캡처할 것인가
  • 메서드를 명시적으로 바인딩할 것인가
  • cleanup 가능한 참조를 유지하고 있는가

커스텀 엘리먼트나 클래스 기반 객체에서는 this.onClick = this.onClick.bind(this) 같은 초기화가 아직도 자주 등장한다.


9. 현대적인 기본 패턴

js
class SaveController {
  constructor(button) {
    this.abortController = new AbortController()
    this.button = button
  }

  mount() {
    this.button.addEventListener('click', this.handleClick, {
      signal: this.abortController.signal,
    })
  }

  handleClick = (event) => {
    console.log('save', event.currentTarget)
  }

  unmount() {
    this.abortController.abort()
  }
}

이 패턴은 원문의 "설정 -> 발생 -> 삭제" 4단계를 그대로 유지하되, cleanup을 더 실무적으로 만든 형태다.


10. 흔한 안티패턴

안티패턴문제점더 나은 방식
onclick= 남용보안, 유지보수, 재사용에 불리addEventListener 사용
익명 함수만 잔뜩 등록제거와 추적이 어려움명명된 함수 또는 signal 사용
스크롤 이벤트에 무조건 기본 옵션입력 지연 가능필요하면 passive: true 고려
리스너 cleanup 누락중복 호출, 메모리 누수AbortController 또는 명시적 제거

11. PR 리뷰 체크리스트

  • 기본 이벤트 등록 방식이 addEventListener 중심으로 구성되어 있는가
  • once, passive, signal이 필요한 상황을 놓치지 않았는가
  • cleanup이 필요한 리스너가 정리되고 있는가
  • 익명 함수 등록 때문에 제거가 어려워진 부분은 없는가
  • 인라인 핸들러나 프로퍼티 핸들러를 쓸 충분한 이유가 있는가

핵심 정리

  • 현재 이벤트 등록의 기본값은 addEventListener + 옵션 객체다
  • passive는 성능, once는 의도, signal은 생명주기 관리와 연결된다
  • 원문의 삭제 원리는 여전히 맞지만, 실무에서는 AbortController까지 알아야 완성된다