Better Auth에서 OTP를 이용한 안전한 비밀번호 찾기 흐름 구축 방법
Source: Dev.to

우리의 previous guide에서는 Better Auth와 Nodemailer를 설정해 이메일 인증 및 초대를 처리했습니다. 오늘은 모든 프로덕션 앱에 필수적인 기능인 “Forgot Password” flow를 다룹니다.
Magic Links(즉시 로그인하거나 비밀번호를 재설정하는 링크)는 인기가 있지만, 사용자를 앱 밖으로 내보내 이메일을 열고 새 탭을 여는 링크를 클릭하도록 강제합니다.
때때로 사용자를 현재 위치에 그대로 두고 싶을 때가 있습니다. 이 가이드에서는 OTP (One‑Time Password) 재설정 흐름을 구축합니다. 사용자는 이메일을 입력하고, 6자리 코드를 받아 탭을 닫지 않은 채 비밀번호를 재설정합니다.
백엔드 구성
Good news: if you followed the previous guide, your backend is already ready.
The emailOTP plugin we installed previously handles the forget-password type automatically. When we call the forgot‑password function from the client, Better Auth will trigger the sendVerificationOTP hook we defined in auth.ts with the type set to "forget-password".
Just ensure your email.ts template handles the subject line dynamically (or is generic enough) for password resets.
Source: …
클라이언트‑사이드 구현
우리 UI에서는 두 가지 별도 단계를 처리해야 합니다:
- 요청 단계 – 사용자가 이메일을 입력 → 시스템이 OTP를 전송합니다.
- 재설정 단계 – 사용자가 OTP와 새 비밀번호를 입력 → 시스템이 자격 증명을 업데이트합니다.
1. 로직 훅
200줄짜리 UI 코드를 모두 나열하기보다, authClient를 이용한 핵심 함수를 살펴보겠습니다.
import { authClient } from "@/lib/auth-client";
import { useState } from "react";
export const usePasswordReset = () => {
const [stage, setStage] = useState("request");
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Step 1: Send the OTP
const sendResetOtp = async (email: string) => {
setIsLoading(true);
setError(null);
const { error } = await authClient.forgetPassword.emailOtp({
email,
redirectTo: "/dashboard", // Optional: strictly for flow control
});
setIsLoading(false);
if (error) {
setError(error.message);
return false;
}
setStage("reset"); // Move to next UI stage
return true;
};
// Step 2: Validate OTP and set new password
const resetPassword = async (
email: string,
otp: string,
password: string
) => {
setIsLoading(true);
setError(null);
const { error } = await authClient.emailOtp.resetPassword({
email,
otp,
password,
});
setIsLoading(false);
if (error) {
setError(error.message);
return false;
}
return true; // Success!
};
return {
stage,
setStage,
isLoading,
error,
sendResetOtp,
resetPassword,
};
};
2. UI 구현
아래는 간소화된 UI 예시입니다. 프리미엄 느낌을 위해 Tailwind 그라디언트를 그대로 사용해도 좋습니다.
요청 폼 (단계 1)
if (stage === "request") {
return (
<>
<h2>Forgot Password</h2>
<p>Enter your email to receive a verification code.</p>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="name@example.com"
/>
<button
onClick={() => sendResetOtp(email)}
disabled={isLoading || !email}
>
{isLoading ? "Sending Code…" : "Send OTP"}
</button>
{error && <p className="error">{error}</p>}
</>
);
}
재설정 폼 (단계 2)
if (stage === "reset") {
return (
<>
<h2>Reset Password</h2>
<p>Code sent to {email}</p>
{/* OTP Input – limit to 6 numbers */}
<label>OTP Code</label>
<input
type="text"
value={otp}
onChange={(e) =>
setOtp(e.target.value.replace(/\D/g, "").slice(0, 6))
}
className="tracking-widest text-center text-lg"
placeholder="000000"
/>
<label>New Password</label>
<input
type="password"
value={newPassword}
onChange={(e) => setNewPassword(e.target.value)}
/>
<label>Confirm Password</label>
<input
type="password"
value={confirmPassword}
onChange={(e) => setConfirmPassword(e.target.value)}
/>
<button
onClick={() => {
if (newPassword === confirmPassword) {
resetPassword(email, otp, newPassword);
}
}}
disabled={isLoading}
>
{isLoading ? "Resetting…" : "Set New Password"}
</button>
{/* Resend Logic */}
<button
onClick={() => sendResetOtp(email)}
className="text-sm underline mt-4"
>
Resend Code
</button>
</>
);
}
매직 링크 vs. OTP: 어느 것을 선택해야 할까요?
Better Auth는 두 가지 접근 방식을 모두 지원합니다. 아래는 어떤 방식이 귀하의 앱에 더 적합한지 결정하는 데 도움이 되는 간략한 비교표입니다.
| 기능 | 매직 링크 | OTP |
|---|---|---|
| 사용자 경험 | 앱을 떠나 이메일을 엽니다 | 앱 내에 머무릅니다 |
| 구현 복잡도 | 간단 – 링크만 클릭 | 코드 입력 UI 필요 |
| 보안 | 한 번 클릭, 이메일 탈취에 취약 | 코드가 빠르게 만료, 피싱이 어려움 |
| 사용 사례 | 빠른 로그인, 낮은 마찰 흐름 | 비밀번호 재설정, 고보안 상황 |
제품의 UX 목표와 보안 요구 사항에 맞는 방식을 선택하세요. 즐거운 코딩 되세요!
OTP 접근 방식 (이 가이드)
장점
- 원활한 컨텍스트: 사용자가 애플리케이션 탭을 떠나지 않아 이탈률이 감소합니다.
- 모바일 친화적: 딥링크(Universal Links)를 처리하기가 버그가 있거나 설정이 복잡한 모바일 앱에 이상적입니다.
- 보안 인식: 사용자는 2FA 코드에 익숙하므로 코드를 입력하는 것이 안전하게 느껴집니다.
단점
- 마찰: 사용자가 코드를 직접 복사‑붙여넣기(또는 입력)해야 합니다.
- 오타: 잘못 입력된 코드나 비밀번호는 추가 오류 처리가 필요합니다.
매직 링크 접근법
장점
- 제로 마찰: 한 번 클릭하면 사용자는 완료됩니다.
- 단순함: 사용자가 “코드”가 무엇인지 이해할 필요가 없습니다.
단점
- 컨텍스트 전환: 사용자가 새 브라우저 창/탭을 열도록 강제합니다.
- 이메일 스캐너: 공격적인 기업 이메일 스캐너가 악성코드 검사를 위해 링크를 “클릭”할 수 있어, 사용자가 보기 전에 일회용 토큰이 무효화될 수 있습니다.
결론
Better Auth의 emailOTP 플러그인을 사용하여, 사용자를 애플리케이션 내에 머무르게 하는 비밀번호 재설정 흐름을 만들었습니다. 작은 UX 디테일이지만, 사용자를 앱 생태계 안에 머무르게 하면 전환율과 유지율이 항상 높아집니다.
자세한 내용은 우리의 블로그를 참고하세요.
