비트베이크

Next.js 15 + Better Auth로 SMS 인증 구현하기: 2026 완벽 가이드

2026-03-24T01:03:35.061Z

BETTER-AUTH-SMS

Next.js 15 + Better Auth로 SMS 인증 구현하기: 2026 완벽 가이드

SMS 인증을 붙이려고 했는데, 사업자등록증 제출하라고? 발신번호 사전등록은 또 뭐야?

사이드 프로젝트에 전화번호 인증 하나 붙이려고 일주일을 날릴 수는 없습니다. 이 글에서는 Better Auth의 Phone Number 플러그인과 Next.js 15 App Router를 사용해서, 실제로 동작하는 SMS OTP 인증을 처음부터 끝까지 구현합니다.

이 글에서 배우는 것

  • Better Auth 설치 및 Next.js 15 연동
  • Phone Number 플러그인 설정
  • SMS 발송 API 연결 (커스텀 프로바이더)
  • OTP 전송/검증 UI 구현
  • 보안 베스트 프랙티스

1단계: 프로젝트 셋업

Better Auth 설치

npx create-next-app@latest my-auth-app --app --typescript
cd my-auth-app
npm install better-auth

환경 변수 설정

.env.local 파일을 생성합니다:

BETTER_AUTH_SECRET=your-secret-key-at-least-32-characters-long
BETTER_AUTH_URL=http://localhost:3000
DATABASE_URL=file:./dev.db
SMS_API_KEY=your-sms-api-key

BETTER_AUTH_SECRET은 최소 32자 이상이어야 합니다. openssl rand -base64 32로 생성할 수 있습니다.


2단계: Better Auth 서버 설정

lib/auth.ts에 인증 인스턴스를 생성합니다:

// lib/auth.ts
import { betterAuth } from "better-auth";
import { phoneNumber } from "better-auth/plugins";
import Database from "better-sqlite3";

export const auth = betterAuth({
  database: new Database("./sqlite.db"),
  plugins: [
    phoneNumber({
      otpLength: 6,
      expiresIn: 300, // 5분
      allowedAttempts: 3,
      sendOTP: async ({ phoneNumber, code }, ctx) => {
        // SMS API를 통해 인증번호 발송
        await fetch("https://api.easyauth.kr/v1/send", {
          method: "POST",
          headers: {
            "Content-Type": "application/json",
            "Authorization": `Bearer ${process.env.SMS_API_KEY}`,
          },
          body: JSON.stringify({
            phoneNumber,
            code,
          }),
        });
        // ⚠️ Better Auth 공식 문서에서는 sendOTP를 await하지 않는 것을 권장합니다
        // 타이밍 공격 방지 및 응답 속도 향상을 위해서입니다
      },
      signUpOnVerification: {
        getTempEmail: (phoneNumber) => `${phoneNumber}@phone.local`,
        getTempName: (phoneNumber) => phoneNumber,
      },
    }),
  ],
});

Next.js Route Handler 연결

// app/api/auth/[...all]/route.ts
import { toNextJsHandler } from "better-auth/next-js";
import { auth } from "@/lib/auth";

export const { POST, GET } = toNextJsHandler(auth);

DB 마이그레이션

npx auth migrate

이 명령이 user 테이블에 phoneNumberphoneNumberVerified 필드를 자동 추가합니다.


3단계: 클라이언트 설정

// lib/auth-client.ts
import { createAuthClient } from "better-auth/react";
import { phoneNumberClient } from "better-auth/client/plugins";

export const authClient = createAuthClient({
  plugins: [phoneNumberClient()],
});

4단계: SMS 인증 UI 구현

OTP 발송 + 검증 컴포넌트

// app/components/PhoneAuth.tsx
"use client";

import { useState } from "react";
import { authClient } from "@/lib/auth-client";

export default function PhoneAuth() {
  const [phone, setPhone] = useState("");
  const [code, setCode] = useState("");
  const [step, setStep] = useState<"phone" | "verify">("phone");
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState("");

  const handleSendOTP = async () => {
    setLoading(true);
    setError("");
    try {
      await authClient.phoneNumber.sendOtp({
        phoneNumber: phone,
      });
      setStep("verify");
    } catch (e) {
      setError("인증번호 발송에 실패했습니다. 번호를 확인해주세요.");
    } finally {
      setLoading(false);
    }
  };

  const handleVerify = async () => {
    setLoading(true);
    setError("");
    try {
      const result = await authClient.phoneNumber.verify({
        phoneNumber: phone,
        code,
      });
      // 인증 성공 → 세션 생성됨
      window.location.href = "/dashboard";
    } catch (e) {
      setError("인증번호가 일치하지 않습니다.");
    } finally {
      setLoading(false);
    }
  };

  return (
    <div>
      <h1>전화번호 인증</h1>

      {step === "phone" ? (
        <div>
           setPhone(e.target.value)}
            className="w-full p-3 border rounded-lg"
          /&gt;
          
            {loading ? "발송 중..." : "인증번호 받기"}
          
        </div>
      ) : (
        <div>
          <p>
            {phone}으로 발송된 6자리 코드를 입력하세요
          </p>
           setCode(e.target.value)}
            className="w-full p-3 border rounded-lg text-center text-2xl tracking-widest"
          /&gt;
          
            {loading ? "확인 중..." : "인증하기"}
          
           { setStep("phone"); setCode(""); }}
            className="w-full p-3 text-gray-500 text-sm"
          &gt;
            번호 다시 입력
          
        </div>
      )}

      {error &amp;&amp; <p>{error}</p>}
    </div>
  );
}

5단계: 세션 확인 (서버 컴포넌트)

// app/dashboard/page.tsx
import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";

export default async function Dashboard() {
  const session = await auth.api.getSession({
    headers: await headers(),
  });

  if (!session) redirect("/login");

  return (
    <div>
      <h1>환영합니다!</h1>
      <p>전화번호: {session.user.phoneNumber}</p>
      <p>인증 상태: {session.user.phoneNumberVerified ? "✅" : "❌"}</p>
    </div>
  );
}

보안 베스트 프랙티스

Rate Limiting

OTP 발송 남용을 막기 위해 반드시 Rate Limiting을 적용하세요:

phoneNumber({
  allowedAttempts: 3, // 3회 실패 시 OTP 무효화
  expiresIn: 300,     // 5분 후 만료
  sendOTP: async ({ phoneNumber, code }) =&gt; {
    // 발송 전 rate limit 체크
  },
})

Better Auth는 allowedAttempts 초과 시 자동으로 OTP를 삭제하고 403 에러를 반환합니다. 사용자는 새 코드를 요청해야 합니다.

전화번호 유효성 검사

phoneNumber({
  phoneNumberValidator: (phoneNumber) =&gt; {
    // 한국 전화번호 형식 검증
    return /^01[016789]\d{7,8}$/.test(phoneNumber.replace(/-/g, ""));
  },
})

타이밍 공격 방지

Better Auth 문서에서 권장하듯, sendOTP 함수 내에서 SMS 발송을 await하지 않는 패턴을 사용하면 발송 성공/실패 여부와 관계없이 일정한 응답 시간을 유지할 수 있습니다.


SMS 프로바이더 선택 가이드

SMS 인증 구현에서 가장 귀찮은 부분이 SMS 발송 프로바이더 연동입니다. 대부분의 국내 서비스는 사업자등록증, 발신번호 사전등록, 이용증명원 등을 요구합니다.

사이드 프로젝트나 MVP 단계에서 이런 서류를 준비하는 건 현실적으로 어렵습니다. EasyAuth(이지어스)처럼 서류 없이 가입 후 바로 API 키를 발급받아 사용할 수 있는 서비스를 활용하면, sendOTP 함수 안에 API 호출 코드만 넣으면 전체 인증 플로우가 완성됩니다. 건당 15~25원으로 가격도 합리적이고, 가입 시 10건 무료 체험도 제공합니다.


전체 프로젝트 구조

my-auth-app/
├── app/
│   ├── api/auth/[...all]/route.ts
│   ├── components/PhoneAuth.tsx
│   ├── dashboard/page.tsx
│   ├── login/page.tsx
│   └── layout.tsx
├── lib/
│   ├── auth.ts
│   └── auth-client.ts
├── .env.local
└── package.json

마무리

Better Auth의 Phone Number 플러그인은 sendOTP 콜백 하나만 구현하면 전체 OTP 인증 플로우(발송, 검증, 세션 생성, 재시도 제한)를 알아서 처리해줍니다. Next.js 15 App Router와의 통합도 Route Handler 파일 하나면 끝입니다.

가장 큰 허들인 SMS 발송 부분은, 서류 제출 없이 5분 만에 시작할 수 있는 EasyAuth 같은 서비스를 활용하면 프로젝트 셋업부터 SMS 인증 완성까지 30분이면 충분합니다.

빠르게 만들고, 빠르게 검증하세요. 🚀

비트베이크에서 광고를 시작해보세요

광고 문의하기

다른 글 보기

2026-04-08T11:02:47.515Z

2026 Professionals Solo Party & Wine Mixer Complete Guide: Real Reviews and Success Tips for Korean Singles

2026-04-08T11:02:47.487Z

2026년 직장인 솔로파티 & 와인모임 소개팅 완벽 가이드 - 실제 후기와 성공 팁

2026-04-08T10:03:28.247Z

Complete Google NotebookLM Guide 2026: Master the New Studio Features, Video Overviews, and Gemini Canvas Integration

2026-04-08T10:03:28.231Z

2026년 구글 NotebookLM 완벽 가이드: 새로운 스튜디오 기능, 비디오 개요 및 제미나이 캔버스 통합 실전 활용법

서비스

피드자주 묻는 질문고객센터

문의

비트베이크

레임스튜디오 | 사업자 등록번호 : 542-40-01042

경기도 남양주시 와부읍 수례로 116번길 16, 4층 402-제이270호

트위터인스타그램네이버 블로그