EmailVerify LogoEmailVerify

Vue.js

Email checker for Vue.js. Real-time email verification in Vue components and forms.

Vue 3 애플리케이션에 EmailVerify를 통합하여 실시간 이메일 유효성 검사를 구현하세요. 이 가이드는 기본 설정부터 고급 패턴까지 Composition API와 Options API 구현을 모두 다룹니다.

설치

EmailVerify 패키지를 설치하고 프로젝트를 설정하세요.

npm install emailverify
yarn add emailverify
pnpm add emailverify

빠른 시작

Composition API

컴포넌트에서 반응형 이메일 검증을 위해 Composition API를 사용하세요.

<template>
  <div class="email-verification">
    <input
      v-model="email"
      type="email"
      placeholder="이메일 주소 입력"
      @blur="verifyEmail"
      class="email-input"
    />

    <div v-if="isLoading" class="status loading">
      검증 중...
    </div>
    <div v-else-if="result" :class="['status', result.status]">
      <span v-if="result.status === 'valid'">✓ 유효한 이메일</span>
      <span v-else-if="result.status === 'invalid'">✗ 유효하지 않은 이메일</span>
      <span v-else>? 확인할 수 없음</span>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue';
import { emailverify } from 'emailverify';

const email = ref('');
const isLoading = ref(false);
const result = ref<any>(null);

const verifyEmail = async () => {
  if (!email.value) return;

  isLoading.value = true;
  try {
    result.value = await emailverify.verify({
      email: email.value,
      apiKey: import.meta.env.VITE_EMAILVERIFY_API_KEY
    });
  } catch (error) {
    console.error('검증 실패:', error);
    result.value = null;
  } finally {
    isLoading.value = false;
  }
};
</script>

<style scoped>
.email-input {
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 16px;
  width: 100%;
}

.status {
  margin-top: 8px;
  padding: 8px 12px;
  border-radius: 4px;
  font-size: 14px;
}

.status.valid {
  background-color: #d4edda;
  color: #155724;
  border: 1px solid #c3e6cb;
}

.status.invalid {
  background-color: #f8d7da;
  color: #721c24;
  border: 1px solid #f5c6cb;
}

.status.loading {
  background-color: #d1ecf1;
  color: #0c5460;
  border: 1px solid #bee5eb;
}
</style>

Options API

Options API 패턴을 사용하는 프로젝트용:

<template>
  <div class="email-verification">
    <input
      v-model="email"
      type="email"
      placeholder="이메일 주소 입력"
      @blur="verifyEmail"
      class="email-input"
    />

    <div v-if="isLoading" class="status loading">
      검증 중...
    </div>
    <div v-else-if="result" :class="['status', result.status]">
      {{ statusMessage }}
    </div>
  </div>
</template>

<script lang="ts">
import { defineComponent } from 'vue';
import { emailverify } from 'emailverify';

export default defineComponent({
  name: 'EmailVerification',
  data() {
    return {
      email: '',
      isLoading: false,
      result: null
    };
  },
  computed: {
    statusMessage(): string {
      if (this.result?.status === 'valid') return '✓ 유효한 이메일';
      if (this.result?.status === 'invalid') return '✗ 유효하지 않은 이메일';
      return '? 확인할 수 없음';
    }
  },
  methods: {
    async verifyEmail() {
      if (!this.email) return;

      this.isLoading = true;
      try {
        this.result = await emailverify.verify({
          email: this.email,
          apiKey: import.meta.env.VITE_EMAILVERIFY_API_KEY
        });
      } catch (error) {
        console.error('검증 실패:', error);
        this.result = null;
      } finally {
        this.isLoading = false;
      }
    }
  }
});
</script>

컴포저블 레퍼런스

useEmailVerification

이메일 검증 상태와 로직을 관리하는 커스텀 컴포저블.

import { ref, computed } from 'vue';
import { emailverify } from 'emailverify';

export function useEmailVerification(apiKey: string) {
  const email = ref('');
  const isLoading = ref(false);
  const error = ref<string | null>(null);
  const result = ref<any>(null);

  const isValid = computed(() => result.value?.status === 'valid');
  const isInvalid = computed(() => result.value?.status === 'invalid');
  const isDisposable = computed(() => result.value?.result?.disposable || false);

  const verify = async (emailToVerify?: string) => {
    const emailAddress = emailToVerify || email.value;
    if (!emailAddress) {
      error.value = '이메일 주소가 필요합니다';
      return;
    }

    isLoading.value = true;
    error.value = null;

    try {
      result.value = await emailverify.verify({
        email: emailAddress,
        apiKey
      });
    } catch (err) {
      error.value = err instanceof Error ? err.message : '검증 실패';
      result.value = null;
    } finally {
      isLoading.value = false;
    }
  };

  const reset = () => {
    email.value = '';
    result.value = null;
    error.value = null;
    isLoading.value = false;
  };

  return {
    email,
    isLoading,
    error,
    result,
    isValid,
    isInvalid,
    isDisposable,
    verify,
    reset
  };
}

useEmailInput

디바운싱과 캐싱이 포함된 이메일 입력 처리 컴포저블.

import { ref, computed, watch } from 'vue';
import { useEmailVerification } from './useEmailVerification';

const CACHE_DURATION = 30 * 60 * 1000; // 30분

export function useEmailInput(apiKey: string) {
  const email = ref('');
  const cache = new Map<string, { result: any; timestamp: number }>();
  const { verify: verifyWithAPI, isLoading, error, result } = useEmailVerification(apiKey);

  const verify = async () => {
    if (!email.value) return;

    // 먼저 캐시 확인
    const cached = cache.get(email.value);
    if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
      result.value = cached.result;
      return;
    }

    // API로 검증
    await verifyWithAPI(email.value);

    // 결과 캐시
    if (result.value) {
      cache.set(email.value, {
        result: result.value,
        timestamp: Date.now()
      });
    }
  };

  const clearCache = () => cache.clear();

  return {
    email,
    isLoading,
    error,
    result,
    verify,
    clearCache
  };
}

컴포넌트 통합

VeeValidate 통합

폼 유효성 검사를 위해 VeeValidate와 이메일 검증을 통합하세요.

<template>
  <Form @submit="onSubmit" class="form">
    <div class="form-group">
      <label>이메일 주소</label>
      <Field
        name="email"
        type="email"
        placeholder="your@email.com"
        v-slot="{ field, meta }"
        :rules="validateEmail"
      >
        <input
          v-bind="field"
          :class="{ 'input-error': meta.touched && !meta.valid }"
          @blur="verifyEmail(field.value)"
        />
      </Field>

      <div v-if="verificationResult" :class="['verification-status', verificationResult.status]">
        {{ getStatusMessage(verificationResult) }}
      </div>

      <ErrorMessage name="email" class="error-message" />
    </div>

    <button type="submit" :disabled="isLoading">
      {{ isLoading ? '검증 중...' : '제출' }}
    </button>
  </Form>
</template>

<script setup lang="ts">
import { Form, Field, ErrorMessage } from 'vee-validate';
import * as yup from 'yup';
import { ref } from 'vue';
import { useEmailVerification } from './composables/useEmailVerification';

const schema = yup.object({
  email: yup.string().email('잘못된 이메일').required('이메일을 입력해 주세요')
});

const { verify, result: verificationResult, isLoading } = useEmailVerification(
  import.meta.env.VITE_EMAILVERIFY_API_KEY
);

const validateEmail = async (value: string) => {
  if (!value) return '이메일을 입력해 주세요';

  try {
    await schema.validate({ email: value });
    return true;
  } catch {
    return '잘못된 이메일 형식입니다';
  }
};

const verifyEmail = async (email: string) => {
  if (email) {
    await verify(email);
  }
};

const getStatusMessage = (result: any) => {
  switch (result.status) {
    case 'valid':
      return '✓ 유효한 이메일 주소';
    case 'invalid':
      return '✗ 유효하지 않은 이메일 주소';
    case 'unknown':
      return '? 이 이메일을 확인할 수 없습니다';
    default:
      return '알 수 없는 상태';
  }
};

const onSubmit = (values: any) => {
  if (verificationResult.value?.status === 'valid') {
    console.log('폼 제출:', values);
  }
};
</script>

<style scoped>
.form-group {
  margin-bottom: 16px;
}

label {
  display: block;
  margin-bottom: 8px;
  font-weight: 500;
}

input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ccc;
  border-radius: 4px;
  font-size: 14px;
}

input.input-error {
  border-color: #dc3545;
  background-color: #fff5f5;
}

.error-message {
  color: #dc3545;
  font-size: 12px;
  margin-top: 4px;
}

.verification-status {
  margin-top: 8px;
  padding: 8px 12px;
  border-radius: 4px;
  font-size: 13px;
}

.verification-status.valid {
  background-color: #d4edda;
  color: #155724;
}

.verification-status.invalid {
  background-color: #f8d7da;
  color: #721c24;
}
</style>

Element Plus 통합

Element Plus 폼 컴포넌트와 EmailVerify를 함께 사용하세요.

<template>
  <el-form :model="form" @submit.prevent="handleSubmit">
    <el-form-item label="이메일" prop="email">
      <el-input
        v-model="form.email"
        type="email"
        placeholder="이메일 입력"
        @blur="verifyEmail"
        :loading="isLoading"
      />

      <el-alert
        v-if="verificationResult"
        :title="getStatusTitle()"
        :type="getAlertType()"
        :closable="false"
        style="margin-top: 8px"
      />
    </el-form-item>

    <el-form-item>
      <el-button
        type="primary"
        @click="handleSubmit"
        :loading="isLoading"
      >
        제출
      </el-button>
    </el-form-item>
  </el-form>
</template>

<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useEmailVerification } from './composables/useEmailVerification';

const form = reactive({
  email: ''
});

const { verify, result: verificationResult, isLoading } = useEmailVerification(
  import.meta.env.VITE_EMAILVERIFY_API_KEY
);

const verifyEmail = async () => {
  if (form.email) {
    await verify(form.email);
  }
};

const getStatusTitle = () => {
  switch (verificationResult.value?.status) {
    case 'valid':
      return '이메일이 유효합니다';
    case 'invalid':
      return '이메일이 유효하지 않습니다';
    default:
      return '이메일을 확인할 수 없습니다';
  }
};

const getAlertType = () => {
  return verificationResult.value?.status === 'valid' ? 'success' : 'error';
};

const handleSubmit = async () => {
  await verifyEmail();
  if (verificationResult.value?.status === 'valid') {
    console.log('이메일 검증됨, 폼 제출 중...');
  }
};
</script>

Nuxt 3에서의 SSR

서버 측 검증

보안과 성능 향상을 위해 서버에서 이메일을 검증하세요.

// server/api/verify-email.ts
import { emailverify } from 'emailverify';

export default defineEventHandler(async (event) => {
  const { email } = await readBody(event);

  if (!email) {
    throw createError({
      statusCode: 400,
      statusMessage: '이메일은 필수입니다'
    });
  }

  try {
    const result = await emailverify.verify({
      email,
      apiKey: process.env.EMAILVERIFY_API_KEY!
    });

    return result;
  } catch (error) {
    throw createError({
      statusCode: 500,
      statusMessage: '검증 실패'
    });
  }
});

Nuxt에서의 클라이언트 측 통합

Nuxt 컴포넌트에서 서버 엔드포인트를 사용하세요.

<template>
  <div class="email-verification">
    <input
      v-model="email"
      type="email"
      placeholder="your@email.com"
      @blur="verifyEmail"
    />

    <div v-if="pending" class="status loading">
      검증 중...
    </div>
    <div v-else-if="result" :class="['status', result.status]">
      {{ statusMessage }}
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue';

const email = ref('');
const { data: result, pending, execute: verifyEmail } = await useFetch(
  '/api/verify-email',
  {
    method: 'POST',
    body: computed(() => ({ email: email.value })),
    immediate: false
  }
);

const statusMessage = computed(() => {
  if (!result.value) return '';
  switch (result.value.status) {
    case 'valid':
      return '✓ 유효한 이메일';
    case 'invalid':
      return '✗ 유효하지 않은 이메일';
    default:
      return '? 알 수 없는 상태';
  }
});
</script>

고급 패턴

디바운스 검증

API 호출을 줄이기 위해 디바운스된 이메일 검증 사용.

import { ref, watch } from 'vue';
import { useEmailVerification } from './composables/useEmailVerification';

export function useDebouncedEmailVerification(apiKey: string, delay = 500) {
  const email = ref('');
  const debounceTimer = ref<NodeJS.Timeout | null>(null);
  const { verify, ...rest } = useEmailVerification(apiKey);

  watch(email, (newEmail) => {
    if (debounceTimer.value) {
      clearTimeout(debounceTimer.value);
    }

    debounceTimer.value = setTimeout(() => {
      if (newEmail) {
        verify(newEmail);
      }
    }, delay);
  });

  return { email, verify, ...rest };
}

이메일 캐싱 전략

API 호출을 줄이기 위한 지능적인 캐싱 구현.

const CACHE_KEY_PREFIX = 'emailverify_';
const CACHE_DURATION = 24 * 60 * 60 * 1000; // 24시간

export function useCachedEmailVerification(apiKey: string) {
  const { verify: verifyWithAPI, result, isLoading } = useEmailVerification(apiKey);

  const getCacheKey = (email: string) => `${CACHE_KEY_PREFIX}${email}`;

  const verify = async (email: string) => {
    const cacheKey = getCacheKey(email);
    const cached = localStorage.getItem(cacheKey);

    if (cached) {
      const { result: cachedResult, timestamp } = JSON.parse(cached);
      if (Date.now() - timestamp < CACHE_DURATION) {
        result.value = cachedResult;
        return;
      }
    }

    await verifyWithAPI(email);

    if (result.value) {
      localStorage.setItem(
        cacheKey,
        JSON.stringify({
          result: result.value,
          timestamp: Date.now()
        })
      );
    }
  };

  const clearCache = (email?: string) => {
    if (email) {
      localStorage.removeItem(getCacheKey(email));
    } else {
      const keys = Object.keys(localStorage);
      keys.forEach(key => {
        if (key.startsWith(CACHE_KEY_PREFIX)) {
          localStorage.removeItem(key);
        }
      });
    }
  };

  return { verify, result, isLoading, clearCache };
}

에러 처리 및 재시도

재시도 로직이 포함된 견고한 에러 처리 구현.

export function useEmailVerificationWithRetry(apiKey: string, maxRetries = 3) {
  const { verify: verifyWithAPI, result, isLoading, error } = useEmailVerification(apiKey);

  const verifyWithRetry = async (email: string, retryCount = 0): Promise<any> => {
    try {
      await verifyWithAPI(email);
      return result.value;
    } catch (err) {
      if (retryCount < maxRetries) {
        // 재시도 전 대기 (지수 백오프)
        await new Promise(resolve => setTimeout(resolve, Math.pow(2, retryCount) * 1000));
        return verifyWithRetry(email, retryCount + 1);
      }
      throw err;
    }
  };

  return { verifyWithRetry, result, isLoading, error };
}

TypeScript 지원

EmailVerify SDK는 타입 정의가 포함된 완전한 TypeScript 지원을 제공합니다.

interface VerificationRequest {
  email: string;
  apiKey: string;
}

interface VerificationResult {
  status: 'valid' | 'invalid' | 'unknown' | 'accept_all';
  result: {
    disposable: boolean;
    smtp_valid: boolean;
    format_valid: boolean;
    email_provider: string;
    risk_level: 'low' | 'medium' | 'high';
  };
  score: number;
}

interface UseEmailVerificationReturn {
  email: Ref<string>;
  isLoading: Ref<boolean>;
  error: Ref<string | null>;
  result: Ref<VerificationResult | null>;
  isValid: ComputedRef<boolean>;
  isInvalid: ComputedRef<boolean>;
  isDisposable: ComputedRef<boolean>;
  verify: (emailToVerify?: string) => Promise<void>;
  reset: () => void;
}

테스트

Vitest를 사용한 단위 테스트

Vitest로 이메일 검증 컴포저블을 테스트하세요.

import { describe, it, expect, vi } from 'vitest';
import { useEmailVerification } from './composables/useEmailVerification';

vi.mock('emailverify', () => ({
  emailverify: {
    verify: vi.fn()
  }
}));

describe('useEmailVerification', () => {
  it('유효한 이메일 검증', async () => {
    const { verify, result, isValid } = useEmailVerification('test-key');

    await verify('user@example.com');

    expect(result.value?.status).toBe('valid');
    expect(isValid.value).toBe(true);
  });

  it('유효하지 않은 이메일 표시', async () => {
    const { verify, result, isInvalid } = useEmailVerification('test-key');

    await verify('invalid@');

    expect(result.value?.status).toBe('invalid');
    expect(isInvalid.value).toBe(true);
  });

  it('검증 에러 처리', async () => {
    const { verify, error } = useEmailVerification('test-key');

    try {
      await verify('');
    } catch (err) {
      expect(error.value).toBeTruthy();
    }
  });
});

컴포넌트 테스트

Vue Test Utils로 Vue 컴포넌트를 테스트하세요.

import { mount } from '@vue/test-utils';
import { describe, it, expect } from 'vitest';
import EmailVerification from './EmailVerification.vue';

describe('EmailVerification', () => {
  it('입력 필드 렌더링', () => {
    const wrapper = mount(EmailVerification);
    expect(wrapper.find('input[type="email"]').exists()).toBe(true);
  });

  it('검증 상태 표시', async () => {
    const wrapper = mount(EmailVerification);
    await wrapper.find('input').setValue('user@example.com');
    await wrapper.find('input').trigger('blur');

    expect(wrapper.find('.status').exists()).toBe(true);
  });

  it('검증 중 로딩 상태 표시', async () => {
    const wrapper = mount(EmailVerification);
    await wrapper.find('input').setValue('user@example.com');

    expect(wrapper.find('.status.loading').exists()).toBe(true);
  });
});

관련 리소스

On this page