v-model 원리와 커스텀 컴포넌트 만들기 (2 of 2)

2023-07-02


2023-04-04에 작성된 원문을 수정한 버전입니다

지난 포스팅에서 v-model의 작동원리와 커스텀 컴포넌트를 만드는 방법에 대해서 살펴보았다. 이번 포스팅에서는 만들어진 컴포넌트에다 각각의 input 특성에 맞는 로직을 구현해보도록 하겠다.

기본 구조

부모 컴포넌트인 MainPage.vue와 전화번호를 위한 InputContact.vue, 이메일을 위한 InputEmail.vue를 자식 컴포넌트로 구성한다. 자식 컴포넌트는 지난 포스팅에서 다루었던 MyInput.vue를 베이스로 작성했다.

App.vue └── MainPage.vue ├── InputContact.vue └── InputEmail.vue

연락처와 이메일과 관련된 변수는 당연히 MainPage.vue에서만 관리하고, 두 Input*.vue에는 props로 전달하여 데이터 처리 로직 수행 후, emit('update:modelValue')로 가공된 데이터를 넘겨주는 방식이다.

전화번호를 위한 input

전화번호를 입력하는 input을 생각해보자. 우선적으로 필요한 기능은 전화번호 사이마다 '-'를 넣어주는 기능이다. 예를 들어 사용자가 01012345678를 입력한다면 input010-1234-5678로 표시되도록 해야한다.

그래서 일단 전화번호 포맷팅을 수행하는 formatContact() 함수에 정규표현식을 사용하려고 한다. 전화번호 포맷팅 관련 정규표현식은 구글링을 해봐도 쉽게 찾을 수 있지만 내가 사용한 정규표현식은 아래와 같다.

const formatContact = rawString => { const formatted = rawString .replace(/[^0-9]/g, "") // 숫자만 필터링하기 .replace(/^(\d{0,3})(\d{0,4})(\d{0,4})$/g, "$1-$2-$3") // 3,4,4자리로 끊고 -로 구분 .replace(/(\-{1,2})$/g, "") //아직 숫자 입력되기 전의 -는 가려주기 return formatted }

formatContact함수를 @input의 콜백함수에 넣어주고, emit함수를 호출할 때도 formatted된 값을 부모 컴포넌트(MainPage.vue)에 넘겨야한다.

InputContact.vue

... const emit = defineEmits(["update:modelValue"]); const onInput = (e) => { const newValue = e.target.value; const formatted = formatContact(newValue); emit("update:modelValue", newValue); }; const formatContact = (rawString) => { const formatted = rawString .replace(/[^0-9]/g, "") // 숫자만 필터링하기 .replace(/^(\d{0,3})(\d{0,4})(\d{0,4})$/g, "$1-$2-$3") // 3,4,4자리로 끊고 -로 구분 .replace(/(\-{1,2})$/g, ""); //아직 숫자 입력되기 전의 -는 가려주기 return formatted; };

MainPage.vue는 그냥 일반적인 <input> 태그를 다룰 때처럼만 작성하면 된다. 커스텀 컴포넌트로 변경했다고 한들, 부모 컴포넌트가 데이터 처리 로직에 관여해서 안되고 Input*.vue에서 emit되는 이벤트에만 의존하고 있어야 한다.

MainPage.vue

<template> <div> <InputContact v-model="contact" /> <p>contact : {{ contact }}</p> </div> </template> <script setup> import { ref } from "vue"; import InputContact from "@/components/InputContact.vue"; const contact = ref(""); </script>

선택적으로 포맷팅하고 싶다면?

위 코드에서는 전화번호가 항상 포맷팅되지만, 상황에 따라서는 포맷팅되지 않는 걸 원할 수도 있다. propsuseFormat 을 받아서 이 값이 true일 때만 포맷팅되도록 할 수도 있다.

InputContact.vue

const props = defineProps({ modelValue: String, useFormat: { type: Boolean, default: true, }, }) const emit = defineEmits(["update:modelValue"]) const onInput = e => { const newValue = e.target.value if (props.useFormat) { const formatted = formatContact(newValue) emit("update:modelValue", formatted) } else { emit("update:modelValue", newValue) } } const formatContact = rawString => { const formatted = rawString .replace(/[^0-9]/g, "") // 숫자만 필터링하기 .replace(/^(\d{0,3})(\d{0,4})(\d{0,4})$/g, "$1-$2-$3") // 3자리,4자리,4자리로 끊고 -로 구분하기 .replace(/(\-{1,2})$/g, "") //아직 숫자 입력되기 전의 -는 가려주기 return formatted }

MainPage.vue

<InputContact :useFormat="true" v-model="contact" />

이메일을 위한 input

이번에는 이메일을 입력받는 InputEmail.vue를 살펴볼텐데, 전화번호와는 다르게 유효성을 검증하는 기능을 하나 추가하려고 한다. 이메일 유효성을 검증하는데에도 역시 구글링해보면 정규표현식이 많이 나오긴 하지만 여기서는 편하게 email-validator라는 npm 패키지를 사용했다.

큰 틀에서는 InputContact.vue와 동일하나, @input 이벤트마다 유효성 검증을 거친 값을 부모 컴포넌트인 MainPage.vueemit으로 전달해주어야 한다. 따라서 나는 emit('is-valid',유효성여부)으로 지정했다.

<template> <input type="email" :value="modelValue" @input="onInput" /> </template> <script setup> import { ref } from "vue"; import * as EmailValidator from "email-validator"; const props = defineProps({ modelValue: String, }); const emit = defineEmits(["update:modelValue", "is-valid"]); const onInput = (e) => { const newValue = e.target.value; const isEmailValid = checkEmailValid(newValue); emit("is-valid", isEmailValid); emit("update:modelValue", newValue); }; const checkEmailValid = (email) => { return EmailValidator.validate(email); }; </script>

그러면 MainPage.vue에서는 emit으로 보낸 값을 @is-valid로 받고 콜백함수에서 유효성 여부를 인자로 받을 수 있다. 콜백함수를 위한 별도의 함수를 하나 만들어도 되긴 하는데, 예제 상에서는 만들지 않고 isEmailValid라는 변수에 직접 넣어주는 방식을 선택하였다.

MainPage.vue

<template> <div> <InputEmail v-model="email" @is-valid="(data) => (isEmailValid = data)" /> <p>email : {{ email }}</p> <p>valid : {{ isEmailValid }}</p> </div> </template> <script setup> import { ref } from "vue"; import InputEmail from "@/components/InputEmail.vue"; const email = ref(""); const isEmailValid = ref(null); </script>

구현


Profile picture

하주헌 Neon

프론트엔드 개발자입니다. 제가 작성한 코드가 화면에 나타나는 모습을 좋아합니다. 백엔드에도 관심이 많습니다.

Loading script...