Auth.js 설정
Auth.js의 공급자·세션 구조를 이해하고 App Router에서 인증 핸들러와 서버 API를 설정합니다.
웹 애플리케이션의 인증(authentication)은 사용자가 누구인지 확인하는 과정이고, 권한 부여(authorization)는 확인된 사용자가 어떤 기능을 쓸 수 있는지 판단하는 과정입니다. Next.js에서 Auth.js를 쓰면 OAuth 로그인, 세션 쿠키, 콜백, 라우트 핸들러를 직접 조립하는 부담을 줄일 수 있습니다.
10장에서는 Auth.js를 설정하고, 로그인과 로그아웃을 만들고, 보호된 라우트와 역할 기반 접근 제어까지 차례로 구현합니다. 이 장은 App Router와 Auth.js v5의 Next.js 통합 API를 기준으로 하며, 설치 예제의 beta 배포 태그와 실제로 설치된 버전은 프로젝트의 잠금 파일로 함께 관리합니다. 이 글의 핵심은 설정 객체를 여러 파일에서 다시 꺼내 쓰지 않고, 한 번의 NextAuth() 호출로 handlers, auth, signIn, signOut을 만들어 함께 쓰는 것입니다.
인증 흐름 이해
사용자가 GitHub 로그인을 고르면 브라우저는 GitHub의 인증 화면으로 이동합니다. 인증이 끝나면 GitHub가 애플리케이션의 콜백 URL로 사용자를 돌려보내고, Auth.js는 그 콜백 요청을 검증해 세션 쿠키를 만든 뒤 애플리케이션으로 이동시킵니다. 이렇게 외부 서비스의 계정으로 로그인하는 OAuth 흐름은 NestJS 교재의 OAuth 2.0과 OIDC 소셜 로그인에서 단계별로 다룹니다. 이후 서버 컴포넌트와 Route Handler는 auth()로 현재 세션을 읽으며, 클라이언트 컴포넌트는 꼭 필요한 경우에만 useSession()을 씁니다.
패키지와 환경 변수 설정
먼저 Auth.js 패키지를 설치합니다. Next.js에서 쓰는 Auth.js 패키지의 이름은 v5에서도 next-auth이고, v5는 beta 태그로 배포되므로 태그를 붙여 설치합니다. 태그 없이 설치하면 v5가 아닌 버전이 들어올 수 있으므로, 설치한 뒤 잠금 파일의 버전을 확인해 둡니다.
npm install next-auth@betaAuth.js v5가 안정 버전으로 설치되는 시점이 오면, 프로젝트의 버전 정책에 맞춰 next-auth를 설치하면 됩니다. 다음으로 프로젝트 루트의 .env.local에 인증 비밀 키와 GitHub OAuth 값을 저장합니다.
AUTH_SECRET=충분히_긴_무작위_문자열
AUTH_GITHUB_ID=GitHub_OAuth_App의_Client_ID
AUTH_GITHUB_SECRET=GitHub_OAuth_App의_Client_SecretAUTH_SECRET은 Auth.js가 세션 토큰을 암호화하는 데 쓰는 값이므로, 사람이 지어낸 짧은 문자열 대신 무작위 값을 넣어야 합니다. 다음 명령은 무작위 비밀 키를 만들어 .env.local에 AUTH_SECRET으로 추가합니다.
npx auth secret명령이 키를 파일에 직접 써 주므로, 위 예시의 AUTH_SECRET 줄은 손으로 채우지 않아도 됩니다. 남은 두 값은 GitHub에서 OAuth App을 만들 때 받습니다. 이때 Authorization callback URL 칸에 개발 환경의 다음 주소를 적어야, GitHub 로그인을 마친 사용자가 Auth.js의 콜백 처리 경로로 돌아옵니다. 주소의 /api/auth는 뒤에서 만들 catch-all 라우트의 위치이고, 마지막 github는 공급자 이름입니다.
http://localhost:3000/api/auth/callback/github배포 환경에서는 실제 HTTPS 도메인으로 콜백 URL을 따로 등록합니다. 환경 변수 파일은 저장소에 커밋하지 않습니다.
중앙 인증 모듈 작성
프로젝트의 인증 설정은 src/auth.ts 한곳에 둡니다. NextAuth()에 설정 객체를 넘기면 그 설정이 적용된 handlers, auth, signIn, signOut이 돌아오고, 아래 코드는 이를 구조 분해해 바로 내보냅니다. session.strategy: 'jwt'는 세션을 데이터베이스가 아니라 암호화된 토큰에 담아 쿠키로 주고받는다는 뜻이므로, 이 장의 예제는 데이터베이스 없이도 동작합니다.
import NextAuth from 'next-auth';
import GitHub from 'next-auth/providers/github';
export const { handlers, auth, signIn, signOut } = NextAuth({
providers: [GitHub],
session: {
strategy: 'jwt',
},
});GitHub 공급자(provider)는 AUTH_GITHUB_ID와 AUTH_GITHUB_SECRET을 자동으로 읽습니다.
NEXT · 10.1 · Auth.js 설정
인증 설정은 한곳에, 서버 API는 세 사용처에 연결합니다
src/auth.ts의 NextAuth()가 handlers, auth, signIn, signOut을 만듭니다. 화살표는 서버 코드의 API 사용 연결이며, 로그인 요청의 시간 순서가 아닙니다.
@/auth는 서버에서 사용합니다. 브라우저 UI는 next-auth/react를 사용하며 필요한 경우 SessionProvider를 둡니다. Provider를 추가해도 children으로 전달한 서버 컴포넌트 전체가 Client로 바뀌지는 않습니다.
위 그림은 중앙 인증 모듈이 만든 API가 어디에서 쓰이는지 보여 줍니다. src/auth.ts의 NextAuth()가 handlers, auth, signIn과 signOut이라는 세 묶음의 서버 API를 만들고, 각 묶음은 서로 독립된 세 위치로 연결됩니다. handlers는 api/auth/[...nextauth]의 route.ts에서 GET과 POST로, auth는 서버 컴포넌트에서 세션을 읽는 데, signIn과 signOut은 서버 액션에서 로그인과 로그아웃을 요청하는 데 쓰입니다. 그림의 화살표는 서버 코드에서 API를 쓰는 연결이며, 로그인 요청의 시간 순서가 아닙니다.
그림의 아래 설명처럼 @/auth는 서버에서만 씁니다. 브라우저 UI는 next-auth/react를 쓰고 필요할 때 SessionProvider를 두며, Provider를 추가해도 children으로 넘긴 서버 컴포넌트 전체가 클라이언트 컴포넌트로 바뀌지는 않습니다.
더 깊이: JWT 세션 쿠키와 데이터베이스 세션의 차이
Auth.js의 세션 전략 안내에 따르면 데이터베이스 어댑터를 설정하지 않은 Auth.js의 기본 세션 전략은 JWT입니다. 이 방식에서는 세션 정보를 담은 JWT를 서버만 아는 비밀 키로 암호화해 HttpOnly 쿠키에 저장하므로, 브라우저의 JavaScript가 그 쿠키를 직접 읽을 수 없습니다. 이 장의 예제가 session.strategy: 'jwt'를 적고 데이터베이스 없이 동작하는 것도 이 때문입니다. JWT의 구조는 NestJS 교재의 JWT 기반 인증에서 다루며, 그 글의 서명만 한 JWT와 달리 Auth.js는 내용까지 암호화합니다.
쿠키에는 크기 한도가 있다는 점도 기억해 둡니다. 브라우저마다 조금씩 다르지만 쿠키 하나는 대략 4096바이트까지 담을 수 있고, Auth.js는 세션 쿠키가 이 한도를 넘으면 여러 쿠키로 나누어 저장합니다. 다만 이 쿠키는 요청마다 함께 전송되므로, 다음 글들에서 역할 같은 값을 토큰에 더할 때도 꼭 필요한 값만 담는 편이 좋습니다.
데이터베이스 세션 전략에서는 쿠키에 데이터베이스의 세션을 가리키는 식별자만 들어가고, 세션을 읽을 때마다 데이터베이스에서 사용자 정보를 조회합니다. 서버에서 언제든 세션을 바꾸거나 지울 수 있으므로 모든 기기에서 로그아웃시키거나 동시 로그인 수를 제한하기 쉽습니다. 대신 세션을 확인할 때마다 데이터베이스를 한 번 더 오가야 하고, 어댑터와 저장소를 따로 준비해야 합니다.
App Router 핸들러 연결
Auth.js가 만든 HTTP 핸들러를 catch-all 라우트에 연결합니다. 폴더 이름 [...nextauth]는 /api/auth 아래의 모든 하위 경로를 이 파일 하나로 받는다는 뜻입니다. 그래서 /api/auth/signin이나 앞에서 GitHub에 등록한 /api/auth/callback/github 요청이 모두 이곳으로 들어오고, handlers 안의 GET, POST 함수를 같은 이름으로 내보내면 그대로 Route Handler가 됩니다.
import { handlers } from '@/auth';
export const { GET, POST } = handlers;이 파일은 설정을 직접 가지지 않으며, URL 요청을 중앙 인증 모듈의 핸들러로 넘기는 역할만 합니다.
서버에서 세션 읽기
서버 컴포넌트에서는 auth()를 직접 호출합니다. auth()는 요청에 실린 세션 쿠키를 읽어 세션 객체를 돌려주고, 로그인하지 않은 요청이면 null을 돌려줍니다. 비동기 함수라서 await로 기다려야 하므로 페이지 컴포넌트도 async로 선언하며, 아래 페이지는 session?.user가 있는지에 따라 안내 문구와 이메일 가운데 하나를 보여 줍니다.
import { auth } from '@/auth';
export default async function AccountPage() {
const session = await auth();
if (!session?.user) {
return <p>로그인이 필요합니다.</p>;
}
return <p>{session.user.email} 계정으로 로그인했습니다.</p>;
}@/auth는 서버 코드에서만 가져옵니다. 브라우저 UI에는 next-auth/react의 클라이언트 API를 쓰며, 공급자의 비밀 키나 서버 인증 설정을 클라이언트 컴포넌트로 가져오지 않습니다. 로그인 여부만 확인할 때는 session?.user가 있는지 검사하고, 사용자 식별자나 역할이 필요하다면 뒤에서 콜백으로 세션 타입을 확장합니다.
클라이언트 세션이 필요한 경우
대부분의 페이지는 서버 컴포넌트에서 세션을 읽는 편이 단순합니다. 브라우저에서 세션 변화에 반응해야 하는 작은 UI에만 SessionProvider와 useSession()을 씁니다. 먼저 SessionProvider를 감싼 클라이언트 컴포넌트를 만듭니다.
'use client';
import { SessionProvider } from 'next-auth/react';
export function Providers({ children }: { children: React.ReactNode }) {
return <SessionProvider>{children}</SessionProvider>;
}SessionProvider는 React Context로 세션을 아래 컴포넌트에 내려 주는데, Context Provider는 클라이언트에서만 동작합니다. 그래서 파일 맨 위에 'use client'를 두고, 서버 컴포넌트인 루트 레이아웃은 이 Providers를 가져와 children을 감쌉니다. 이렇게 감싼 뒤에야 그 아래의 클라이언트 컴포넌트가 useSession()으로 세션을 읽을 수 있습니다.
import './globals.css';
import { Providers } from './providers';
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="ko">
<body>
<Providers>{children}</Providers>
</body>
</html>
);
}Provider를 추가한다고 루트 전체가 클라이언트 컴포넌트로 바뀌지는 않습니다. 다만 클라이언트 세션이 전혀 필요하지 않다면 Provider도 추가하지 않습니다.
설정 확인
먼저 src/auth.ts와 src/app/api/auth/[...nextauth]/route.ts를 연결한 뒤, 서버 페이지의 auth() 결과로 로그인 전후를 비교합니다. 브라우저 버튼을 만들기 전에도 기본 페이지인 /api/auth/signin으로 공급자 인증을 시작할 수 있습니다.
개발 서버를 실행한 뒤 /api/auth/signin에 접속해 GitHub 로그인을 마치고 애플리케이션으로 돌아오는지 확인합니다. 그다음 auth()를 호출한 서버 컴포넌트에서 사용자 정보가 보이는지 확인합니다. 환경 변수를 바꿨다면 터미널에 Reload env: .env.local이 나와 새 값을 다시 읽었는지 확인하고, 이 줄이 나오지 않으면 개발 서버를 다시 시작합니다. 로그인에 실패하면 공급자 키, 콜백 URL, AUTH_SECRET 순서로 점검합니다.
인증 모듈이 준비되었다는 것과 특정 업무 API의 권한 검사가 구현되었다는 것은 별개입니다. Auth.js 설치 안내도 핸들러를 연결한 뒤의 리소스 보호를 따로 설명합니다.
정리
- 인증은 사용자가 누구인지 확인하고, 권한 부여는 확인된 사용자가 무엇을 할 수 있는지 판단합니다.
src/auth.ts에서NextAuth()를 한 번 호출해handlers,auth,signIn,signOut을 만들고, 다른 파일은 이를 가져와 씁니다.api/auth/[...nextauth]/route.ts는handlers의 GET과 POST를 내보내 요청을 중앙 인증 모듈로 넘깁니다.- 서버 컴포넌트는
auth()로 세션을 읽고, 브라우저에서 세션 변화에 반응해야 하는 UI에만SessionProvider와useSession()을 씁니다. AUTH_SECRET과 공급자 키는.env.local에 두고 커밋하지 않으며, 배포 환경의 콜백 URL은 따로 등록합니다.
연습 문제
1. 서버 조회와 브라우저 세션 구독
AccountPage만 쓰는 앱에서 Providers를 없앴습니다. 이것만으로 서버의 auth() 호출이 불가능해질까요? 반대로 클라이언트 버튼 컴포넌트에 useSession()을 추가할 때 필요한 연결은 무엇일까요? 공급자 설정, 핸들러, 세션 쿠키는 정상이라고 가정합니다.
해설 보기
불가능해지지 않습니다. 서버 페이지는 중앙 인증 모듈의 auth()로 요청의 세션을 읽으므로, React의 SessionProvider가 있는지는 그 호출의 조건이 아닙니다. 반면 useSession()을 쓰는 브라우저 UI에는 상위에 SessionProvider가 있어야 합니다. 따라서 서버의 세션 조회 실패를 Provider 추가만으로 해결하려 하지 말고, 서버 설정과 요청 쿠키의 문제, 브라우저 트리의 문제를 각각 점검합니다.
2. 두 번째 공급자 추가하기
GitHub 로그인에 더해 Google 로그인도 지원하려고 합니다. route.ts, src/auth.ts, .env.local 가운데 어느 파일을 고쳐야 할까요?
해설 보기
src/auth.ts의 providers 배열에 Google 공급자를 추가하고, .env.local에 Google OAuth의 클라이언트 ID와 비밀 값을 더합니다. Google 쪽에도 /api/auth/callback/google 콜백 URL을 등록해야 합니다.
route.ts는 고칠 필요가 없습니다. 이 파일은 중앙 인증 모듈의 handlers를 그대로 넘길 뿐이므로, 공급자가 늘어나도 같은 catch-all 라우트가 요청을 받습니다.
3. 배포한 뒤 로그인에 실패할 때
개발 환경에서는 GitHub 로그인이 잘 되었는데, https://example.com에 배포한 뒤에는 GitHub에서 콜백 URL이 맞지 않는다는 오류가 납니다. 무엇을 먼저 확인해야 할까요?
해설 보기
GitHub OAuth App에 배포 도메인의 콜백 URL인 https://example.com/api/auth/callback/github가 등록되어 있는지 먼저 확인합니다. 개발용으로 등록한 http://localhost:3000/api/auth/callback/github만으로는 배포 환경의 콜백을 받아들이지 않습니다.
그다음 배포 환경에도 AUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET이 설정되어 있는지 확인합니다. .env.local은 커밋하지 않으므로, 배포 플랫폼의 환경 변수 설정에 따로 넣어야 합니다.
다음 글 로그인과 로그아웃에서는 서버 액션으로 signIn과 signOut을 호출하는 로그인, 로그아웃 버튼을 만듭니다.