본문으로 건너뛰기
개발 머꼬
개발 노트Vue
hohyeon.dev17

modelValue prop과 update:modelValue emit을 defineModel 한 줄로 줄이기

  • #Engineering Note
  • #Vue

문제 발생

v-model을 지원하는 입력 컴포넌트를 만들 때마다 같은 배선을 반복하고 있었습니다.

<script setup>
const props = defineProps(["modelValue"]);
const emit = defineEmits(["update:modelValue"]);
</script>

<template>
  <input
    :value="props.modelValue"
    @input="emit('update:modelValue', $event.target.value)"
  />
</template>

컴포넌트가 늘어날수록 update:modelValue 오타로 동작하지 않는 일이 반복됐습니다.

원인 분석

컴포넌트의 v-model은 문법 설탕입니다. 부모의 v-model="foo"는 이렇게 펼쳐집니다.

<Child
  :modelValue="foo"
  @update:modelValue="$event => (foo = $event)"
/>

그래서 자식은 modelValue라는 prop과 update:modelValue라는 emit을 선언해야 했습니다. 위 코드는 그 규약을 사람이 손으로 다시 적은 것이고, 문자열이라 오타가 컴파일 시점에 잡히지 않습니다.

Vue 3.4에서 defineModel() 이 안정화되면서 이 배선이 매크로 한 줄로 줄었습니다. 컴파일러가 modelValue prop과 update:modelValue emit으로 펼쳐 주고, 반환값은 양방향으로 연결된 ref입니다. .value가 부모의 값과 동기화되고, 자식이 바꾸면 부모까지 반영됩니다.

해결 방안

  1. 한 줄로 대체합니다.
<script setup>
const model = defineModel();

function increment() {
  model.value++;   // 부모 값까지 갱신
}
</script>

<template>
  <input v-model="model" />
  <button @click="increment">증가</button>
</template>
  1. 이름 있는 모델은 인자로 넘깁니다. 부모의 v-model:title="bookTitle"에 대응합니다.
const title = defineModel("title");        // prop: title, emit: update:title
const firstName = defineModel("firstName");
const lastName = defineModel("lastName");
  1. prop 옵션은 두 번째 인자입니다.
const model = defineModel({ required: true });
const title = defineModel("title", { required: true });
  1. default에는 함정이 있습니다. 공식 문서가 경고합니다 — default를 줬는데 부모가 값을 주지 않으면 부모와 자식이 어긋납니다.
<!-- 자식 -->
<script setup>
const model = defineModel({ default: 1 });
</script>

<!-- 부모 -->
<script setup>
const myRef = ref();   // undefined
</script>
<template><Child v-model="myRef" /></template>

결과는 부모 myRefundefined, 자식 model1입니다. 부모에서 초기값을 주거나, 기본값을 자식에 두지 않는 편이 안전합니다.

  1. 수정자(modifier)는 구조 분해로 받습니다. v-model.capitalize="text" 같은 사용자 정의 수정자를 get/set으로 처리할 수 있습니다.
const [model, modifiers] = defineModel({
  set(value) {
    if (modifiers.capitalize) {
      return value.charAt(0).toUpperCase() + value.slice(1);
    }
    return value;
  },
});

이름 있는 모델도 같은 형태입니다.

const [firstName, firstNameModifiers] = defineModel("firstName");
  1. 3.4 미만이면 기존 방식을 유지합니다. 매크로이므로 폴리필이 없습니다. 버전을 올릴 수 없다면 computedget/set으로 감싸는 편이 오타에 강합니다.
const model = computed({
  get: () => props.modelValue,
  set: (value) => emit("update:modelValue", value),
});

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.