EmailVerify LogoEmailVerify

Next.js

Email checker for Next.js. Server-side and client-side email verification in React.

서버 측 유효성 검사, API 라우트 및 React 서버 컴포넌트를 사용하여 Next.js 애플리케이션에서 이메일 검증을 구현하세요.

설치

npm install @emailverify/node
yarn add @emailverify/node
pnpm add @emailverify/node

환경 설정

.env.local에 API 키를 추가하세요:

EMAILVERIFY_API_KEY=bv_live_xxx

App Router (Next.js 13+)

서버 액션

이메일 검증을 위한 서버 액션 생성:

// app/actions/verify-email.ts
'use server';

import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export async function verifyEmail(email: string) {
  try {
    const result = await client.verify(email);
    return {
      success: true,
      data: result,
    };
  } catch (error) {
    return {
      success: false,
      error: '검증 실패',
    };
  }
}

서버 액션을 사용하는 클라이언트 컴포넌트

// app/components/signup-form.tsx
'use client';

import { useState, useTransition } from 'react';
import { verifyEmail } from '../actions/verify-email';

export function SignupForm() {
  const [email, setEmail] = useState('');
  const [verification, setVerification] = useState<any>(null);
  const [isPending, startTransition] = useTransition();

  const handleEmailBlur = () => {
    if (!email) return;

    startTransition(async () => {
      const result = await verifyEmail(email);
      setVerification(result);
    });
  };

  return (
    <form className="space-y-4">
      <div>
        <label htmlFor="email" className="block text-sm font-medium">
          이메일
        </label>
        <input
          id="email"
          type="email"
          value={email}
          onChange={(e) => setEmail(e.target.value)}
          onBlur={handleEmailBlur}
          className="mt-1 block w-full rounded-md border-gray-300 shadow-sm"
        />

        {isPending && (
          <p className="text-sm text-gray-500 mt-1">검증 중...</p>
        )}

        {verification?.data?.status === 'valid' && (
          <p className="text-sm text-green-600 mt-1">✓ 이메일 검증됨</p>
        )}

        {verification?.data?.status === 'invalid' && (
          <p className="text-sm text-red-600 mt-1">
            유효한 이메일을 입력해 주세요
          </p>
        )}

        {verification?.data?.result?.disposable && (
          <p className="text-sm text-yellow-600 mt-1">
            영구적인 이메일을 사용해 주세요
          </p>
        )}
      </div>

      <button
        type="submit"
        disabled={isPending || verification?.data?.status !== 'valid'}
        className="w-full py-2 px-4 bg-blue-600 text-white rounded-md disabled:opacity-50"
      >
        가입하기
      </button>
    </form>
  );
}

API 라우트 핸들러

// app/api/verify-email/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export async function POST(request: NextRequest) {
  try {
    const body = await request.json();
    const { email } = body;

    if (!email) {
      return NextResponse.json(
        { error: '이메일은 필수입니다' },
        { status: 400 }
      );
    }

    const result = await client.verify(email);
    return NextResponse.json(result);
  } catch (error) {
    console.error('Verification error:', error);
    return NextResponse.json(
      { error: '검증 실패' },
      { status: 500 }
    );
  }
}

React 서버 컴포넌트

렌더링 전 서버 측에서 이메일 검증:

// app/user/[id]/page.tsx
import { EmailVerify } from '@emailverify/node';
import { notFound } from 'next/navigation';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

async function getUser(id: string) {
  // 데이터베이스에서 사용자 조회
  const user = await db.users.findUnique({ where: { id } });
  return user;
}

export default async function UserPage({ params }: { params: { id: string } }) {
  const user = await getUser(params.id);

  if (!user) {
    notFound();
  }

  // 아직 검증되지 않은 경우 이메일 검증
  let emailVerification = null;
  if (!user.emailVerified) {
    emailVerification = await client.verify(user.email);
  }

  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>

      {emailVerification && (
        <div className="mt-4">
          <h2>이메일 상태</h2>
          <p>상태: {emailVerification.status}</p>
          <p>점수: {emailVerification.score}</p>
        </div>
      )}
    </div>
  );
}

Pages Router (Next.js 12)

API 라우트

// pages/api/verify-email.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.method !== 'POST') {
    return res.status(405).json({ error: 'Method not allowed' });
  }

  const { email } = req.body;

  if (!email) {
    return res.status(400).json({ error: '이메일은 필수입니다' });
  }

  try {
    const result = await client.verify(email);
    return res.json(result);
  } catch (error) {
    console.error('Verification error:', error);
    return res.status(500).json({ error: '검증 실패' });
  }
}

getServerSideProps

SSR 중 이메일 검증:

// pages/user/[id].tsx
import { GetServerSideProps } from 'next';
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

interface Props {
  user: User;
  emailVerification: VerificationResult | null;
}

export const getServerSideProps: GetServerSideProps<Props> = async (context) => {
  const { id } = context.params!;

  const user = await getUser(id as string);

  if (!user) {
    return { notFound: true };
  }

  let emailVerification = null;
  if (!user.emailVerified) {
    try {
      emailVerification = await client.verify(user.email);
    } catch (error) {
      console.error('Verification failed:', error);
    }
  }

  return {
    props: {
      user,
      emailVerification,
    },
  };
};

export default function UserPage({ user, emailVerification }: Props) {
  return (
    <div>
      <h1>{user.name}</h1>
      {emailVerification && (
        <p>이메일 상태: {emailVerification.status}</p>
      )}
    </div>
  );
}

폼 처리 패턴

useFormState 사용 (React 19)

// app/components/form-with-state.tsx
'use client';

import { useFormState, useFormStatus } from 'react-dom';
import { signupAction } from '../actions/signup';

function SubmitButton() {
  const { pending } = useFormStatus();

  return (
    <button type="submit" disabled={pending}>
      {pending ? '제출 중...' : '가입하기'}
    </button>
  );
}

export function SignupFormWithState() {
  const [state, formAction] = useFormState(signupAction, {
    success: false,
    errors: {},
  });

  return (
    <form action={formAction}>
      <div>
        <input type="email" name="email" required />
        {state.errors?.email && (
          <p className="text-red-600">{state.errors.email}</p>
        )}
      </div>

      <div>
        <input type="text" name="name" required />
        {state.errors?.name && (
          <p className="text-red-600">{state.errors.name}</p>
        )}
      </div>

      <SubmitButton />

      {state.success && (
        <p className="text-green-600">계정이 성공적으로 생성되었습니다!</p>
      )}
    </form>
  );
}
// app/actions/signup.ts
'use server';

import { EmailVerify } from '@emailverify/node';
import { z } from 'zod';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

const signupSchema = z.object({
  email: z.string().email(),
  name: z.string().min(2),
});

export async function signupAction(
  prevState: any,
  formData: FormData
) {
  const rawData = {
    email: formData.get('email') as string,
    name: formData.get('name') as string,
  };

  // 스키마 유효성 검사
  const parsed = signupSchema.safeParse(rawData);
  if (!parsed.success) {
    return {
      success: false,
      errors: parsed.error.flatten().fieldErrors,
    };
  }

  // 이메일 검증
  const verification = await client.verify(parsed.data.email);

  if (verification.status === 'invalid') {
    return {
      success: false,
      errors: { email: ['유효한 이메일 주소를 입력해 주세요'] },
    };
  }

  if (verification.result.disposable) {
    return {
      success: false,
      errors: { email: ['일회용 이메일은 허용되지 않습니다'] },
    };
  }

  // 데이터베이스에 사용자 생성
  await createUser(parsed.data);

  return { success: true, errors: {} };
}

React Hook Form 사용

// app/components/react-hook-form-example.tsx
'use client';

import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod';
import { z } from 'zod';

const schema = z.object({
  email: z.string().email('잘못된 이메일입니다'),
  password: z.string().min(8, '비밀번호는 최소 8자 이상이어야 합니다'),
});

type FormData = z.infer<typeof schema>;

export function SignupFormHookForm() {
  const {
    register,
    handleSubmit,
    setError,
    formState: { errors, isSubmitting },
  } = useForm<FormData>({
    resolver: zodResolver(schema),
  });

  const onSubmit = async (data: FormData) => {
    // 이메일 검증
    const response = await fetch('/api/verify-email', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email: data.email }),
    });

    const verification = await response.json();

    if (verification.status === 'invalid') {
      setError('email', {
        type: 'manual',
        message: '유효한 이메일 주소를 입력해 주세요',
      });
      return;
    }

    // 가입 진행
    console.log('제출:', data);
  };

  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-4">
      <div>
        <input
          type="email"
          {...register('email')}
          className="w-full p-2 border rounded"
        />
        {errors.email && (
          <p className="text-red-600 text-sm">{errors.email.message}</p>
        )}
      </div>

      <div>
        <input
          type="password"
          {...register('password')}
          className="w-full p-2 border rounded"
        />
        {errors.password && (
          <p className="text-red-600 text-sm">{errors.password.message}</p>
        )}
      </div>

      <button
        type="submit"
        disabled={isSubmitting}
        className="w-full py-2 bg-blue-600 text-white rounded disabled:opacity-50"
      >
        {isSubmitting ? '검증 중...' : '가입하기'}
      </button>
    </form>
  );
}

미들웨어 검증

Next.js 미들웨어에서 이메일 검증:

// middleware.ts
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';

export async function middleware(request: NextRequest) {
  // 가입 라우트에서만 실행
  if (request.nextUrl.pathname !== '/api/signup') {
    return NextResponse.next();
  }

  const body = await request.json();
  const { email } = body;

  if (email) {
    // 가입 처리 전 이메일 검증
    const verifyResponse = await fetch(
      'https://api.emailverify.ai/v1/verify',
      {
        method: 'POST',
        headers: {
          'Authorization': `Bearer ${process.env.EMAILVERIFY_API_KEY}`,
          'Content-Type': 'application/json',
        },
        body: JSON.stringify({ email }),
      }
    );

    const verification = await verifyResponse.json();

    if (verification.status === 'invalid') {
      return NextResponse.json(
        { error: '유효하지 않은 이메일 주소입니다' },
        { status: 400 }
      );
    }
  }

  return NextResponse.next();
}

export const config = {
  matcher: '/api/signup',
};

캐싱 전략

라우트 핸들러 캐싱

// app/api/verify-email/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

// 인메모리 캐시 (프로덕션에서는 Redis 사용)
const cache = new Map<string, { result: any; timestamp: number }>();
const CACHE_DURATION = 3600000; // 1시간

export async function POST(request: NextRequest) {
  const { email } = await request.json();

  // 캐시 확인
  const cached = cache.get(email);
  if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
    return NextResponse.json(cached.result, {
      headers: { 'X-Cache': 'HIT' },
    });
  }

  // 이메일 검증
  const result = await client.verify(email);

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

  return NextResponse.json(result, {
    headers: { 'X-Cache': 'MISS' },
  });
}

Redis 캐싱 (프로덕션)

// lib/verify-with-cache.ts
import { Redis } from '@upstash/redis';
import { EmailVerify } from '@emailverify/node';

const redis = Redis.fromEnv();
const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export async function verifyEmailWithCache(email: string) {
  const cacheKey = `email-verify:${email}`;

  // 캐시 확인
  const cached = await redis.get(cacheKey);
  if (cached) {
    return { ...cached, fromCache: true };
  }

  // 이메일 검증
  const result = await client.verify(email);

  // 24시간 동안 캐시
  await redis.setex(cacheKey, 86400, result);

  return { ...result, fromCache: false };
}

대량 검증

API 라우트를 사용한 백그라운드 작업

// app/api/verify-bulk/route.ts
import { NextRequest, NextResponse } from 'next/server';
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export async function POST(request: NextRequest) {
  const { emails, webhookUrl } = await request.json();

  // 대량 작업 제출
  const job = await client.verifyBulk(emails, {
    webhookUrl: webhookUrl || `${process.env.NEXT_PUBLIC_URL}/api/webhook/bulk-complete`,
  });

  return NextResponse.json(job);
}

// 완료 웹훅 핸들러
// app/api/webhook/bulk-complete/route.ts
export async function POST(request: NextRequest) {
  const event = await request.json();

  if (event.event === 'bulk.completed') {
    // 완료된 작업 처리
    await processCompletedJob(event.data.job_id);
  }

  return NextResponse.json({ received: true });
}

에러 처리

전역 에러 핸들러

// lib/emailverify.ts
import { EmailVerify } from '@emailverify/node';

const client = new EmailVerify({
  apiKey: process.env.EMAILVERIFY_API_KEY!,
});

export async function verifyEmailSafe(email: string) {
  try {
    const result = await client.verify(email);
    return { success: true, data: result };
  } catch (error: any) {
    // 모니터링을 위한 에러 로깅
    console.error('Email verification failed:', {
      email,
      error: error.message,
      code: error.code,
    });

    // 우아한 폴백 반환
    if (error.code === 'RATE_LIMIT_EXCEEDED') {
      return {
        success: false,
        error: '속도 제한 초과',
        retryAfter: error.retryAfter,
      };
    }

    if (error.code === 'INSUFFICIENT_CREDITS') {
      return {
        success: false,
        error: '서비스를 일시적으로 사용할 수 없습니다',
      };
    }

    return {
      success: false,
      error: '검증 실패',
    };
  }
}

TypeScript 설정

완전한 타입 안전성을 위해 타입을 확장하세요:

// types/emailverify.ts
import type { VerificationResult } from '@emailverify/node';

export interface VerifyEmailResponse {
  success: boolean;
  data?: VerificationResult;
  error?: string;
  fromCache?: boolean;
}

export interface SignupFormState {
  success: boolean;
  errors: {
    email?: string[];
    name?: string[];
  };
}

모범 사례

1. 항상 서버 측에서 검증

// ✅ 좋음 - 서버 검증
export async function signupAction(data: FormData) {
  const email = data.get('email') as string;
  const verification = await verifyEmail(email);
  // ...
}

// ❌ 나쁨 - 클라이언트 전용 검증은 우회될 수 있음

2. 모든 상태 처리

function EmailStatus({ verification }: { verification: any }) {
  if (verification.isLoading) return <Loading />;
  if (verification.error) return <ErrorMessage />;
  if (!verification.data) return null;

  switch (verification.data.status) {
    case 'valid':
      return <ValidBadge />;
    case 'invalid':
      return <InvalidMessage />;
    case 'unknown':
      return <UnknownWarning />;
    case 'accept_all':
      return <CatchAllWarning />;
    default:
      return null;
  }
}

3. 사용자 흐름을 차단하지 않기

// 제출은 허용하되 사용자에게 경고
<button
  type="submit"
  disabled={isSubmitting}
>
  {verification?.status === 'invalid'
    ? '그래도 제출'
    : '제출'}
</button>

관련 리소스

On this page