Vue 이벤트 핸들링과 폼 입력 바인딩

0

이벤트 핸들링

이벤트 청취

Vue는 v-on 디렉티브를 사용하여 DOM 이벤트를 듣고 트리거 될 때 JavaScript를 실행할 수 있습니다.

<div id="example-1">
  <button v-on:click="counter += 1">Add 1</button>
  <p>위 버튼을 클릭한 횟수는 {{ counter }} 번 입니다.</p>
</div>
var example1 = new Vue({
  el: '#example-1',
  data: {
    counter: 0
  }
})

메소드 이벤트 핸들러

대부분의 이벤트 로직은 복잡하기 때문에 v-on 디렉티브에 Javascript 표현식을 직접 작성하는 것은 좋은 방법이 아닙니다. 그래서 v-on 디렉티브는 메소드의 이름으로도 이벤트를 호출할 수 있습니다.

<div id="example-2">
  <!-- greet는 메소드 이름으로 아래에 정의 -->
  <button v-on:click="greet">Greet</button>
</div>
var example2 = new Vue({
  el: '#example-2',
  data: {
    name: 'Vue.js'
  },
  // 메소드는 methods 객체 안에 정의
  methods: {
    greet: function (event) {
      // 메소드 안에서 사용하는 this는 Vue 인스턴스를 가리킴
      alert('Hello ' + this.name + '!')
      // event는 네이티브 DOM 이벤트
      if (event) {
        alert(event.target.tagName)
      }
    }
  }
})

// JavaScript를 이용해서 메소드를 호출
example2.greet() // => 'Hello Vue.js!'

인라인 메소드 핸들러

메소드 이름을 직접 바인딩 하는 대신 인라인 JavaScript 구문에 메소드를 사용할 수도 있습니다.

<div id="example-3">
  <button v-on:click="say('hi')">Say hi</button>
  <button v-on:click="say('what')">Say what</button>
</div>
new Vue({
  el: '#example-3',
  methods: {
    say: function (message) {
      alert(message)
    }
  }
})

가끔 인라인 명령문 핸들러에서 원본 DOM 이벤트에 액세스를 해야하는 경우도 있는데, 이런 경우에는 특별한 변수인 $event를 사용해 메소드에 전달할 수도 있습니다.

<button v-on:click="warn('Form cannot be submitted yet.', $event)">
  Submit
</button>
// ...
methods: {
  warn: function (message, event) {
    // 네이티브 이벤트에 액세스
    if (event) event.preventDefault()
    alert(message)
  }
}

이벤트 수식어

이벤트 핸들러의 내부에서 event.preventDefault() 또는 event.stopPropagation()를 호출하는 것은 매우 일반적인 경우라고 할 수 있는데, 메소드 내에서는 이 작업을 쉽게 할 수 있습니다.

그렇지만 DOM 이벤트의 세부 사항 대신 데이터 로직에 대한 메소드만 사용하는 것이 더 편리하기 때문에 Vue는 v-on 이벤트에 연결해 쓸 수 있는 이벤트 수식어를 제공합니다. 이벤트 수식어는 점.으로 연결해 사용할 수 있고, 다음과 같은 종류가 있습니다.

  • .stop
  • .prevent
  • .capture
  • .self
  • .once
  • .passive
이벤트 접미사 사용 예제
<!-- 클릭 이벤트 전파 중지 -->
<a v-on:click.stop="doThis"></a>

<!-- 제출 이벤트가 페이지를 다시 로드 하지 않음 -->
<form v-on:submit.prevent="onSubmit"></form>

<!-- 수식어는 체이닝이 가능함 -->
<a v-on:click.stop.prevent="doThat"></a>

<!-- 수식어만 사용하면 에러 발생 -->
<form v-on:submit.prevent></form>

<!-- 이벤트 리스너를 추가할 때 캡처모드를 사용하여, 내부 엘리먼트를 대상으로 하는 이벤트가 해당 엘리먼트에서 처리되기 전에 이곳에서 처리함 -->
<div v-on:click.capture="doThis">...</div>

<!-- event.target이 엘리먼트 자체인 경우에만 트리거를 처리하며, 자식 엘리먼트에서는 실행되지 않음 -->
<div v-on:click.self="doThat">...</div>

이벤트 수식어 체이닝을 하는 경우 관련 코드가 동일한 순서로 생성되기 때문에 수식어를 사용할 때 순서를 지정해야 합니다. 예를 들어 v-on:click.prevent.self의 순서로 사용하면 “모든 클릭”을 막을 수 있지만, v-on:click.self.prevent의 경우에는 “엘리먼트 자체에 대한 클릭”만 막을 수 있습니다.

once 이벤트 수식어
<!-- 클릭 이벤트는 최대 한번만 트리거됨 -->
<a v-on:click.once="doThis"></a>

다른 이벤트 수식어는 네이티브 DOM 이벤트에 독점적이지만, .once 수식어는 컴포넌트 이벤트에서도 사용할 수 있습니다.

passive 이벤트 수식어
<!-- 스크롤의 기본 이벤트를 취소할 수 없음 -->
<div v-on:scroll.passive="onScroll">...</div>

Vue는 addEventListenerpassive option에 해당하는 .passive 수식어도 제공하는데, 이는 특히 모바일 환경에서 성능향상에 도움이 됩니다.

브라우저의 경우에는 핸들러가 event.preventDefault()를 언제 호출하는지 알 수 없어 프로세스가 완료된 후 스크롤을 하게 되는데, .passive 수식어를 사용하게 되면 이 이벤트가 기본 동작을 멈추지 않는다는 것을 브라우저에 알릴 수 있습니다.

.passive를 사용하는 경우에는 .prevent와 함께 사용하지 않도록 주의할 필요가 있습니다. .passive.prevent를 함께 사용하게 되면, .prevent는 무시되고 브라우저는 오류를 발생시킵니다.

키 수식어

키보드 이벤트를 청취할 때, 공통 키 코드를 확인해야 하는 경우가 있습니다. 이런 경우 Vue는 키 이벤트를 수신할 때 v-on에 대한 키 수식어를 추가할 수 있습니다.

<!-- key가 Enter일 때만 vm.submit()을 호출 -->
<input v-on:keyup.enter="submit">

Vue는 KeyboardEvent.key를 통해 노출된 유효 키 이름을 케밥 케이스로 변환하여 수식어로 사용할 수 있는데, 다음 코드와 같이 핸들러는 $event.key === ‘PageDown’ 일 때에만 호출이 될 수 있습니다.

<input v-on:keyup.page-down="onPageDown">

시스템 수식어 키 목록

Vue는 시스템 수식어의 키가 눌러진 경우에만 마우스 또는 키보드 이벤트 리스너를 트리거 할 수 있는데, 사용할 수 있는 시스템 수식어의 종류는 다음과 같습니다.

  • .ctrl
  • .alt
  • .shift
  • .meta

참고로 매킨토시 키보드에서 metacommand이고, Windows 키보드에서 metawindows를 말합니다.

시스템 수식어의 사용 예
<!-- Alt + C -->
<input @keyup.alt.67="clear">

<!-- Ctrl + Click -->
<div @click.ctrl="doSomething">Do something</div>

수식어 키는 일반 키와는 다른 기능을 하는데, keyup 이벤트와 함께 사용되면 이벤트가 발생할 때 수식어 키가 눌려있어야 합니다. 즉, keyup.ctrlctrl을 누른 상태에서 키를 놓으면 트리거되고, ctrl 키만 놓으면 트리거되지 않습니다.

.exact 수식어

.exact 수식어는 다른 시스템 수식어와 조합됐을 때, 그 핸들러가 실행되기 위해서는 정확한 조합이 눌러야 한다는 것을 알려주는 수식어입니다.

exact 수식어의 사용 예
<!-- Alt 또는 Shift와 함께 눌린 경우에도 실행 -->
<button @click.ctrl="onClick">A</button>

<!-- Ctrl 키만 눌려있을 때만 실행 -->
<button @click.ctrl.exact="onCtrlClick">A</button>

<!-- 시스템 키가 눌리지 않은 상태인 경우에만 작동 -->
<button @click.exact="onClick">A</button>

마우스 버튼 수식어

Vue는 특정 마우스 버튼에 의해 트리거 된 이벤트로 핸들러를 제한하는 수식어도 제공합니다.

  • .left
  • .right
  • .middle

HTML 리스너를 사용하는 이유

Vue의 모든 이벤트 청취 접근 방법이 관심사 분리에 대한 오래된 규칙을 어긴다고 생각될 수 있지만, 실제로 모든 핸들러의 함수와 표현식은 현재의 처리를 하는 ViewModel에 엄격히 바인딩 되기 때문에 유지보수가 어렵지 않은데, 실제로 v-on을 사용하면 다음과 같은 몇가지 이점이 있다고 합니다.

  • HTML 템플릿을 간단히 하여 JavaScript 코드 내에서 핸들러 함수 구현을 찾는 것이 더 쉽다.
  • JavaScript에서 이벤트 리스너를 수동으로 연결할 필요가 없으므로 ViewModel 코드는 순수 로직과 DOM이 필요하지 않기 때문에 테스트가 쉬워진다.
  • ViewModel이 파기되면 모든 이벤트 리스너가 자동으로 제거되어, 이벤트 제거에 대한 걱정이 필요 없어진다.

폼 입력 바인딩

기본 사용법

Vue에서 폼 입력 바인딩은 v-model 디렉트브로 엘리먼트에 양방향 데이터 바인딩을 생성할 수 있는데, v-model 디렉티브는 폼 inputtextarea 엘리먼트 등 입력 유형에 따라 엘리먼트를 업데이트하는 올바른 방법을 자동으로 선택합니다.

그런데 Vue 문서에 따르면 v-model은 기본적으로 사용자 입력 이벤트에 대한 데이터를 업데이트하는 “문법적 설탕”이기 때문에 일부 경우에는 특별한 주의가 필요합니다.

v-model은 모든 form 엘리먼트의 초기 valuechecked, selected 속성을 무시하고, 항상 Vue 인스턴스의 데이터를 원본 소스로 취급하기 때문에 컴포넌트의 data 옵션 안에 있는 JavaScript에서 초기값을 선언해야 되는데, v-model은 내부적으로 서로 다른 속성을 사용하고 서로 다른 입력 요소에 대해 서로 다른 이벤트를 전송합니다.

  • texttextarea 태그는 value 속성과 input 이벤트를 사용
  • 체크박스들과 라디오버튼들은 checked 속성과 change 이벤트를 사용
  • Select 태그는 valueprop으로, change를 이벤트로 사용

중국어, 일본어, 한국어 등 IME가 필요한 언어의 경우, IME 중에 v-model이 업데이트 되지 않는데, 이런 업데이트를 처리하려면 input 이벤트를 대신 사용해야 합니다.

문자열 바인딩하기

<input v-model="message" placeholder="여기를 수정해보세요">
<p>메시지: {{ message }}</p>

여러줄 문장 바인딩하기

<span>여러 줄을 가지는 메시지:</span>
<p style="white-space: pre-line">{{ message }}</p>
<br>
<textarea v-model="message" placeholder="여러줄을 입력해보세요"></textarea>

textarea 엘리먼트에 데이터를 바인딩하는 경우 <textarea>{{ text }}</textarea>와 같이 텍스트 영역의 보간은 작동하지 않기 때문에 반드시 v-model을 사용해야 합니다.

체크박스 바인딩하기

<input type="checkbox" id="checkbox" v-model="checked">
<label for="checkbox">{{ checked }}</label>

하나의 체크박스는 단일 boolean 값을 가지는데, 여러개의 체크박스는 같은 배열을 바인딩 할 수 있습니다.

여러 체크박스 바인딩하기
<div id='example-3'>
  <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
  <label for="jack">Jack</label>
  <input type="checkbox" id="john" value="John" v-model="checkedNames">
  <label for="john">John</label>
  <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
  <label for="mike">Mike</label>
  <br>
  <span>체크한 이름: {{ checkedNames }}</span>
</div>
체크박스 바인딩 데이터
new Vue({
  el: '#example-3',
  data: {
    checkedNames: []
  }
})

라디오 바인딩하기

<input type="radio" id="one" value="One" v-model="picked">
<label for="one">One</label>
<br>
<input type="radio" id="two" value="Two" v-model="picked">
<label for="two">Two</label>
<br>
<span>선택: {{ picked }}</span>

셀렉트 바인딩하기

<select v-model="selected">
  <option disabled value="">Please select one</option>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<span>선택함: {{ selected }}</span>
new Vue({
  el: '...',
  data: {
    selected: ''
  }
})

만약 v-model 표현식의 초기 값이 어떤 옵션에도 없으면, <select> 엘리먼트는 “선택없음” 상태로 렌더링되는데, iOS에서는 이런 경우 변경 이벤트가 발생하지 않기 때문에 사용자가 첫 번째 항목을 선택할 수 없게 됩니다. 그래서 위 코드와 같이 사용하지 않는 옵션에도 빈 값을 넣어주는 것이 좋습니다.

다중 셀렉트 바인딩하기

<select v-model="selected" multiple>
  <option>A</option>
  <option>B</option>
  <option>C</option>
</select>
<br>
<span>Selected: {{ selected }}</span>

다중 셀렉트는 배열을 바인딩하는데, 다음 코드와 같이 v-for를 이용해 동적으로 옵션을 렌더링 할 수도 있습니다.

동적으로 옵션 렌더링하기
<select v-model="selected">
  <option v-for="option in options" v-bind:value="option.value">
    {{ option.text }}
  </option>
</select>
<span>Selected: {{ selected }}</span>
다중 셀렉트 데이터
new Vue({
  el: '...',
  data: {
    selected: 'A',
    options: [
      { text: 'One', value: 'A' },
      { text: 'Two', value: 'B' },
      { text: 'Three', value: 'C' }
    ]
  }
})

값 바인딩하기

라디오, 체크박스, 셀렉트 옵션의 경우, v-model의 바인딩 값은 정적인 문자열이거나 boolean 데이터입니다.

<!-- picked는 선택시 문자열 "a" -->
<input type="radio" v-model="picked" value="a">

<!-- toggle은 true 또는 false -->
<input type="checkbox" v-model="toggle">

<!-- selected는 "ABC" 선택시 "abc" -->
<select v-model="selected">
  <option value="abc">ABC</option>
</select>

만약 값을 Vue 인스턴스의 동적 속성에 바인딩해야 하는 경우라면, v-bind를 사용할 수 있습니다. v-bind를 사용하면 입력 값을 문자열이 아닌 값에 바인딩 할 수도 있습니다.

체크박스

<input
  type="checkbox"
  v-model="toggle"
  true-value="yes"
  false-value="no"
>
// 체크된 경우
vm.toggle === 'yes'
// 체크 되지 않은 경우
vm.toggle === 'no'

true-valuefalse-value 속성을 사용하면 폼 전송시 체크되지 않은 박스를 포함하지 않기 때문에 입력의 value 속성에 영향을 미치지 않습니다.

라디오

<input type="radio" v-model="pick" v-bind:value="a">
// 체크 시
vm.pick === vm.a

셀렉트 옵션

<select v-model="selected">
  <!-- inline object literal -->
  <option v-bind:value="{ number: 123 }">123</option>
</select>
// 선택 시
typeof vm.selected // -> 'object'
vm.selected.number // -> 123

수식어

.lazy

기본적으로 v-model은 각 입력 이벤트 후 입력과 데이터를 동기화하는데, .lazy 수식어를 추가하면 change 이벤트 이후에 동기화를 하게 됩니다.

<!-- "input" 대신 "change" 이후에 동기화 -->
<input v-model.lazy="msg" >

.number

HTML의 입력 엘리먼트는 type="number"를 사용하는 경우에도 항상 문자열을 반환하기 때문에, 사용자 입력이 자동으로 숫자로 형변환 되기를 원하는 경우에는 v-model이 관리하는 inputnumber라는 수식어를 추가해주면 됩니다.

<input v-model.number="age" type="number">

.trim

v-model이 관리하는 input 문자열의 공백을 자동으로 trim하고 싶다면, trim이라는 수식어를 추가해주면 됩니다.

<input v-model.trim="msg">

v-model과 컴포넌트

HTML의 기본 제공 input 유형이 항상 사용자의 요구를 만족시킬 수는 없는데, Vue의 컴포넌트를 사용하면 사용자가 원하는 동작으로 재사용 가능한 input을 만들 수 있습니다. 이렇게 만든 재사용 가능한 inputv-model과도 잘 작동될 수 있습니다.

Leave a Reply