Next.js
Email checker for Next.js. Server-side and client-side email verification in React.
서버 측 유효성 검사, API 라우트 및 React 서버 컴포넌트를 사용하여 Next.js 애플리케이션에서 이메일 검증을 구현하세요.
설치
npm install @emailverify/nodeyarn add @emailverify/nodepnpm add @emailverify/node환경 설정
.env.local에 API 키를 추가하세요:
EMAILVERIFY_API_KEY=bv_live_xxxApp 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>