Front-End프로그래밍

[Vue3, TypeScript] vee-validate에서 입력 필드 마스킹/포맷팅방법

vee-validate에서 입력 필드 마스킹(Masking) 및 포맷팅(Formatting) 을 구현하는 일반적인 방식은 다음과 같습니다


구현 방식 요약

방법설명예시
v-model + watch()입력값 직접 감시 및 수정전화번호 자동 하이픈
마스킹 라이브러리 사용imask, cleave.js, vue-the-mask카드 번호, 주민번호
FieldparseValue, 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. 공식 지원: FieldparseValue / 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()
양방향 포맷/정규화FieldparseValue, 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. 숫자 단위 쉼표 포맷 (1000010,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와 함께 쓰려면?

FieldparseValue, 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)
카드번호123456781234567812345678123456781234-5678-****-****
주민번호90101012345679010101234567901010-1******
이름홍길자홍길자홍길*
금액100001000010,000

🧩 추가 팁

  • 유효성 검사는 unmasked 데이터에 적용됩니다 (Field의 실제 value는 원본)
  • 서버 전송 시 마스킹이 제거된 값만 전달됩니다
  • 마스킹 규칙을 확장하려면 maskValue() 함수에 분기 추가만 하면 됩니다


6. 마스킹 필드를 포함한 Form 제출 처리

마스킹된 필드를 포함한 Form을 어떻게 제출 처리(validation + 원본 값 전송)할 수 있는지 전체 흐름을 예제


목표

  • MaskedField마스킹된 값만 UI에 표시
  • 실제 Form 제출 시에는 언마스크된 원본 값values로 전달됨
  • vee-validateForm, 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)
카드번호12345678123456781234-5678-****-****"1234567812345678"
주민번호9010101234567901010-1******"9010101234567"
금액1000010,000"10000"
이름홍길자홍길*"홍길자"

요약

항목설명
마스킹 필드MaskedField.vue로 공통화
v-model원본 값 바인딩
Fieldvee-validate 유효성 검사 적용
Form 제출마스킹 제거된 값이 onSubmit에 전달됨
서버 전송 처리필요시 replace()로 쉼표/마스킹 제거

[연관자료]

[Vue3, TypeScript] vee-validate에서 입력 필드 마스킹/포맷팅방법

JavaScript/TypeScript 기본 문법: Spread, Rest, Map 사용법 예제 총정리

[Vue3, TypeScript] vee-validate와 yup에 대한 예제 총정리

error: Content is protected !!