본문으로 건너뛰기
홈
기술
기술 전체
프로그래밍68
컴퓨터 과학63
AI48
웹 개발36
인프라33
데이터31
소프트웨어 공학18
소개
← 목록으로웹 개발 › 프론트엔드 › Vue

v-on

목차

v-on

버튼으로 수량을 3씩 늘리고, 폼을 제출하면 현재 수량을 확인하는 화면을 만들어 보자. 이벤트 처리는 브라우저 이벤트 → Vue 메서드 → 상태 변경 → 화면 갱신으로 읽으면 이해하기 쉽다.

아래 예제는 Vue 2 Options API를 기준으로 한다. Vue 2는 지원이 종료된 버전이며 기존 코드 학습용으로 구분한다. Vue 2 지원 종료 안내

v-on:click의 축약은 @click

<div id="app">
  <p>수량: {{ count }}</p>
  <button type="button" v-on:click="increase(1)">1 증가</button>
  <button type="button" @click="increase(3)">3 증가</button>
  <form @submit.prevent="confirm">
    <button type="submit">수량 확인</button>
  </form>
  <p role="status">{{ message }}</p>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({
  el: '#app',
  data: { count: 0, message: '' },
  methods: {
    increase(amount) {
      this.count += amount;
      this.message = '';
    },
    confirm() {
      this.message = `선택한 수량은 ${this.count}개입니다.`;
    }
  }
});
</script>

@click에는 @ 뒤에 콜론을 붙이지 않는다. @:click은 v-on:click의 올바른 축약형이 아니다. 또 methods는 이름과 함수로 구성된 객체다. 함수 안의 this는 Vue 인스턴스에 연결되므로 this.count로 상태를 읽고 쓴다.

Options API의 메서드를 increase: () => { ... }처럼 화살표 함수로 작성하면 외부의 this를 캡처한다. Vue가 연결하는 인스턴스를 사용할 목적이면 위와 같은 메서드 문법이나 일반 함수를 사용한다.

메서드 이름과 호출 표현식의 차이

@click="confirm"은 이벤트가 발생했을 때 메서드를 호출하며 DOM 이벤트 객체를 받을 수 있다. @click="increase(3)"은 명시적으로 3을 전달한다. 사용자 인자와 원래 이벤트가 모두 필요하면 $event를 전달한다. Vue 이벤트 처리

<button type="button" @click="select('item-42', $event)">
  <span>선택</span>
</button>
methods: {
  select(id, event) {
    console.log(id);
    console.log(event.currentTarget.tagName); // BUTTON
  }
}

버튼 안의 <span>을 클릭하면 event.target은 실제 클릭한 span일 수 있다. currentTarget은 현재 핸들러가 연결된 버튼이다. 무조건 target을 버튼이라고 가정해 속성을 읽으면 내부 아이콘이나 텍스트를 추가했을 때 코드가 깨질 수 있다.

기본 동작과 전파는 서로 다른 문제

폼 제출은 브라우저의 문서 이동을 일으킬 수 있다. @submit.prevent는 그 기본 동작을 막고 Vue 메서드로 처리하게 한다. 버튼에 클릭 이벤트만 붙이는 대신 폼의 submit을 처리하면 Enter 키로 제출하는 흐름도 같은 경로를 거친다.

중첩된 요소에서 클릭이 부모로 올라가는 것을 막는 것은 .stop이다. 예를 들어 카드 전체를 클릭하면 상세로 이동하고 카드 안 삭제 버튼만 별도로 처리한다면 전파를 따로 고려해야 한다. .prevent를 붙였다고 전파까지 중단되는 것은 아니다.

수식자제어 대상예시
.prevent기본 동작폼 제출의 문서 이동 방지
.stop이벤트 전파자식 버튼 클릭이 부모 핸들러로 전달되는 것 방지
.self이벤트 발생 대상오버레이 배경 자체를 눌렀을 때만 닫기
.once리스너 실행 횟수한 번만 처리할 안내 동작

수식자를 무조건 여러 개 붙이지 않는다. .prevent.self와 .self.prevent는 생성되는 처리 순서가 달라질 수 있다. .passive는 기본 동작을 막지 않겠다는 의도를 담으므로 .prevent와 조합하지 않는다.

키보드 입력은 의미 있는 키 이름으로 처리하기

<input v-model="query" @keyup.enter="search" @keyup.esc="query = ''">

위 코드는 Enter를 떼면 검색을 호출하고 Escape를 떼면 검색어를 비운다. query와 search는 각각 data와 methods에 선언한다. 예전 숫자 keyCode 목록을 새 코드의 기준으로 삼기보다 .enter, .esc처럼 의미가 보이는 이름을 사용한다.

화면의 주요 동작은 <div @click>보다 <button>이나 <a>의 원래 의미를 활용한다. 기본 키보드 조작과 포커스 동작을 직접 재구현할 일이 줄어든다. 입력 조합 중 Enter가 제출로 처리되는지처럼 언어 입력 환경에 따른 경계도 실제 폼에서 확인한다.

비동기 저장에서는 클릭 한 번과 처리 한 번이 다르다

요청 중 버튼을 비활성화하더라도 네트워크 재시도, 다른 창의 요청, 서버 타임아웃까지 막는 것은 아니다. UI에는 진행 상태를 두고 finally에서 해제하되, 서버의 중복 처리 방지는 별도의 요청 식별자나 업무 규칙으로 해결한다.

// data에 saving: false, error: '', draft: {}, saved: null을 선언한 경우
methods: {
  async save() {
    if (this.saving) return;
    this.saving = true;
    this.error = '';
    try {
      // 이 URL과 API 응답 계약은 실제 서버에 맞게 구현해야 한다.
      const response = await fetch('/api/drafts', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(this.draft)
      });
      if (!response.ok) throw new Error('저장 실패');
      this.saved = await response.json();
    } catch (error) {
      this.error = '저장하지 못했습니다. 잠시 후 다시 시도하세요.';
    } finally {
      this.saving = false;
    }
  }
}

이는 서버 API를 호출하는 메서드 부분의 예시다. 요청 전에 진행 상태를 설정하고, 실패해도 finally에서 해제하므로 다시 시도할 수 있다. 폼은 @submit.prevent="save"로 연결하고 버튼에는 :disabled="saving"을 지정한다. 실제 저장 API에는 인증·요청 위조 방지·입력 검증 등 서비스의 기존 계약을 적용해야 한다. .once는 첫 클릭 뒤 리스너를 한 번만 실행하는 기능이므로 실패 후 재시도가 필요한 저장 동작의 중복 방지책으로 사용하기에 적합하지 않다.

같은 카테고리의 글