사용자 생성(회원가입)에 이어, 이제 실제 인증 기능(로그인)을 구현할 차례입니다. 사용자를 로그인시키고, 로그인된 사용자를 위한 토큰(권한)을 발급받아야 합니다. 또한, 사용자가 로그인했는지 여부를 클라이언트 사이드(UI 변경, 특정 라우트 접근 제한)와 서버 사이드(API 라우트) 양쪽에서 확인할 수 있어야 합니다.
이 과정에서 이전에 설치한 NextAuth 패키지가 핵심적인 역할을 합니다. NextAuth는 사용자 인증 과정을 돕고, 토큰 생성 및 저장을 백그라운드에서 관리하여 사용자가 권한을 가졌는지 쉽게 확인할 수 있게 해줍니다.
[...nextauth].js API 라우트 설정
NextAuth를 사용하기 위해 먼저 새로운 API 라우트를 추가해야 합니다. 로그인은 결국 사용자의 정보를 받아 데이터베이스에서 일치하는 사용자가 있는지, 비밀번호가 정확한지 확인하는 요청을 처리하는 API 라우트가 필요하기 때문입니다.
api/auth 폴더 내에 새 파일을 생성하되, 이 파일은 특별한 이름인 [...nextauth].js로 명명해야 합니다.
- 대괄호 []: 이 파일이 동적 API 라우트임을 의미합니다.
- 줄임표 ...: 이것이 'catch-all' 라우트임을 의미합니다.
이 파일은 api/auth/로 시작하는 모든 하위 경로의 요청을 잡아채는(catch-all) 동적 API 라우트입니다. 이 catch-all 라우트가 필요한 이유는, NextAuth 패키지가 내부적으로 사용자 로그인, 로그아웃 등을 처리하기 위해 여러 개의 라우트를 자체적으로 생성하고 노출하기 때문입니다. 이 파일을 통해 api/auth로 시작하는 모든 특수 요청이 자동으로 NextAuth 패키지에 의해 처리되도록 위임하는 것입니다.
물론, 이전에 만든 api/auth/signup처럼 우리만의 커스텀 라우트를 추가로 정의할 수도 있습니다. 단, NextAuth가 내장한 라우트 경로와 겹치지 않도록 주의해야 합니다.
공식 문서: NextAuth의 내장 라우트
NextAuth가 내부적으로 어떤 라우트를 생성하고 처리하는지 궁금하다면, 공식 NextAuth 문서를 확인해볼 수 있습니다.
https://next-auth.js.org/getting-started/rest-api
REST API | NextAuth.js
NextAuth.js exposes a REST API that is used by the NextAuth.js client.
next-auth.js.org
공식 문서의 'REST API' 섹션을 살펴보면, NextAuth가 처리하는 모든 내장 라우트 목록이 나와있습니다. 이 라우트들은 모두 /api/auth로 시작하므로, 우리가 직접 API 라우트를 만들 때 이 경로들과 충돌하지 않도록 해야 합니다.
NextAuth 함수 상세 분석
이제 [...nextauth].js 파일의 내용을 작성해 보겠습니다. 먼저 next-auth에서 NextAuth를 임포트합니다.
v3 / v4
import NextAuth from 'next-auth';
그리고 이 NextAuth를 export default 합니다. 하지만 단순히 객체를 내보내는 것이 아니라, NextAuth()처럼 함수로 실행하여 내보내야 합니다.
v3
export default NextAuth({
// ... 설정 객체 ...
});
v4 - (App 라우터 사용 시) App 라우터에서는 route.js 파일에서 GET과 POST 핸들러를 export 해야 합니다.
// app/api/auth/[...nextauth]/route.js
const handler = NextAuth({
// ... 설정 객체 ...
});
export { handler as GET, handler as POST };
NextAuth는 함수이며, 이 함수를 호출하면 API 라우트에 필요한 실제 핸들러(handler) 함수를 반환합니다. API 라우트는 항상 함수를 export 해야 하므로, NextAuth를 호출하여 그 결과로 반환된 핸들러 함수를 export 하는 것입니다.
NextAuth() 함수를 호출할 때, 우리는 NextAuth의 동작을 구성하는 **설정 객체(configuration object)**를 인자로 전달합니다. (모든 설정 옵션은 공식 문서의 'Configuration Options'에서 자세히 확인할 수 있습니다.)
https://next-auth.js.org/configuration/options
Options | NextAuth.js
Environment Variables
next-auth.js.org
NextAuth 설정 객체: providers
우리가 전달할 설정 객체에서 중요한 옵션 중 하나는 providers입니다. 이는 인증 방식을 정의하는 배열입니다.
v3
import Providers from 'next-auth/providers';
export default NextAuth({
// ...
providers: [
Providers.Credentials({
// ... Credentials 프로바이더 설정 ...
})
],
});
v4
import CredentialsProvider from 'next-auth/providers/credentials';
export default NextAuth({ // 또는 App 라우터 방식
// ...
providers: [
CredentialsProvider({
// ... Credentials 프로바이더 설정 ...
})
],
});
next-auth/providers에서 Providers를 임포트한 뒤, providers 배열 안에 사용할 프로바이더를 추가합니다. 여기서는 Providers.Credentials()를 사용합니다. 이는 구글, 페이스북 같은 소셜 로그인이 아니라, 우리가 직접 이메일과 비밀번호를 검증하는 방식(자체 자격증명)을 사용하겠다는 의미입니다.
Credentials 프로바이더는 자체적인 설정 객체를 가집니다. 만약 credentials 키를 설정하면 NextAuth가 자동으로 로그인 폼을 생성해주기도 하지만, 우리는 이미 직접 만든 로그인 폼을 사용할 것이므로 이 옵션은 사용하지 않습니다. 대신, 우리가 반드시 설정해야 하는 것은 authorize 메소드입니다.
authorize 메소드: 핵심 인증 로직
authorize는 NextAuth가 로그인 요청을 받았을 때 호출되는 비동기(async) 함수입니다. 이 함수는 클라이언트로부터 제출된 credentials(이메일, 비밀번호 등이 담긴 객체)를 인자로 받습니다. 이 함수 내부에서, 우리는 우리의 자체 인증 로직을 구현해야 합니다.
v3
// ...
providers: [
Providers.Credentials({
async authorize(credentials) {
// 1. 데이터베이스 연결
const client = await connectToDatabase();
// 2. 사용자 컬렉션 접근
const usersCollection = client.db().collection('users');
// 3. 이메일로 사용자 찾기
const user = await usersCollection.findOne({
email: credentials.email,
});
// 4. 사용자가 없는 경우
if (!user) {
client.close();
throw new Error('No user found!');
}
// 5. 비밀번호 검증
const isValid = await verifyPassword(
credentials.password,
user.password
);
// 6. 비밀번호가 틀린 경우
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}
// 7. 인증 성공
client.close();
return { email: user.email }; // JWT에 포함될 데이터 반환
},
}),
],
// ...
v4 - CredentialsProvider를 사용하고, authorize 함수는 req를 두 번째 인자로 받을 수 있습니다. 로직 자체는 동일하지만, 실패 시 null을 반환하는 것이 권장됩니다.
// ...
providers: [
CredentialsProvider({
// v4에서는 자동 폼 생성을 위해 name, credentials 키를 명시할 수 있습니다.
// name: 'Credentials',
// credentials: {
// email: { label: "Email", type: "text" },
// password: { label: "Password", type: "password" }
// },
async authorize(credentials, req) { // v4는 'req' 인자 사용 가능
// 1. 데이터베이스 연결
const client = await connectToDatabase();
// 2. 사용자 컬렉션 접근
const usersCollection = client.db().collection('users');
// 3. 이메일로 사용자 찾기
const user = await usersCollection.findOne({
email: credentials.email,
});
// 4. 사용자가 없는 경우
if (!user) {
client.close();
throw new Error('No user found!');
// 또는: return null; (v4에서는 null 반환 시 기본 에러 메시지 표시)
}
// 5. 비밀번호 검증
const isValid = await verifyPassword(
credentials.password,
user.password
);
// 6. 비밀번호가 틀린 경우
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
// 또는: return null;
}
// 7. 인증 성공
client.close();
return { email: user.email, id: user._id }; // user 객체(또는 일부) 반환
},
}),
],// ...
authorize 함수의 로직은 다음과 같습니다.
- connectToDatabase를 임포트하여 데이터베이스에 연결합니다.
- usersCollection에 접근합니다.
- credentials.email을 사용해 findOne으로 사용자를 찾습니다. (이때 credentials 객체에 email 프로퍼티가 있을 것으로 기대합니다. 왜냐하면 나중에 우리가 클라이언트 측에서 이 형식으로 데이터를 보낼 것이기 때문입니다.)
- 사용자 없음 처리: 만약 user가 없다면, client.close()로 DB 연결을 닫고 new Error('No user found!')를 throw 합니다. authorize 내에서 에러를 throw 하면 인증은 실패하며, 기본적으로 클라이언트는 다른 페이지로 리다이렉트됩니다.
- 비밀번호 검증: 사용자를 찾았다면, 이제 비밀번호를 검증해야 합니다. 데이터베이스에는 비밀번호가 해시되어 저장되어 있으므로, bcrypt.js의 compare 함수가 필요합니다. (이 로직은 lib/auth.js 등에 verifyPassword 함수로 분리하여 임포트합니다.)
- 비밀번호 불일치 처리: verifyPassword의 결과(isValid)가 false라면, client.close()를 호출하고 new Error('Could not log you in!')를 throw 합니다.
- 인증 성공: 위 두 검증을 모두 통과했다면 인증이 성공한 것입니다. client.close()를 호출한 뒤, 객체를 반환합니다.
- authorize 함수가 객체를 반환하면, NextAuth는 인증이 성공했다고 판단합니다.
- 중요: 이때 반환된 객체는 JSON Web Token (JWT)에 인코딩될 데이터입니다. 따라서 return { email: user.email };과 같이 클라이언트가 알아도 되는 최소한의 정보만 담아야 합니다. 절대 전체 user 객체나 비밀번호를 반환해서는 안 됩니다. NextAuth 설정 객체: session Credentials 프로바이더를 사용할 때, 실제로 JWT가 생성되고 사용되도록 NextAuth 설정 객체에 session 옵션을 추가해야 합니다.
NextAuth 설정 객체: session
Credentials 프로바이더를 사용할 때, 실제로 JWT가 생성되고 사용되도록 NextAuth 설정 객체에 session 옵션을 추가해야 합니다.
v3
export default NextAuth({
session: {
jwt: true,
},
providers: [
// ...
],
});
v4
export default NextAuth({ // 또는 App 라우터 방식
session: {
strategy: 'jwt',
},
providers: [
// ...
],
});
session: { jwt: true }로 설정하여, 인증된 사용자의 세션을 관리할 때 JSON Web Token을 사용하도록 명시합니다.
Credentials 인증 방식은 데이터베이스에 세션을 저장하는 방식이 아닌 JWT 사용이 필수입니다. (사실 데이터베이스 설정을 따로 하지 않으면 기본값이 true이긴 하지만, 명시적으로 설정하는 것이 좋습니다.) 만약 이메일 매직 링크 같은 다른 인증 프로바이더를 사용한다면 데이터베이스에 세션을 저장해야 할 수도 있지만, 여기서는 JWT를 사용합니다.
전체 코드 정리
최종적인 [...nextauth].js 파일의 코드는 다음과 같습니다.
v3
import NextAuth from 'next-auth';
import Providers from 'next-auth/providers';
import { verifyPassword } from '../../../lib/auth';
import { connectToDatabase } from '../../../lib/db';
export default NextAuth({
session: {
jwt: true, // 세션 관리 방식으로 JWT를 사용하도록 설정
},
providers: [
Providers.Credentials({
async authorize(credentials) {
// 데이터베이스 연결
const client = await connectToDatabase();
const usersCollection = client.db().collection('users');
// 이메일로 사용자 검색
const user = await usersCollection.findOne({
email: credentials.email,
});
// 사용자가 없으면 에러 발생
if (!user) {
client.close();
throw new Error('No user found!');
}
// 비밀번호 검증 (lib/auth.js의 verifyPassword 함수 사용)
const isValid = await verifyPassword(
credentials.password,
user.password
);
// 비밀번호가 틀리면 에러 발생
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}
// 인증 성공 시 DB 연결 닫기
client.close();
// JWT 토큰에 담을 정보 반환 (비밀번호 제외)
return { email: user.email };
},
}),
],
});
v4
import NextAuth from 'next-auth';
import CredentialsProvider from 'next-auth/providers/credentials';
import { verifyPassword } from '../../../lib/auth';
import { connectToDatabase } from '../../../lib/db';
export default NextAuth({
session: {
strategy: 'jwt', // v4: 세션 전략 명시
},
providers: [
CredentialsProvider({
name: 'Credentials',
credentials: {
email: { label: 'Email', type: 'text' },
password: { label: 'Password', type: 'password' },
type: { label: 'Type', type: 'text' },
},
// v4: CredentialsProvider 직접 임포트
async authorize(credentials, req) {
// 데이터베이스 연결
const client = await connectToDatabase();
const usersCollection = client.db().collection('users');
// 이메일로 사용자 검색
const user = await usersCollection.findOne({
email: credentials.email,
});
// 사용자가 없으면 에러 발생 (또는 null 반환)
if (!user) {
client.close();
throw new Error('No user found!');
}
// 비밀번호 검증 (lib/auth.js의 verifyPassword 함수 사용)
const isValid = await verifyPassword(
credentials.password,
user.password
);
// 비밀번호가 틀리면 에러 발생 (또는 null 반환)
if (!isValid) {
client.close();
throw new Error('Could not log you in!');
}
// 인증 성공 시 DB 연결 닫기
client.close();
// JWT 토큰에 담을 정보 반환 (비밀번호 제외)
// v4에서는 이 반환값이 JWT 콜백의 'user' 파라미터로 전달됩니다.
return { email: user.email, id: user._id };
},
}),
],
// v4에서는 JWT 콜백 등을 추가로 설정하여 세션에 id 등 추가 정보를 담을 수 있습니다.
// callbacks: {
// async jwt({ token, user }) {
// if (user) {
// token.id = user.id; // authorize에서 반환한 id를 토큰에 추가
// }
// return token;
// },
// async session({ session, token }) {
// session.user.id = token.id; // 세션 객체에 id 추가
// return session;
// },
// },
});
이 코드는 NextAuth의 핵심 설정, 특히 Credentials 프로바이더의 authorize 함수를 정의합니다. 이 함수는 DB에서 사용자를 찾아 비밀번호를 검증하는 실제 인증 로직을 수행하며, 성공 시 JWT에 포함될 사용자 정보를 반환합니다. 모든 에러 상황에서는 client.close()를 호출하여 DB 연결이 낭비되지 않도록 처리합니다. 이제 인증을 위한 백엔드 API 라우트가 준비되었습니다. 다음 단계는 프론트엔드(클라이언트)에서 로그인 폼을 통해 이 API 라우트로 요청을 보내는 것입니다.
'NextJS > NextAuth' 카테고리의 다른 글
| [NextAuth] 클라이언트 사이드 페이지 가드 (라우트 보호) 추가하기 - 6 (0) | 2025.11.18 |
|---|---|
| [NextAuth] 클라이언트 컴포넌트 인증 : 로그인, 로그아웃 -5 (0) | 2025.11.17 |
| [NextAuth] 회원가입 컴포넌트 및 API 연동 - 3 (0) | 2025.11.12 |
| [NextAuth] 회원가입 API 구현하기 - 2 (1) | 2025.11.10 |
| [NextAuth] React/NextJS 인증(Authentication)의 원리와 적용 - 1 (0) | 2025.11.06 |