이번 포스트에서는 자식 컴포넌트에 v-model을 적용하는 방법에 대해 알아보자.
v-model의 동작
native element에서의 v-model
native element 즉 html의 input 요소에 v-model을 적용하면 손쉽게 양방향의 바인딩이 구현된다. 그럼 사용자 정의 컴포넌트에서 처리하려면 어떤 과정이 필요할까?
사실 v-model은 내부적으로 value 속성에 값을 할당하는 v-bind와 input 이벤트 시 값을 입력하는 v-on 두 개의 directive가 합쳐서 동작하는 것이다.
예를 들어 input 요소에 age를 연결하는 과정은 아래 두 가지 과정으로 나눠볼 수 있다.
<script setup>
import { ref } from 'vue';
const age = ref(0);
</script>
<template>
<label>vmodel : </label><input type="number" v-model="age">
<label>사실은.. </label><input type="number" :value="age" @input="age = $event.target.value">
</template>
component에서의 v-model
그럼 위 개념이 그대로 component에 확장해 보자. 조상 컴포넌트(ex: input을 가지고 있는 녀석)에서 자식 컴포넌트(ex: input)에 값을 내려보내야 하고 자식 컴포넌트에서 변경된 값을 조상 컴포넌트에서 알아야 한다.
즉 props와 event emit이 필요하다! v-model을 컴포넌트에 적용했을 때 사실은 다음과 같이 확장돼서 동작한다.
<CustomInput
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
따라서 자식 컴포넌트에 v-model을 적용하기 위해서는 modelValue이라는 property와 update:<modelValue>라는 이벤트를 처리할 수 있어야 한다.
구현해 보기
먼저 자식 컴포넌트를 만들어보자.
<template>
<div>
<label>검색어: </label><br />
<!--script에서 처리-->
<input :value="modelValue" @input="handleInput" /><br>
<!--inline 처리-->
<input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)" />
</div>
</template>
<script setup>
defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const handleInput = ($event) => {
console.log($event.target.value, '로 검색함')
emit('update:modelValue', $event.target.value)
}
</script>
<style scoped></style>
이것을 사용하는 부모 컴포넌트에서는 props만 넘겨주면 되고 update:modelValue에 대한 처리는 자동으로 진행된다.
<script setup>
import { ref } from 'vue';
import CustomInput from './components/vmodel/CustomInput.vue';
const age = ref(0);
const searchText = ref("검색어");
</script>
<template>
<label>vmodel : </label><input type="number" v-model="age">
<label>사실은.. </label><input type="number" :value="age" @input="age = $event.target.value">
<CustomInput v-model="searchText"></CustomInput>
지금 검색어는: {{ searchText }}
</template>
<style scoped></style>
v-model argument 활용 (v-model:argument)
props 이름 변경
자식 컴포넌트에서 이미 modelValue라는 이름의 속성을 사용 중이라면 부모에서 받을 props의 이름을 변경해 주면 된다. 당연히 이벤트 이름도 그렇다.
다음은 props 이름을 age, 이벤트 이름을 update:age로 변경한 예이다.
<script setup>
defineProps(['age'])
defineEmits(['update:age'])
</script>
<template>
<input type="number" :value="age" @input="$emit('update:age', $event.target.value)"/>
</template>
이제 부모 컴포넌트에서 이 컴포넌트를 사용하면서 v-model의 argument로 새롭게 설정한 age를 넘겨주면 된다. 이벤트는 특별히 설정할 필요 없다.
<CustomInput2 v-model:age="age"/>
여러 개의 v-model 처리
자식 컴포넌트에 여러 개의 v-model을 사용하려면 그냥 여러 개의 props와 emit을 선언해 주면 된다.
<script setup>
defineProps(['age', 'searchText'])
defineEmits(['update:age', 'update:searchText'])
</script>
<template>
<input type="number" :value="age"
@input="$emit('update:age', $event.target.value)" />
<input type="text" :value="searchText"
@input="$emit('update:searchText', $event.target.value)" />
</template>
사용하는 쪽에서는 예약된 props를 사용해 주면 끝이다.
<MultipleVmodel v-model:age="age" v-model:search-text="searchText“/>
수식어 사용 처리
수식어 사용 처리
v-model에는 수식어(lazy, number, trim)를 이용해서 입력 값에 대한 부가적인 처리가 가능하다. 하지만 이들은 native input 요소에만 적용 가능할 뿐 컴포넌트에는 적용할 수 없다. 필요하다면 사용자 정의의 수식어를 만들어서 사용할 수 있다.
자식 컴포넌트에 입력되는 값의 첫 번째 글자를 대문자로 처리하는 수식어를 만든다고 생각해 보자. 그럼 부모 컴포넌트는 아래와 같이 작성될 것이다.
<label>첫글자 대문자</label>
<ModelModifier v-model:search-text.capitalize="searchText" />
그러면 자식 컴포넌트는 searchText 외에 searchTextModifiers라는 props가 추가로 필요하다. searchTextModifiers는 객체형태인데 전달되는 수식어가 true로 설정된다.(전달되지 않으면 false로 간주된다.)
const props = defineProps({
searchText: String,
searchTextModifiers: {},
})
이제 searchTextModifiers의 값을 이용해서 프로그래밍해 주면 된다.
<script>
const emit = defineEmits(['update:searchText'])
function emitSearchText(e) {
let value = e.target.value
if (props.searchTextModifiers.capitalize) {
value = value.substring(0, 1).toUpperCase() + value.substring(1)
}
emit('update:searchText', value)
}
</script>
<template>
<input type="text" :value="searchText" @change="emitSearchText" />
</template>
이제 부모 컴포넌트에서 두 가지 방식으로 자식 컴포넌트의 수식어를 사용해 보자.
// capitalize 수식어 전달
<ModelModifier v-model:search-text.capitalize="searchText" />
// 수식어 없음
<ModelModifier v-model:search-text="searchText" />
첫 번째 경우는 capitalize가 true로 전달되는 경우이고 두 번째는 전달되지 않으므로 false인 상황이어서 대문자화는 진행되지 않는다.