[Next.js] 서류 없이 5분 만에 회원가입 SMS 본인인증 구현하기 (feat. App Router)
사이드 프로젝트에 SMS 인증을 붙이려다 좌절한 적 있으신가요?
새로운 서비스의 MVP(Minimum Viable Product)를 개발하거나 사이드 프로젝트를 진행할 때, 사용자 본인인증은 필수적인 기능 중 하나입니다. 가짜 계정 생성을 막고, 안전한 서비스 환경을 구축하기 위해 가장 널리 쓰이는 방법이 바로 '휴대폰 SMS 인증'이죠.
하지만 국내에서 SMS API를 연동하려고 하면 시작부터 큰 장벽에 부딪히게 됩니다. API 문서를 읽기도 전에 다음과 같은 서류들을 요구하기 때문입니다:
- 사업자등록증 사본
- 통신서비스 이용증명원
- 발신번호 사전등록
주말 해커톤이나 아직 법인을 설립하지 않은 개인 개발자, 프리랜서에게 이러한 절차는 사실상 '구현 불가'를 의미합니다.
이 글에서는 복잡한 서류 제출 없이, 단 5분 만에 Next.js(App Router) 환경에서 SMS OTP 인증을 구현하는 방법을 알아봅니다. Server Actions를 활용한 최신 Next.js 패턴과 함께, 즉시 실무에 적용할 수 있는 코드를 제공합니다.
이 글에서 배울 내용
- Next.js App Router와 Server Actions를 활용한 안전한 API 통신 구조
- 서류 없이 즉시 사용할 수 있는 SMS API 연동 방법
- 타이머, 에러 처리 등을 포함한 완전한 클라이언트 UI 구현
- 실무에 필요한 보안 및 어뷰징 방지 팁
1단계: API 인증 정보 설정하기
복잡한 심사 과정 없이 바로 사용할 수 있는 EasyAuth(이지어스) API를 기준으로 진행합니다. (가입 시 즉시 발급되는 API Key를 사용합니다)
프로젝트 루트의 .env.local 파일에 다음 환경 변수를 추가합니다.
# .env.local
EASYAUTH_API_KEY=your_api_key_here
보안 팁: NEXT_PUBLIC_ 접두사를 붙이지 않아야 API 키가 브라우저에 노출되지 않습니다.
2단계: Server Actions 작성 (SMS 발송 및 검증)
Next.js 14 이후부터는 Server Actions를 사용하여 클라이언트에서 서버 측 코드를 직접 호출할 수 있습니다. 이를 통해 API Routes를 따로 만들 필요 없이, 백엔드 로직을 안전하게 감출 수 있습니다.
app/actions/auth.ts 파일을 생성하고 다음 코드를 작성합니다.
'use server';
// app/actions/auth.ts
interface ActionResponse {
success: boolean;
message?: string;
}
// 1. 인증번호 발송 액션
export async function sendVerificationSMS(phoneNumber: string): Promise {
try {
// 전화번호 형식 검증 (숫자만 추출)
const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
if (cleanPhone.length < 10 || cleanPhone.length > 11) {
return { success: false, message: '유효한 휴대폰 번호를 입력해주세요.' };
}
const response = await fetch('https://api.easyauth.kr/send', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
},
body: JSON.stringify({ to: cleanPhone })
});
if (!response.ok) {
throw new Error('SMS 발송에 실패했습니다.');
}
return { success: true };
} catch (error) {
console.error('SMS Send Error:', error);
return { success: false, message: '서버 오류가 발생했습니다. 잠시 후 다시 시도해주세요.' };
}
}
// 2. 인증번호 검증 액션
export async function verifySMSCode(phoneNumber: string, code: string): Promise {
try {
const cleanPhone = phoneNumber.replace(/[^0-9]/g, '');
const response = await fetch('https://api.easyauth.kr/verify', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.EASYAUTH_API_KEY}`
},
body: JSON.stringify({ to: cleanPhone, code })
});
if (!response.ok) {
return { success: false, message: '인증번호가 일치하지 않거나 만료되었습니다.' };
}
return { success: true };
} catch (error) {
console.error('SMS Verify Error:', error);
return { success: false, message: '인증 처리 중 오류가 발생했습니다.' };
}
}
3단계: 클라이언트 UI 컴포넌트 구현
이제 사용자가 전화번호와 인증번호를 입력할 수 있는 UI를 만듭니다. 3분(180초) 타이머 기능도 함께 구현합니다.
app/components/SmsAuth.tsx를 생성합니다.
'use client';
import { useState, useEffect } from 'react';
import { sendVerificationSMS, verifySMSCode } from '../actions/auth';
export default function SmsAuth() {
const [phone, setPhone] = useState('');
const [code, setCode] = useState('');
const [step, setStep] = useState<'IDLE' | 'SENT' | 'VERIFIED'>('IDLE');
const [timeLeft, setTimeLeft] = useState(180); // 3분
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');
// 타이머 로직
useEffect(() => {
if (step === 'SENT' && timeLeft > 0) {
const timer = setInterval(() => setTimeLeft((prev) => prev - 1), 1000);
return () => clearInterval(timer);
}
}, [step, timeLeft]);
const formatTime = (seconds: number) => {
const m = Math.floor(seconds / 60);
const s = seconds % 60;
return `${m}:${s.toString().padStart(2, '0')}`;
};
const handleSend = async () => {
setLoading(true);
setError('');
const res = await sendVerificationSMS(phone);
if (res.success) {
setStep('SENT');
setTimeLeft(180);
} else {
setError(res.message || '발송 실패');
}
setLoading(false);
};
const handleVerify = async () => {
if (timeLeft === 0) {
setError('인증 시간이 만료되었습니다. 다시 요청해주세요.');
return;
}
setLoading(true);
setError('');
const res = await verifySMSCode(phone, code);
if (res.success) {
setStep('VERIFIED');
} else {
setError(res.message || '인증 실패');
}
setLoading(false);
};
return (
<div>
<h2>휴대폰 본인인증</h2>
{/* 에러 메시지 표시 */}
{error && <p>{error}</p>}
{step === 'VERIFIED' ? (
<div>
✅ 인증이 완료되었습니다!
</div>
) : (
<div>
{/* 전화번호 입력부 */}
<div>
휴대폰 번호
<div>
setPhone(e.target.value)}
placeholder="01012345678"
disabled={step === 'SENT'}
className="flex-1 p-2 border rounded focus:ring-2 focus:ring-blue-500"
/>
{step === 'SENT' ? '재전송' : '인증번호 받기'}
</div>
</div>
{/* 인증번호 입력부 */}
{step === 'SENT' && (
<div>
인증번호 <span>({formatTime(timeLeft)})</span>
<div>
setCode(e.target.value)}
placeholder="6자리 숫자"
maxLength={6}
className="flex-1 p-2 border rounded focus:ring-2 focus:ring-blue-500"
/>
확인
</div>
</div>
)}
</div>
)}
</div>
);
}
Tips & Best Practices (실무 팁)
1. 보안과 비용을 위한 Rate Limiting (호출 제한)
악의적인 사용자가 무단으로 SMS 발송 API를 계속 호출하면 과도한 비용이 발생할 수 있습니다.
Next.js 환경에서는 @upstash/ratelimit과 Redis를 활용해 IP당 하루 최대 발송 횟수(예: 5회)를 제한하는 것을 강력히 권장합니다.
2. 연락처 입력 정규식 처리
사용자들은 010-1234-5678처럼 하이픈을 넣기도 하고, 01012345678로 붙여 쓰기도 합니다. 클라이언트에서 1차적으로 하이픈을 제거하고, 서버 Action에서도 replace(/[^0-9]/g, '')를 통해 숫자만 남기도록 이중 처리하는 것이 안전합니다.
3. 인증 만료 시간 설정 (3분 규칙)
OTP 보안의 핵심은 제한된 시간입니다. 클라이언트 UI에서 3분 타이머를 보여주는 것은 물론이고, 백엔드(API)에서도 발송된 지 3분이 지난 코드는 무효화 처리해야 합니다.
마치며: 개발자를 위한 가장 쉬운 선택, EasyAuth
지금까지 Next.js를 이용해 회원가입용 SMS 본인인증을 구현하는 방법을 살펴보았습니다. 코드는 간단하지만, 막상 실제로 구현하려고 하면 **'국내 통신망 심사'**라는 거대한 장벽 때문에 코딩보다 서류 준비에 더 많은 시간을 쏟아야 하는 경우가 많습니다.
토이 프로젝트, 스타트업 MVP, 1인 개발을 진행 중이라면 **EasyAuth(이지어스)**를 추천합니다.
- 서류 0장: 사업자등록증, 통신서비스 이용증명원 제출이 필요 없습니다.
- 즉시 연동: 회원가입 후 발급받은 API 키로 즉시 발송 가능합니다. (자동 발신번호 사용)
- 압도적 가성비: 기존 API들이 건당 30~50원인 반면, EasyAuth는 건당 15~25원으로 매우 합리적입니다.
- 무료 테스트: 가입 시 10건의 무료 크레딧이 제공되어 개발 및 테스트 비용이 들지 않습니다.
단 두 개의 엔드포인트(POST /send, POST /verify)만으로 끝나는 초간단 SMS 인증 API, 지금 바로 EasyAuth로 5분 만에 본인인증 로직을 완성해 보세요!