비트베이크

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

2026.03.24

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 테이블에 phoneNumber과 phoneNumberVerified 필드를 자동 추가합니다.


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.08.09

중개사 블로그 저품질 탈출! 네이버 검색 누락 막는 관리 노하우

중개사 블로그 운영 중 겪을 수 있는 저품질과 네이버 검색 누락은 심각한 영업 기회 손실로 이어집니다. 본 글에서는 저품질의 의미와 진단법을 소개하고, 핵심 관리 전략 5가지와 이미 저품질에 걸렸을 때의 정상화 단계를 상세히 안내합니다. 중개사님 블로그의 안정적인 성장을 위한 실질적인 관리 노하우를 지금 바로 확인해보세요.

2026.08.08

손님 부르는 중개사 블로그, 상위노출 키워드 전략: 우리 사무소로 고객을 이끄는 비법!

부동산 중개사님을 위한 블로그 상위노출 키워드 전략! 우리 동네 잠재 고객을 사로잡고, 매물 정보에 키워드를 자연스럽게 녹이는 노하우를 공개합니다. 손님 유입을 늘리고 사무소 경쟁력을 높이는 중개사 마케팅 비법을 지금 바로 확인하세요.

2026.08.08

변동성 시장, 중개사가 고객 신뢰 얻는 지역 동향 브리핑 노하우

변동성 시장에서 부동산 중개사가 고객 신뢰를 얻는 핵심은 심도 있는 지역 시장 분석과 맞춤형 브리핑입니다. 공신력 있는 데이터를 활용하여 지역 동향을 해석하고, 고객의 눈높이에 맞춰 복잡한 정보를 명확하게 전달하는 노하우를 통해 중개사는 경쟁력을 강화하고 시장 변동성 속 기회를 창출할 수 있습니다.

2026.08.06

2026 GTX 개통 임박! A/B/C 노선 수혜지역 투자 가이드

2026년 GTX A/B/C 노선 개통이 임박하며 수도권 부동산 시장이 들썩이고 있습니다. GTX 노선별 개통 현황과 함께, 주요 수혜지역을 심층 분석하고 실거주 및 투자를 위한 현명한 전략과 유의점을 제시하여 성공적인 아파트 투자를 돕는 가이드입니다.

서비스

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

문의

비트베이크

주식회사 컴파운딩 | 사업자 등록번호 : 647-86-03697

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

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