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

v-model

목차

v-model

이름 입력란에 철수를 쓰면 화면의 인사말도 바뀌고, 코드에서 이름을 초기화하면 입력란도 비워진다. v-model은 이처럼 폼 요소의 값과 Vue 상태를 연결하는 문법이다. 서버 저장이나 입력 검증까지 수행하는 기능은 아니다.

예제는 원래 글의 환경인 Vue 2 Options API를 사용한다. 기존 코드를 이해하기 위한 설명이며 신규 프로젝트의 버전 선택은 Vue 2 지원 종료 안내를 확인한다.

텍스트 입력에서 실제로 연결되는 것

<div id="app">
  <label for="name">이름</label>
  <input id="name" v-model.trim="name" placeholder="이름을 입력하세요">
  <p>{{ name || '손님' }}님, 안녕하세요.</p>
  <button type="button" @click="name = ''">초기화</button>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue@2.7.16/dist/vue.js"></script>
<script>
new Vue({ el: '#app', data: { name: '' } });
</script>

일반 텍스트 입력은 value와 input 이벤트를 사용한다. 수식자를 제외한 기본 연결을 풀어 쓰면 다음과 비슷하다.

<input :value="name" @input="name = $event.target.value">

상태에서 DOM으로 내려가는 값 연결과 DOM 이벤트에서 상태로 올라가는 변경이 한 쌍을 이룬다. Vue 상태가 기준이므로 HTML의 value="초기값"과 v-model을 함께 쓰고 서로 다른 초기값을 기대하면 혼란이 생긴다. 초기값은 data에 선언한다. 폼 입력 바인딩

flowchart LR
    S[Vue 상태 name] -->|value| I[입력 요소]
    I -->|input 이벤트| E[변경된 문자열]
    E --> S

한글처럼 조합 입력을 사용하는 경우에는 조합 중 매번 모델 값이 갱신되는 것으로 가정하면 안 된다. Vue는 IME 조합 처리를 고려한다. 화면에 보이는 조합 중 글자와 최종 확정된 상태의 갱신 시점은 다를 수 있다.

입력 종류마다 상태 타입이 달라진다

요소모델 값연결 의미
text, textarea문자열입력한 텍스트
단일 checkbox보통 불리언체크 여부
같은 모델을 쓰는 여러 checkbox배열선택된 각 요소의 value
radio 그룹선택한 한 값한 항목만 선택
select선택한 값option의 value
multiple select배열여러 선택값

같은 v-model이라고 해서 모든 입력이 문자열 하나를 반환하는 것은 아니다. API 요청을 만들 때 모델의 실제 타입을 확인해야 한다.

<div id="preferences">
  <label><input type="checkbox" v-model="agreed">약관 동의</label>
  <fieldset>
    <legend>알림 수단</legend>
    <label><input type="checkbox" value="email" v-model="channels">이메일</label>
    <label><input type="checkbox" value="sms" v-model="channels">문자</label>
  </fieldset>
  <label>수신 빈도
    <select v-model="frequency">
      <option disabled value="">선택하세요</option>
      <option value="daily">매일</option>
      <option value="weekly">매주</option>
    </select>
  </label>
  <p>{{ channels }} / {{ frequency }}</p>
  <button type="button" :disabled="!agreed || !frequency">다음</button>
</div>
<script>
// 위 예제에서 Vue 2 스크립트가 이미 로드되었다고 가정한다.
new Vue({
  el: '#preferences',
  data: { agreed: false, channels: [], frequency: '' }
});
</script>

이메일과 문자를 모두 체크하면 channels는 ['email', 'sms']가 된다. 체크박스 그룹을 하나의 <label>로 통째로 감싸지 않고 입력마다 레이블을 둔다. 사용자가 글자를 눌러 해당 입력을 선택할 수 있고 보조 기술도 입력의 이름을 파악할 수 있다.

value="1"은 문자열이고 :value="1"은 숫자다. 옵션의 식별자를 숫자로 관리하는 API라면 이 차이를 결정한 뒤 사용한다. <textarea>{{ content }}</textarea>처럼 자식 텍스트로 초기화하지 않고 <textarea v-model="content">와 상태의 초기값을 사용한다.

.lazy, .trim, .number의 역할과 한계

.lazy는 일반 텍스트 입력의 반영을 input 대신 change 기준으로 바꾼다. 보통 값 편집을 마치고 포커스를 옮길 때 차이를 볼 수 있다. 검색어를 즉시 반영해야 하는 화면과 입력을 확정한 뒤 검사하는 화면의 요구를 구분한다.

.trim은 앞뒤 공백을 다룰 때 편리하다. 그러나 비밀번호나 공백 자체가 의미 있는 데이터에는 일괄 적용하지 않는다. .number는 숫자로 변환을 시도할 뿐 범위·필수 여부를 검증하지 않는다. 빈 문자열을 포함해 변환할 수 없는 값도 고려한다.

function quantityPayload(value) {
  if (typeof value !== 'number' || !Number.isInteger(value) || value < 1) {
    throw new Error('수량은 1 이상의 정수여야 합니다.');
  }
  return { quantity: value };
}

모델에 바인딩됐다는 이유로 유효한 데이터라고 판단하지 않는다. 위 함수는 요청을 만들기 직전의 검사 예시다. 서버에서도 같은 업무 제약을 검증해야 한다.

폼 입력과 저장 완료를 구분하기

입력 중 상태와 서버에 저장된 상태는 다르다. 사용자가 값을 바꾸면 즉시 화면에는 보이지만 저장 버튼을 누르기 전까지 서버 값은 그대로일 수 있다. 편집 화면은 원본을 복사한 draft를 두고, 저장 성공 시 원본을 새 응답으로 교체하는 구조로 설계할 수 있다. 취소 버튼은 draft를 원본으로 다시 초기화한다.

자식 컴포넌트에 전달한 prop을 곧바로 수정하는 대신 변경 이벤트를 올려 부모가 상태를 갱신하도록 한다. Vue 2의 컴포넌트 v-model은 기본적으로 value prop과 input 이벤트를 사용하며, 네이티브 input의 설명과 구분해야 한다. Vue 3의 기본 컴포넌트 계약은 다르므로 이 코드를 옮길 때 마이그레이션 가이드를 함께 확인한다.

같은 카테고리의 글