본문 바로가기

FrontEnd

Vue3 컴포넌트 디자인 패턴 : v-model

반응형

v-model을 이용하여 단순 상태 업데이트 관련 코드 상용구를 줄여봅니다.

v-model의 사용사례

프런트엔드에서 양식을 처리할 때 양식 입력 요소의 상태를 JavaScript의 해당 상태와 동기화해야 하는 경우가 많습니다.
값을 수동으로 바인딩하고 이벤트 리스너를 연결하는 것은 번거로울 수 있습니다.
Vue3 공식문서

v-model은 form input값과 같은 컴포넌트 내부 상태(값)와 외부 반응형 값의 상태 동기화를 쉽게 만들어 줍니다.

v-model의 비밀

이전 게시물에서 언급했듯이, 실제론 단방향 바인딩으로 컴파일 되어 처리합니다.

<Comp
  :modelValue="first"
  @update:modelValue="newValue => first = newValue"
/>

만약 v-model이 아니었다면 항상 저런 패턴으로 작성해야 합니다.

v-model 쉽게 구현하기

컴파일러가 디폴트로 적용하는 modelValue를 넘어

한 컴포넌트 내에 여러개의 v-model 바인딩을 프롭으로 허용하고 싶다면 어떻게 할까요?

라는 기능을 활용하면 됩니다.

<UserName
  v-model:first-name="first"
  v-model:last-name="last"
/>

위와 같이 인수를 사용해 여러개의 v-model을 사용할 수 있습니다.

위 컴포넌트는 실제 아래와 같이 처리될 것입니다.

<UserName
  :first-name="first"
  :last-name="last"
  @update:first-name="newValue => first = value"
  @update:last-name="newValue => last = value"
/>

여러개의 v-model을 활용하는 컴포넌트

직접 개발해 봅시다!

1. 간단한 컴포저블 정의

프롭을 이용하는 계산된 값을 만들고, 적절한 이벤트를 방출(emit)하여

프롭의 변경을 컴포넌트에 반영할 수 있게 해줍니다.

import {computed} from "vue"
export function useModelWrapper<T extends Record<string, unknown>, R extends string>(
  props: T,
  emit: (e: `update:${R}`, ...args: any[]) => void,
  name: R
) {
  return computed({
    get: () => props[name],
    set: value => emit(`update:${name}`, value)
  })
}

2. 자식 컴포넌트 정의

  • default v-model로 바인딩된 프롭은 modelValue로 전달됩니다.
    • emit('update:modelValue',새값)을 방출하여 부모 컴포넌트의 props을 변경할 수 있도록 합니다.
  • 부모에서 v-model:draft로 전달된 프롭은 draft로 전달됩니다.
    • emit('update:draft',새값)을 방출하여 부모 컴포넌트의 props을 변경할 수 있도록 합니다.
<template>
  <label>
    <input type="text" v-model="message" />
    <input type="checkbox" v-model="isDraft" /> Draft
  </label>
</template>

<script setup lang="ts">
import { defineEmits, defineProps } from "vue";
import { useModelWrapper } from "../useModelWrapper";
const emit = defineEmits(["update:modelValue", "update:isDraft"]);
const props = defineProps<{ modelValue: string; draft: boolean }>();
const message = useModelWrapper(props, emit, "modelValue");
const isDraft = useModelWrapper(props, emit, "draft");
</script>

3. 부모 컴포넌트에서 사용

2에서 정의한 대로 v-model, v-model:draft를 활용합니다.

<template>
  <img alt="Vue logo" src="./assets/logo.png" />
  <h2>model : {{ model }}</h2>
  <h2>draft : {{ draft }}</h2>
  <MyForms v-model="model" v-model:draft="draft" />
</template>

<script setup lang="ts">
import { ref } from "vue";
import MyForms from "./components/MyForms.vue";
const model = ref("test");
const draft = ref(false);
</script>

기타 : input v-model 한글 지원

아시다시피 html input에 그대로 v-model을 사용하면 한자, 한글이 제대로 완성이 안되는 문제가 있습니다.

이는 아래와 같이 wrapper 컴포넌트를 하나 만드는 방법으로 우회 가능합니다.

예제

 

Vue SFC Playground

 

sfc.vuejs.org

참고

Vue3 공식문서 : Form Input Bindings

 

Form Input Bindings | Vue.js

 

vuejs.org

 

Component Events | Vue.js

 

vuejs.org

 

반응형