이벤트 핸들링
Vue의 이벤트 핸들링은 HTML의
onclick과 비슷해 보이지만, 이벤트 수식어와 타입 안전성이라는 강력한 무기가 있습니다.
면접에서 "이벤트 수식어를 왜 쓰나요?"라는 질문을 받으면, 단순히 "편해서요"가 아니라 선언적 프로그래밍의 장점 을 설명할 수 있어야 합니다.
기본 이벤트 핸들링
<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 이벤트 처리를 선언적으로 표현합니다.
<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>
수식어 순서가 중요합니다
<template>
<!-- @click.prevent.self — 모든 클릭의 기본 동작을 막고, 자신일 때만 핸들러 실행 -->
<!-- @click.self.prevent — 자신 클릭일 때만 기본 동작을 막음 -->
</template>
키보드 이벤트 수식어
<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>
마우스 버튼 수식어
<template>
<div @click.left="handleLeftClick">왼쪽 클릭</div>
<div @click.right="handleRightClick">오른쪽 클릭</div>
<div @click.middle="handleMiddleClick">가운데 클릭</div>
</template>
컴포넌트 이벤트(emit)
자식 컴포넌트에서 부모로 이벤트를 전달할 때 emit을 사용합니다.
<!-- 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>
<!-- 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>
이벤트 유효성 검사
<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"를 활용합니다.
<!-- 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로 타입 안전하게 구현합니다.
댓글 로딩 중...