Vue의 이벤트 핸들링은 HTML의 onclick과 비슷해 보이지만, 이벤트 수식어와 타입 안전성이라는 강력한 무기가 있습니다.

면접에서 "이벤트 수식어를 왜 쓰나요?"라는 질문을 받으면, 단순히 "편해서요"가 아니라 선언적 프로그래밍의 장점 을 설명할 수 있어야 합니다.


기본 이벤트 핸들링

VUE
<script setup lang="ts">
import { ref } from 'vue'

const count = ref(0)
const name = ref('')

// 인라인 핸들러 — 간단한 로직
// 메서드 핸들러 — 복잡한 로직
const increment = () => {
  count.value++
}

// 이벤트 객체를 받는 핸들러
const handleClick = (event: MouseEvent) => {
  console.log('클릭 좌표:', event.clientX, event.clientY)
}

// 인라인에서 이벤트 객체 전달
const handleWithArgs = (message: string, event: Event) => {
  console.log(message, event.target)
}
</script>

<template>
  <!-- 인라인 핸들러 -->
  <button @click="count++">카운트: {{ count }}</button>

  <!-- 메서드 핸들러 — 이벤트 객체가 자동 전달됨 -->
  <button @click="handleClick">클릭</button>

  <!-- 인라인에서 $event로 이벤트 객체 전달 -->
  <button @click="handleWithArgs('안녕', $event)">인사</button>

  <!-- 여러 핸들러 바인딩 -->
  <button @click="increment(), handleClick($event)">둘 다 실행</button>
</template>

이벤트 수식어(Event Modifiers)

이벤트 수식어는 event.preventDefault()event.stopPropagation() 같은 DOM 이벤트 처리를 선언적으로 표현합니다.

VUE
<template>
  <!-- .prevent — event.preventDefault() -->
  <form @submit.prevent="handleSubmit">
    <button type="submit">제출</button>
  </form>

  <!-- .stop — event.stopPropagation() -->
  <div @click="handleOuter">
    <button @click.stop="handleInner">
      버블링 차단
    </button>
  </div>

  <!-- .once — 한 번만 실행 -->
  <button @click.once="showWelcome">환영 메시지 (1회)</button>

  <!-- .self — event.target이 자신일 때만 -->
  <div @click.self="handleDiv">
    <button>이 버튼 클릭 시 handleDiv 실행 안 됨</button>
  </div>

  <!-- .capture — 캡처 단계에서 처리 -->
  <div @click.capture="handleCapture">
    <button @click="handleButton">캡처 우선</button>
  </div>

  <!-- .passive — 스크롤 성능 최적화 -->
  <div @scroll.passive="handleScroll">
    스크롤 영역
  </div>

  <!-- 수식어 체이닝 -->
  <a @click.stop.prevent="handleLink">링크</a>
</template>

수식어 순서가 중요합니다

VUE
<template>
  <!-- @click.prevent.self — 모든 클릭의 기본 동작을 막고, 자신일 때만 핸들러 실행 -->
  <!-- @click.self.prevent — 자신 클릭일 때만 기본 동작을 막음 -->
</template>

키보드 이벤트 수식어

VUE
<script setup lang="ts">
const handleSearch = () => {
  console.log('검색 실행')
}

const handleSave = () => {
  console.log('저장')
}
</script>

<template>
  <!-- 키 별칭 수식어 -->
  <input @keyup.enter="handleSearch" placeholder="Enter로 검색" />
  <input @keyup.tab="handleTab" />
  <input @keyup.delete="handleDelete" />   <!-- Delete + Backspace -->
  <input @keyup.esc="handleEsc" />
  <input @keyup.space="handleSpace" />
  <input @keyup.up="handleUp" />
  <input @keyup.down="handleDown" />
  <input @keyup.left="handleLeft" />
  <input @keyup.right="handleRight" />

  <!-- 시스템 수식어 키 -->
  <input @keyup.ctrl.enter="handleSave" placeholder="Ctrl+Enter로 저장" />
  <div @click.ctrl="handleCtrlClick">Ctrl+클릭</div>
  <div @click.alt="handleAltClick">Alt+클릭</div>
  <div @click.shift="handleShiftClick">Shift+클릭</div>
  <div @click.meta="handleMetaClick">Cmd/Win+클릭</div>

  <!-- .exact — 정확히 해당 수식어만 눌렸을 때 -->
  <button @click.ctrl.exact="handleCtrlOnly">
    Ctrl만 누른 상태에서 클릭
  </button>
  <button @click.exact="handlePureClick">
    아무 수식어 키 없이 클릭
  </button>
</template>

마우스 버튼 수식어

VUE
<template>
  <div @click.left="handleLeftClick">왼쪽 클릭</div>
  <div @click.right="handleRightClick">오른쪽 클릭</div>
  <div @click.middle="handleMiddleClick">가운데 클릭</div>
</template>

컴포넌트 이벤트(emit)

자식 컴포넌트에서 부모로 이벤트를 전달할 때 emit을 사용합니다.

VUE
<!-- ChildComponent.vue -->
<script setup lang="ts">
// 이벤트 타입 선언
const emit = defineEmits<{
  // 이벤트명: (인자) => void
  'update:count': [value: number]
  'submit': [data: { name: string; email: string }]
  'close': []
}>()

const handleSubmit = () => {
  emit('submit', {
    name: '심정훈',
    email: 'test@example.com'
  })
}

const handleClose = () => {
  emit('close')
}
</script>

<template>
  <button @click="handleSubmit">제출</button>
  <button @click="handleClose">닫기</button>
</template>
VUE
<!-- ParentComponent.vue -->
<script setup lang="ts">
import ChildComponent from './ChildComponent.vue'

const handleSubmit = (data: { name: string; email: string }) => {
  console.log('제출됨:', data)
}

const handleClose = () => {
  console.log('닫힘')
}
</script>

<template>
  <ChildComponent
    @submit="handleSubmit"
    @close="handleClose"
  />
</template>

이벤트 유효성 검사

VUE
<script setup lang="ts">
// 런타임 유효성 검사 추가
const emit = defineEmits({
  // null 반환 또는 반환값 없으면 유효성 검사 없음
  click: null,

  // 유효성 검사 함수 — false 반환 시 경고
  submit: (payload: { email: string }) => {
    if (!payload.email.includes('@')) {
      console.warn('유효하지 않은 이메일!')
      return false
    }
    return true
  }
})
</script>

네이티브 이벤트 전달

컴포넌트의 루트 엘리먼트에 네이티브 이벤트를 전달하려면 v-bind="$attrs"를 활용합니다.

VUE
<!-- CustomButton.vue -->
<script setup lang="ts">
// inheritAttrs를 false로 설정하면 자동 전달 비활성화
defineOptions({
  inheritAttrs: false
})
</script>

<template>
  <div class="button-wrapper">
    <!-- $attrs로 명시적 전달 -->
    <button v-bind="$attrs">
      <slot />
    </button>
  </div>
</template>

면접 팁

  • 이벤트 수식어는 관심사 분리 원칙을 따릅니다 — 핸들러는 비즈니스 로직만 담당하고, DOM 이벤트 처리는 수식어가 담당
  • .passive는 모바일 스크롤 성능에 중요합니다. preventDefault를 호출하지 않겠다고 브라우저에 미리 알려주는 것
  • defineEmits의 TypeScript 제네릭 문법을 사용하면 이벤트 페이로드의 타입 안전성을 확보할 수 있습니다

요약

Vue의 이벤트 핸들링은 @(v-on) 디렉티브로 시작합니다. 이벤트 수식어(.prevent, .stop, .once 등)는 DOM 이벤트 처리를 선언적으로 만들어주고, 키보드/마우스 수식어로 세밀한 제어가 가능합니다. 컴포넌트 간 통신은 defineEmits로 타입 안전하게 구현합니다.

댓글 로딩 중...