
2025.6.28 토요일의 공부기록
이번 글에서는 Twilio를 사용하여 SMS 인증번호를 발송하는 방법을 Next.js 기반의 실제 코드로 상세히 알아본다. 추가적으로, Twilio 무료 계정의 한계점과 이를 극복할 수 있는 대안(Vonage)에 대해서도 다룬다.
Twilio는 SMS, 전화, 이메일 등의 다양한 통신 서비스를 제공하는 클라우드 기반 플랫폼이다. 특히 SMS 발송을 위한 API가 직관적이고 강력해서 사용자 인증 등의 목적으로 널리 사용된다.
npm i twilio
import twilio from 'twilio';
const client = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
client.messages.create({
body: 'Hello from Twilio!',
to: '+821012345678', // 실제 수신자의 전화번호
from: '+12025550123', // Twilio에서 제공하는 번호
}).then((message) => console.log(message.sid));
.env 파일에서 안전하게 관리해야 한다.다음은 실제 Next.js 환경에서 구현된 SMS 인증번호 발송 및 검증 코드이다.
"use server";
import { z } from "zod";
import validator from "validator";
import db from "@/lib/db";
import crypto from "crypto";
import { getSession } from "@/lib/session";
import { redirect } from "next/navigation";
import twilio from "twilio";
// 전화번호 유효성 검증
const phoneSchema = z
.string()
.trim()
.refine(
(phone) => validator.isMobilePhone(phone, "ko-KR"),
"Wrong phone number format"
);
// 토큰 존재 여부 검증 함수
async function tokenExists(token: number) {
const exists = await db.sMSToken.findUnique({
where: { token: token.toString() },
select: { id: true },
});
return Boolean(exists);
}
// 토큰 유효성 검사
const tokenSchema = z.coerce
.number()
.min(100000)
.max(999999)
.refine(tokenExists, "This token does not exist");
interface ActionState {
token: boolean;
error?: object;
}
// 랜덤한 SMS 토큰 생성
async function getToken() {
const token = crypto.randomInt(100000, 999999).toString();
const existingToken = await db.sMSToken.findUnique({
where: { token },
select: { id: true },
});
if (existingToken) return getToken();
return token;
}
// SMS 로그인 액션
export async function smsLogIn(prevState: ActionState, formData: FormData) {
const phone = formData.get("phone");
const token = formData.get("token");
if (!prevState.token) {
const result = phoneSchema.safeParse(phone);
if (!result.success) {
return { token: false, error: result.error.flatten() };
}
// 이전 토큰 삭제 후 새로운 토큰 생성
await db.sMSToken.deleteMany({ where: { user: { phone: result.data } } });
const newToken = await getToken();
await db.sMSToken.create({
data: {
token: newToken,
user: {
connectOrCreate: {
where: { phone: result.data },
create: {
phone: result.data,
username: crypto.randomBytes(10).toString("hex"),
},
},
},
},
});
// Twilio로 SMS 전송
const client = twilio(process.env.TWILIO_SID, process.env.TWILIO_TOKEN);
await client.messages.create({
body: `Your Karrot verification code is ${newToken}`,
from: process.env.TWILIO_PHONE_NUMBER!,
to: process.env.MY_PHONE_NUMBER!, // 실제 사용자 번호로 교체 필요
});
return { token: true };
} else {
const result = await tokenSchema.safeParseAsync(token);
if (!result.success) {
return { token: true, error: result.error.flatten() };
}
// 인증 성공 시 세션 생성 후 로그인
const validToken = await db.sMSToken.findUnique({
where: { token: result.data.toString() },
select: { id: true, user: true },
});
const session = await getSession();
session.userId = validToken!.user.id;
await session.save();
await db.sMSToken.delete({ where: { id: validToken!.id } });
redirect("/profile");
}
}
Twilio의 무료 계정에서는 인증되지 않은 번호로 SMS를 보낼 수 없다. 즉, Verified Number로 등록된 번호로만 발송 가능하다. 만약 실서비스에서 제대로 활용하려면 다음 중 하나를 선택해야 한다:
Vonage (구 Nexmo)는 Twilio와 유사한 SMS 발송 서비스를 제공한다. Vonage는 무료 계정으로도 특정 국가로 메시지 전송이 가능해 Twilio의 무료 계정의 한계를 극복할 수 있는 대안으로 적합하다.
npm install @vonage/server-sdk
npm install @vonage/auth
import { Vonage } from "@vonage/server-sdk";
import { Auth } from "@vonage/auth";
const credentials = new Auth({
apiKey: process.env.VONAGE_API_KEY,
apiSecret: process.env.VONAGE_API_SECRET,
});
const vonage = new Vonage(credentials);
await vonage.sms
.send({
to: process.env.MY_PHONE_NUMBER!,
//to: result.data,
from: process.env.VONAGE_SMS_FROM!,
text: `Your Karrot verification code is: ${token}`,
})
대안을 활용하여 원활히 서비스를 운영해 보자.