
2025.6.27 금요일의 공부기록
이번 글에서는 Next.js 애플리케이션에서 SMS 인증 토큰 검증 기능을 구현하는 방법에 대해 다룬다. 인증번호의 존재 여부 확인, 사용자 로그인 처리, 그리고 세션 관리까지이다.
사용자가 받은 SMS 인증 토큰을 입력하면 다음의 과정을 통해 유효성을 검증하고 사용자를 로그인 처리한다.
과정 요약:
app/sms/actions.ts)"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";
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;
}
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;
}
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() };
} else {
// delete previous token
await db.sMSToken.deleteMany({
where: {
user: {
phone: result.data,
},
},
});
// create new token
const token = await getToken();
await db.sMSToken.create({
data: {
token,
user: {
connectOrCreate: {
where: {
phone: result.data,
},
create: {
phone: result.data,
username: crypto.randomBytes(10).toString("hex"),
},
},
},
},
});
// send token to phone using twilio
return { token: true };
}
} else {
const result = await tokenSchema.safeParseAsync(token);
if (!result.success) {
return {
token: true,
error: result.error.flatten(),
};
} else {
const token = await db.sMSToken.findUnique({
where: {
token: result.data.toString(),
},
select: {
id: true,
user: true,
},
});
const session = await getSession();
session.userId = token!.user.id;
await session.save();
await db.sMSToken.delete({
where: {
id: token!.id,
},
});
redirect("/profile");
}
}
}
tokenExists 함수)async function tokenExists(token: number) {
const exists = await db.sMSToken.findUnique({
where: {
token: token.toString(),
},
select: {
id: true,
},
});
return Boolean(exists);
}
tokenSchema)100000)과 최대값(999999)을 설정하여 6자리 숫자인지 확인한다.tokenExists 함수로 토큰이 실제 존재하는지 검증한다.const tokenSchema = z.coerce
.number()
.min(100000)
.max(999999)
.refine(tokenExists, "This token does not exist");
const result = await tokenSchema.safeParseAsync(token);
if (!result.success) {
return {
token: true,
error: result.error.flatten(),
};
} else {
const token = await db.sMSToken.findUnique({
where: {
token: result.data.toString(),
},
select: {
id: true,
user: true,
},
});
const session = await getSession();
session.userId = token!.user.id;
await session.save();
await db.sMSToken.delete({
where: {
id: token!.id,
},
});
redirect("/profile");
}
| 단계 | 작업 | 목적 |
|---|---|---|
| 1 | 전화번호 제출 시 SMS 토큰 생성 및 발송 | 사용자 인증을 위한 토큰 제공 |
| 2 | 사용자 토큰 입력 | 인증 요청 |
| 3 | 토큰 형식 및 DB 존재 여부 확인 | 유효한 인증 번호인지 검사 |
| 4 | 사용자 세션 생성 및 로그인 처리 | 인증 성공 후 사용자 접근 권한 부여 |
| 5 | 인증 완료 후 토큰 삭제 | 재사용 방지 및 보안 강화 |