Better Auth로 TypeScript 인증 시스템 구축하기
웹 애플리케이션을 만들 때 인증은 거의 빠지지 않는 기능인데요. 직접 구현하자니 보안 허점이 걱정되고 기존 라이브러리를 쓰자니 특정 프레임워크에 묶이거나 설정이 복잡한 경우가 많습니다. Better Auth는 이런 고민에서 출발한 TypeScript 네이티브 인증 라이브러리입니다.
프레임워크를 가리지 않고 플러그인으로 기능을 확장하고 데이터베이스 스키마까지 직접 관리해 주는 게 특징인데요. 이 글에서는 Better Auth의 설정부터 이메일/비밀번호 인증, 소셜 로그인, 플러그인 활용까지 단계별로 살펴보겠습니다.
Better Auth란
Better Auth는 TypeScript로 작성된 프레임워크 독립적인 인증·인가 라이브러리입니다. Next.js, Nuxt, SvelteKit, Hono, Express 등 어떤 프레임워크에서든 동일한 API로 인증을 처리할 수 있습니다.
기존 인증 솔루션과 비교했을 때 눈에 띄는 점을 몇 가지 꼽자면, 우선 데이터베이스를 직접 관리한다는 겁니다. Better Auth는 사용자, 세션, 계정 테이블의 스키마를 직접 정의하고 마이그레이션해 주기 때문에 인증 데이터가 어디에 어떤 구조로 저장되는지 명확히 파악할 수 있어요. TypeScript 타입 추론이 서버에서 클라이언트까지 자연스럽게 이어지는 것도 장점입니다. 세션 객체나 사용자 정보에 자동완성이 잘 동작하거든요. 플러그인 시스템도 있어서 2단계 인증이나 조직 관리, 관리자 기능 같은 것을 필요한 만큼만 얹을 수 있습니다.
설치
Better Auth의 패키지를 설치합니다.
bun add better-auth
npm install better-auth
서버 설정
프로젝트 루트에 auth.ts 파일을 만들고 betterAuth 함수로 인증 인스턴스를 생성합니다.
가장 기본적인 설정은 데이터베이스 연결 정보만 넘겨 주는 것입니다.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
});
Better Auth는 PostgreSQL, MySQL, SQLite를 직접 지원합니다. 연결 문자열만 넘기면 내부적으로 Kysely를 사용해서 테이블을 생성하고 쿼리를 실행해 줍니다.
Drizzle이나 Prisma 같은 ORM을 이미 쓰고 있다면 해당 어댑터를 연결할 수도 있습니다.
import { betterAuth } from "better-auth";
import { drizzleAdapter } from "better-auth/adapters/drizzle";
import { db } from "./db"; // Drizzle 인스턴스
export const auth = betterAuth({
database: drizzleAdapter(db, {
provider: "sqlite",
}),
});
이렇게 하면 Better Auth가 Drizzle 인스턴스를 통해 데이터베이스에 접근합니다.
API 핸들러 연결
Better Auth의 인증 인스턴스를 웹 프레임워크의 라우트에 연결해야 합니다.
auth.handler는 표준 Request를 받아 Response를 반환하는 웹 표준 핸들러라서 대부분의 프레임워크와 쉽게 연동됩니다.
Express에서는 이렇게 연결합니다.
import express from "express";
import { toNodeHandler } from "better-auth/node";
import { auth } from "./auth";
const app = express();
app.all("/api/auth/*", toNodeHandler(auth));
app.listen(3000);
Hono라면 더 간단합니다.
import { Hono } from "hono";
import { auth } from "./auth";
const app = new Hono();
app.on(["POST", "GET"], "/api/auth/**", (c) => auth.handler(c.req.raw));
export default app;
요점은 /api/auth/* 경로로 들어오는 모든 요청을 Better Auth 핸들러에 위임하는 겁니다.
로그인, 회원가입, 로그아웃, 콜백 등 인증에 필요한 엔드포인트가 이 경로 아래에 자동으로 생성됩니다.
이메일/비밀번호 인증
가장 기본적인 인증 방식인 이메일/비밀번호를 활성화해 봅시다.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
emailAndPassword: {
enabled: true,
},
});
emailAndPassword.enabled를 true로 설정하면 회원가입과 로그인 엔드포인트가 활성화됩니다.
이것만으로 사용자 등록과 로그인이 가능합니다. 비밀번호는 내부적으로 해싱되어 안전하게 저장되니 걱정 없습니다.
이메일 인증까지 요구하고 싶다면 설정을 조금 더 추가합니다.
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
emailAndPassword: {
enabled: true,
requireEmailVerification: true,
minPasswordLength: 10,
sendResetPassword: async ({ user, url, token }, request) => {
// 비밀번호 재설정 이메일 발송 로직
},
},
emailVerification: {
sendVerificationEmail: async ({ user, url, token }, request) => {
// 이메일 인증 메일 발송 로직
},
},
});
requireEmailVerification을 켜면 인증 메일을 확인하기 전까지 로그인이 제한됩니다.
minPasswordLength로 비밀번호 최소 길이를 지정할 수 있고 sendResetPassword와 sendVerificationEmail 콜백에서 실제 이메일을 보내는 로직을 구현하면 됩니다.
클라이언트 설정
서버 쪽 설정을 마쳤으니 이제 클라이언트를 살펴볼 차례입니다. Better Auth는 React, Vue, Svelte, Solid 등 주요 프론트엔드 프레임워크에 맞는 클라이언트를 제공합니다.
React 프로젝트에서는 이렇게 설정합니다.
import { createAuthClient } from "better-auth/react";
export const authClient = createAuthClient({
baseURL: "http://localhost:3000",
});
baseURL은 Better Auth 서버가 실행되는 주소입니다.
서버와 클라이언트가 같은 도메인에서 동작한다면 생략할 수 있습니다.
이렇게 만든 authClient로 회원가입과 로그인을 호출할 수 있습니다.
import { authClient } from "./lib/auth-client";
function SignUp() {
const handleSignUp = async () => {
const result = await authClient.signUp.email({
email: "user@example.com",
password: "secure-password",
name: "홍길동",
});
};
return <button onClick={handleSignUp}>회원가입</button>;
}
import { authClient } from "./lib/auth-client";
function SignIn() {
const handleSignIn = async () => {
const result = await authClient.signIn.email({
email: "user@example.com",
password: "secure-password",
});
};
return <button onClick={handleSignIn}>로그인</button>;
}
signUp.email로 사용자를 등록하고 signIn.email로 로그인합니다.
API 호출 방식이지만 fetch를 직접 다루지 않아도 되고 응답 타입도 자동으로 추론돼서 편합니다.
세션 확인
로그인한 사용자의 세션 정보를 가져오려면 useSession 훅을 사용합니다.
import { authClient } from "./lib/auth-client";
function Profile() {
const { data: session, isPending } = authClient.useSession();
if (isPending) return <p>로딩 중...</p>;
if (!session) return <p>로그인이 필요합니다.</p>;
return (
<div>
<p>안녕하세요, {session.user.name}님!</p>
<p>{session.user.email}</p>
<button onClick={() => authClient.signOut()}>로그아웃</button>
</div>
);
}
useSession은 현재 세션 상태를 실시간으로 반환하는 React 훅입니다.
data에 사용자 정보와 세션 정보가 담겨 있고 isPending으로 로딩 상태를 처리할 수 있습니다.
세션은 기본적으로 데이터베이스에 저장되고 쿠키로 클라이언트와 서버 간 세션을 유지합니다.
세션 만료 시간이나 갱신 주기를 조절하고 싶다면 서버 설정에서 session 옵션을 사용합니다.
export const auth = betterAuth({
// ...기존 설정
session: {
expiresIn: 604800, // 7일 (초 단위)
updateAge: 86400, // 1일마다 세션 갱신
cookieCache: {
enabled: true,
maxAge: 300, // 쿠키 캐시 5분
},
},
});
cookieCache를 활성화하면 매 요청마다 데이터베이스를 조회하는 대신 쿠키에 캐싱된 세션 정보를 사용해서 응답 속도를 높일 수 있습니다.
소셜 로그인
Better Auth는 구글, 깃허브를 비롯해 여러 OAuth 제공자를 지원합니다.
socialProviders 옵션에 원하는 제공자의 클라이언트 ID와 시크릿을 추가하면 됩니다.
import { betterAuth } from "better-auth";
export const auth = betterAuth({
database: {
provider: "sqlite",
url: "./db.sqlite",
},
emailAndPassword: {
enabled: true,
},
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
},
github: {
clientId: process.env.GITHUB_CLIENT_ID!,
clientSecret: process.env.GITHUB_CLIENT_SECRET!,
},
},
});
클라이언트에서는 signIn.social을 호출하면 그 제공자의 인증 페이지로 리다이렉트됩니다.
const handleGoogleLogin = async () => {
await authClient.signIn.social({
provider: "google",
});
};
const handleGithubLogin = async () => {
await authClient.signIn.social({
provider: "github",
});
};
구글이나 깃허브에서 인증이 완료되면 Better Auth가 콜백을 처리하고 사용자 정보를 데이터베이스에 저장한 뒤 세션을 생성합니다. 이미 가입된 이메일로 소셜 로그인을 시도하면 기존 계정에 자동으로 연결됩니다.
제공자별로 추가 스코프를 요청하거나 프로필 정보를 사용자 데이터에 매핑할 수도 있습니다.
socialProviders: {
google: {
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
scope: ["https://www.googleapis.com/auth/calendar.readonly"],
mapProfileToUser: (profile) => ({
name: profile.name,
image: profile.picture,
}),
},
},
scope로 추가 권한을 요청하고 mapProfileToUser로 제공자 프로필에서 어떤 필드를 사용자 데이터로 저장할지 지정할 수 있습니다.
플러그인 시스템
Better Auth에서 눈여겨볼 부분이 플러그인 시스템입니다. 코어는 가볍게 유지하면서 필요한 기능만 플러그인으로 추가하는 구조인데요.
2단계 인증을 예로 들어 봅시다.
import { betterAuth } from "better-auth";
import { twoFactor } from "better-auth/plugins";
export const auth = betterAuth({
// ...기존 설정
plugins: [twoFactor()],
});
서버에 플러그인을 추가하면 관련 API 엔드포인트가 자동으로 생성됩니다. 클라이언트에서도 대응하는 플러그인을 추가해야 타입 추론과 메서드가 활성화됩니다.
import { createAuthClient } from "better-auth/react";
import { twoFactorClient } from "better-auth/client/plugins";
export const authClient = createAuthClient({
plugins: [twoFactorClient()],
});
이렇게 하면 authClient.twoFactor라는 네임스페이스가 생기고 TOTP 등록이나 인증 코드 검증 같은 메서드를 쓸 수 있습니다.
Better Auth에서 기본 제공하는 플러그인이 꽤 많습니다.
username— 이메일 대신 사용자명으로 로그인twoFactor— TOTP 기반 2단계 인증admin— 사용자 관리, 역할 기반 접근 제어organization— 조직·팀 단위 다중 테넌시magicLink— 이메일 링크로 비밀번호 없이 로그인passkey— 생체 인증이나 보안 키를 이용한 패스키 로그인
각 플러그인은 필요한 데이터베이스 테이블도 알아서 추가하기 때문에 별도로 마이그레이션을 작성할 필요가 없습니다.
데이터베이스 마이그레이션
Better Auth는 설정에 따라 필요한 테이블을 자동으로 관리합니다. CLI로 마이그레이션을 실행할 수 있습니다.
bunx @better-auth/cli migrate
npx @better-auth/cli migrate
이 명령을 실행하면 auth.ts에서 설정한 데이터베이스와 플러그인 정보를 읽어서 필요한 테이블(user, session, account 등)을 생성하거나 업데이트합니다.
어떤 테이블이 생성되는지 미리 확인하고 싶다면 generate 명령을 사용합니다.
bunx @better-auth/cli generate
이 명령은 실제로 데이터베이스를 건드리지 않고 SQL 마이그레이션 파일만 생성합니다. Drizzle이나 Prisma와 함께 쓸 때는 이 SQL을 ORM의 마이그레이션 흐름에 통합할 수 있습니다.
마치며
Better Auth는 TypeScript 생태계에서 인증 시스템을 구축할 때 고려할 만한 라이브러리입니다. 프레임워크에 종속되지 않으면서도 타입 안전성이 뛰어나고 플러그인 시스템 덕분에 프로젝트 규모에 맞춰 기능을 조절할 수 있습니다.
이 글에서는 이메일/비밀번호 인증부터 소셜 로그인, 세션 관리, 플러그인 활용까지 살펴보았는데요. OAuth 2.0의 동작 원리나 JWT의 구조를 먼저 이해해 두면 Better Auth의 내부 동작을 파악하는 데 도움이 됩니다. 반대로 직접 구현보다 관리형 인증 서비스를 검토하고 싶다면 Auth0로 빠르게 로그인 기능 붙이기나 WorkOS로 엔터프라이즈 SSO 붙이기와 비교해 보세요.
더 자세한 내용은 Better Auth 공식 문서를 참고하세요.
This work is licensed under
CC BY 4.0