[Vue3, TypeScript] vee-validate에서 입력 필드 마스킹/포맷팅방법
vee-validate
에서 입력 필드 마스킹(Masking) 및 포맷팅(Formatting) 을 구현하는 일반적인 방식은 다음과 같습니다
구현 방식 요약
방법 | 설명 | 예시 |
---|---|---|
v-model + watch() | 입력값 직접 감시 및 수정 | 전화번호 자동 하이픈 |
마스킹 라이브러리 사용 | imask , cleave.js , vue-the-mask 등 | 카드 번호, 주민번호 |
Field 의 parseValue , formatValue 사용 | vee-validate 공식 API | 숫자 통화 포맷, 입력 정규화 |
1. 기본 마스킹: 전화번호 하이픈 자동 삽입
<template>
<Form @submit="onSubmit">
<Field
name="phone"
v-model="phone"
:rules="'required'"
as="input"
/>
<ErrorMessage name="phone" />
</Form>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue';
import { Field, Form, ErrorMessage, useForm } from 'vee-validate';
const phone = ref('');
watch(phone, (newVal, oldVal) => {
const digits = newVal.replace(/\D/g, '');
if (digits.length <= 3) {
phone.value = digits;
} else if (digits.length <= 7) {
phone.value = `${digits.slice(0, 3)}-${digits.slice(3)}`;
} else {
phone.value = `${digits.slice(0, 3)}-${digits.slice(3, 7)}-${digits.slice(7, 11)}`;
}
});
const { handleSubmit } = useForm();
const onSubmit = handleSubmit((values) => {
console.log('제출:', values);
});
</script>
2. 공식 지원: Field
의 parseValue
/ formatValue
옵션 사용
<Field
name="amount"
as="input"
:rules="'required|numeric'"
:parseValue="parseAmount"
:formatValue="formatAmount"
/>
function parseAmount(value: string) {
return Number(value.replace(/,/g, ''));
}
function formatAmount(value: number | string) {
const num = typeof value === 'number' ? value : parseFloat(value || '0');
return num.toLocaleString();
}
parseValue
: 사용자가 입력한 값 → 내부 값으로 변환formatValue
: 내부 값 → 보여줄 값으로 변환
✅ 숫자, 금액, 날짜 등의 포맷팅에 유용
3. 외부 마스킹 라이브러리 사용 (imask
, vue-the-mask
, etc.)
예: vue-the-mask
npm install vue-the-mask
<template>
<input v-mask="'###-####-####'" v-model="phone" />
</template>
<script setup>
import { mask } from 'vue-the-mask';
</script>
v-mask
디렉티브만 추가하면 간단하게 마스킹- 하지만
Field
컴포넌트와 함께 사용 시 주의 필요
커스텀 컴포넌트 + 마스킹 예시
<template>
<input
v-model="maskedValue"
@input="onInput"
/>
</template>
<script setup>
import { computed, watch, ref } from 'vue';
import { useField } from 'vee-validate';
const props = defineProps<{ name: string }>();
const { value, errorMessage } = useField(props.name);
const maskedValue = ref('');
watch(value, (val) => {
maskedValue.value = formatPhone(val);
});
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value.replace(/\D/g, '');
value.value = raw;
}
function formatPhone(raw: string) {
if (raw.length <= 3) return raw;
if (raw.length <= 7) return `${raw.slice(0, 3)}-${raw.slice(3)}`;
return `${raw.slice(0, 3)}-${raw.slice(3, 7)}-${raw.slice(7, 11)}`;
}
</script>
정리
기능 | 방법 |
---|---|
간단한 마스킹 | v-model + watch() |
양방향 포맷/정규화 | Field 의 parseValue , formatValue |
라이브러리 통합 | vue-the-mask , imask , cleave.js |
고급 컨트롤 | useField() 로 커스텀 input 구성 |
예제로 알아보자
1. 카드번호 마스킹 (입력 중 포맷: 1234-5678-****-****
)
🎯 입력 중 실시간 마스킹 (v-model
방식)
<template>
<input v-model="maskedCard" @input="onInput" placeholder="카드번호 입력" />
</template>
<script setup lang="ts">
import { ref } from 'vue';
const maskedCard = ref('');
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value.replace(/\D/g, '').slice(0, 16);
const parts = raw.match(/.{1,4}/g) || [];
let formatted = parts.join('-');
// 마스킹 마지막 8자리
if (raw.length > 8) {
const visible = raw.slice(0, 8);
const hidden = '*'.repeat(raw.length - 8);
formatted = `${visible.match(/.{1,4}/g)?.join('-')}-${hidden.replace(/(.{4})/g, '$1-')}`.replace(/-$/, '');
}
maskedCard.value = formatted;
}
</script>
✅ 실제 전송 값은
value.replace(/\D/g, '')
로 원본 숫자만 추출.
2. 주민등록번호 마스킹 (입력 시: 901010-1******
)
<template>
<input v-model="ssn" @input="onInput" placeholder="주민등록번호" />
</template>
<script setup>
import { ref } from 'vue';
const ssn = ref('');
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value.replace(/\D/g, '').slice(0, 13);
if (raw.length <= 6) {
ssn.value = raw;
} else {
const front = raw.slice(0, 6);
const mid = raw.slice(6, 7);
const masked = '*'.repeat(raw.length - 7);
ssn.value = `${front}-${mid}${masked}`;
}
}
</script>
주민번호 뒷자리는 마스킹만 보여주고 저장 시
raw
값 그대로 제출 가능.
3. 입력된 이름 일부 마스킹 (예: 홍길**
)
<template>
<input v-model="maskedName" @input="onInput" placeholder="이름 입력" />
</template>
<script setup>
import { ref } from 'vue';
const maskedName = ref('');
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value.trim();
const visible = raw.slice(0, 2);
const mask = '*'.repeat(Math.max(0, raw.length - 2));
maskedName.value = visible + mask;
}
</script>
홍길동
→홍길*
,홍길자
→홍길*
,홍길자영수
→홍길***
4. 숫자 단위 쉼표 포맷 (10000
→ 10,000
)
사용자가 입력하면 자동 포맷팅, 내부 값은 숫자로 유지
<template>
<input
v-model="displayValue"
@input="onInput"
placeholder="금액"
/>
</template>
<script setup>
import { ref } from 'vue';
const displayValue = ref('');
let rawValue = '';
function onInput(e: Event) {
rawValue = (e.target as HTMLInputElement).value.replace(/[^0-9]/g, '');
displayValue.value = Number(rawValue).toLocaleString();
}
</script>
전송 시
rawValue
사용 → 숫자로 저장.
🧩 확장: vee-validate와 함께 쓰려면?
Field
의 parseValue
, formatValue
옵션을 활용하면 됩니다.
<Field
name="amount"
as="input"
:parseValue="parseAmount"
:formatValue="formatAmount"
rules="required|numeric"
/>
function parseAmount(v: string) {
return parseInt(v.replace(/,/g, ''), 10);
}
function formatAmount(v: string | number) {
return Number(v).toLocaleString();
}
5. 마스킹된 필드를 Field
컴포넌트로 일반화
여러 필드에 재사용할 수 있는 재사용 가능한 마스킹 필드 컴포넌트를 만드는 것을 목표로 합니다.
목표
<MaskedField>
컴포넌트를 만들고maskType
prop으로 유형 지정 (card
,ssn
,comma
,name
등)Field
의 내부 value를 관리하고- 실시간 마스킹 적용
예: MaskedField.vue
<template>
<Field
v-slot="{ field }"
v-bind="field"
:name="name"
:rules="rules"
>
<input
:name="name"
v-model="maskedValue"
@input="onInput"
v-bind="attrs"
/>
</Field>
</template>
<script setup lang="ts">
import { Field } from 'vee-validate';
import { ref, watch, computed } from 'vue';
const props = defineProps<{
name: string;
rules?: string;
modelValue?: string | number;
maskType?: 'card' | 'ssn' | 'name' | 'comma';
}>();
const emit = defineEmits(['update:modelValue']);
const maskedValue = ref('');
// 마스킹 처리 함수 정의
function maskValue(raw: string, type: string = '') {
const digits = raw.replace(/\D/g, '');
switch (type) {
case 'card': {
if (digits.length <= 8) return digits.replace(/(.{4})/g, '$1-').replace(/-$/, '');
const masked = digits
.slice(0, 8)
.replace(/(.{4})/g, '$1-')
+ '****-****';
return masked;
}
case 'ssn': {
if (digits.length <= 6) return digits;
return `${digits.slice(0, 6)}-${digits.slice(6, 7)}******`;
}
case 'name': {
return raw.slice(0, 2) + '*'.repeat(Math.max(0, raw.length - 2));
}
case 'comma': {
return Number(digits).toLocaleString();
}
default:
return raw;
}
}
// 사용자가 입력했을 때
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value;
const unmasked = raw.replace(/[^a-zA-Z0-9]/g, '');
const masked = maskValue(raw, props.maskType);
maskedValue.value = masked;
emit('update:modelValue', unmasked); // 내부 값은 원본
}
// 외부 value 변화 감지 (초기값 등)
watch(() => props.modelValue, (val) => {
if (val !== undefined && val !== null) {
maskedValue.value = maskValue(String(val), props.maskType);
}
}, { immediate: true });
const attrs = computed(() => {
return {
autocomplete: 'off',
placeholder: props.maskType === 'card' ? '1234-****-****' : '',
};
});
</script>
사용 예시
<template>
<Form @submit="onSubmit">
<MaskedField name="card" maskType="card" rules="required" />
<MaskedField name="ssn" maskType="ssn" rules="required" />
<MaskedField name="name" maskType="name" rules="required" />
<MaskedField name="amount" maskType="comma" rules="required" />
<button type="submit">제출</button>
</Form>
</template>
<script setup>
import MaskedField from './MaskedField.vue';
import { useForm } from 'vee-validate';
const { handleSubmit } = useForm();
const onSubmit = handleSubmit(values => {
console.log('원본 데이터 전송됨:', values);
});
</script>
출력 예
필드 | 입력 | 실제 저장 (unmasked) | 표시됨 (masked) |
---|---|---|---|
카드번호 | 1234567812345678 | 1234567812345678 | 1234-5678-****-**** |
주민번호 | 9010101234567 | 9010101234567 | 901010-1****** |
이름 | 홍길자 | 홍길자 | 홍길* |
금액 | 10000 | 10000 | 10,000 |
🧩 추가 팁
- 유효성 검사는
unmasked
데이터에 적용됩니다 (Field
의 실제 value는 원본) - 서버 전송 시 마스킹이 제거된 값만 전달됩니다
- 마스킹 규칙을 확장하려면
maskValue()
함수에 분기 추가만 하면 됩니다
6. 마스킹 필드를 포함한 Form
제출 처리
마스킹된 필드
를 포함한 Form
을 어떻게 제출 처리
(validation + 원본 값 전송)할 수 있는지 전체 흐름을 예제
목표
MaskedField
는 마스킹된 값만 UI에 표시- 실제
Form
제출 시에는 언마스크된 원본 값이values
로 전달됨 vee-validate
의Form
,Field
,useForm
과 완전히 통합v-model
로 마스킹 필드 상태도 외부 제어 가능
1. MaskedField
컴포넌트 (요약 버전, v-model 포함)
이미 구현된 마스킹 필드를
v-model
로 원본 값 연결하는 방식
<!-- MaskedField.vue -->
<template>
<Field :name="name" :rules="rules" v-slot="{ field }">
<input
v-model="maskedValue"
@input="onInput"
v-bind="field"
v-bind="attrs"
/>
</Field>
</template>
<script setup lang="ts">
import { Field } from 'vee-validate';
import { ref, watch, computed } from 'vue';
const props = defineProps<{
name: string;
rules?: string;
modelValue: string | number;
maskType?: 'card' | 'ssn' | 'name' | 'comma';
}>();
const emit = defineEmits(['update:modelValue']);
const maskedValue = ref('');
function maskValue(raw: string): string {
const digits = raw.replace(/\D/g, '');
switch (props.maskType) {
case 'card':
return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-****-****`;
case 'ssn':
return `${digits.slice(0, 6)}-${digits.slice(6, 7)}******`;
case 'name':
return raw.slice(0, 2) + '*'.repeat(Math.max(0, raw.length - 2));
case 'comma':
return Number(digits || 0).toLocaleString();
default:
return raw;
}
}
function onInput(e: Event) {
const raw = (e.target as HTMLInputElement).value;
const unmasked = raw.replace(/[^a-zA-Z0-9]/g, '');
maskedValue.value = maskValue(unmasked);
emit('update:modelValue', unmasked); // 실제 데이터는 원본
}
watch(() => props.modelValue, (val) => {
maskedValue.value = maskValue(String(val ?? ''));
}, { immediate: true });
const attrs = computed(() => ({
autocomplete: 'off',
}));
</script>
2. Form
사용 및 제출 처리
<!-- ExampleForm.vue -->
<template>
<Form @submit="onSubmit">
<MaskedField
name="cardNumber"
maskType="card"
v-model="form.cardNumber"
rules="required|min:16"
/>
<MaskedField
name="ssn"
maskType="ssn"
v-model="form.ssn"
rules="required|min:13"
/>
<MaskedField
name="amount"
maskType="comma"
v-model="form.amount"
rules="required|numeric"
/>
<MaskedField
name="name"
maskType="name"
v-model="form.name"
rules="required"
/>
<button type="submit">제출</button>
</Form>
</template>
<script setup lang="ts">
import { Form } from 'vee-validate';
import { reactive } from 'vue';
import MaskedField from './MaskedField.vue';
const form = reactive({
cardNumber: '',
ssn: '',
amount: '',
name: '',
});
function onSubmit(values: typeof form) {
console.log('✅ 제출된 원본 데이터:', values);
// 예: 서버 전송용 payload
const payload = {
...values,
amount: parseInt(values.amount.replace(/,/g, ''), 10), // 쉼표 제거
};
console.log('📦 서버 전송 payload:', payload);
}
</script>
출력 예시
입력 UI:
필드 | 입력 | 표시값 (마스킹) | 원본(v-model) |
---|---|---|---|
카드번호 | 1234567812345678 | 1234-5678-****-**** | "1234567812345678" |
주민번호 | 9010101234567 | 901010-1****** | "9010101234567" |
금액 | 10000 | 10,000 | "10000" |
이름 | 홍길자 | 홍길* | "홍길자" |
요약
항목 | 설명 |
---|---|
마스킹 필드 | MaskedField.vue 로 공통화 |
v-model | 원본 값 바인딩 |
Field | vee-validate 유효성 검사 적용 |
Form 제출 | 마스킹 제거된 값이 onSubmit 에 전달됨 |
서버 전송 처리 | 필요시 replace() 로 쉼표/마스킹 제거 |
[연관자료]
[Vue3, TypeScript] vee-validate에서 입력 필드 마스킹/포맷팅방법