비밀번호 변경 로직 추가
이전 단계에서 API 라우트 보호 코드를 작성하여 요청이 인증된 사용자로부터 왔으며, 올바른 메서드(PATCH)를 사용하고 있는지 확인했다. 이제 요청에서 데이터(기존 비밀번호, 새 비밀번호)를 추출하고, 세션에서 사용자 이메일을 가져와야 한다. 데이터베이스에는 사용자의 이메일과 비밀번호가 저장되어 있으므로, 사용자를 식별하기 위해 이메일이 필요하다.
사용자 이메일은 폼 데이터에 포함되어 있지 않지만, 토큰에 인코딩되어 있으므로 세션 객체(session.user.email)를 통해 쉽게 접근할 수 있다. 기존 비밀번호와 새 비밀번호는 req.body를 통해 추출한다.
이제 데이터베이스에 연결하여 다음 단계를 수행한다.
- 이메일로 사용자를 찾는다.
- 사용자를 찾으면 데이터베이스에 저장된 현재 비밀번호와 입력받은 기존 비밀번호가 일치하는지 확인한다.
- 일치한다면 새 비밀번호를 해시(Hash)화하여 기존 비밀번호를 덮어쓴다.
API 라우트 구현 (change-password.js)
먼저 데이터베이스 연결을 위해 connectToDatabase를 사용하고 users 컬렉션에 접근한다. findOne을 사용하여 세션의 이메일과 일치하는 사용자를 찾는다. 사용자가 없다면 로직상 이상한 상황(로그인된 상태인데 DB에 없음)이지만, 연결을 닫고 404 에러를 반환하여 처리한다.
사용자를 찾았다면 lib/auth.js에 있는 verifyPassword 함수를 사용하여 비밀번호를 검증한다. 입력된 oldPassword와 DB의 currentPassword를 비교한다. 비밀번호가 일치하지 않으면 403(Forbidden) 또는 422(Unprocessable Entity) 상태 코드를 반환하고 작업을 중단한다. 여기서는 권한 없음의 의미로 403을 사용한다.
비밀번호가 일치하면 hashPassword 함수로 새 비밀번호를 해시화한다. 그 후 updateOne 메서드와 $set 연산자를 사용하여 해당 사용자의 비밀번호 필드만 업데이트한다.
다음은 완성된 코드다.
Next Auth v3
// pages/api/user/change-password.js
import { getSession } from 'next-auth/client';
import { hashPassword, verifyPassword } from '../../../lib/auth';
import { connectToDatabase } from '../../../lib/db';
async function handler(req, res) {
// 1. 메서드 확인
if (req.method !== 'PATCH') {
return;
}
// 2. 세션 확인
const session = await getSession({ req: req });
if (!session) {
res.status(401).json({ message: 'Not authenticated!' });
return;
}
const userEmail = session.user.email;
const oldPassword = req.body.oldPassword;
const newPassword = req.body.newPassword;
const client = await connectToDatabase();
const usersCollection = client.db().collection('users');
// 3. 사용자 찾기
const user = await usersCollection.findOne({ email: userEmail });
if (!user) {
res.status(404).json({ message: 'User not found.' });
client.close();
return;
}
// 4. 기존 비밀번호 검증
const currentPassword = user.password;
const passwordsAreEqual = await verifyPassword(oldPassword, currentPassword);
if (!passwordsAreEqual) {
res.status(403).json({ message: 'Invalid password.' });
client.close();
return;
}
// 5. 비밀번호 업데이트
const hashedPassword = await hashPassword(newPassword);
const result = await usersCollection.updateOne(
{ email: userEmail },
{ $set: { password: hashedPassword } }
);
client.close();
res.status(200).json({ message: 'Password updated!' });
}
export default handler;
Next Auth v4
v4에서는 getSession 대신 getServerSession을 사용하며, authOptions를 함께 전달해야 한다.
// pages/api/user/change-password.js (v4)
import { getServerSession } from "next-auth/next";
import { authOptions } from "../../api/auth/[...nextauth]";
import { hashPassword, verifyPassword } from '../../../lib/auth';
import { connectToDatabase } from '../../../lib/db';
async function handler(req, res) {
if (req.method !== 'PATCH') {
res.status(405).json({ message: 'Method Not Allowed' });
return;
}
const session = await getServerSession(req, res, authOptions);
if (!session) {
res.status(401).json({ message: 'Not authenticated!' });
return;
}
// 이후 로직은 v3와 동일함
const userEmail = session.user.email;
// ... 데이터베이스 연결 및 비밀번호 검증/업데이트 로직 ...
}
export default handler;
프론트엔드에서 비밀번호 변경 요청 보내기
이제 백엔드와 프론트엔드를 연결해야 한다. UserProfile 컴포넌트 내부의 ProfileForm 컴포넌트에서 폼 제출을 처리하고 데이터를 추출하여 부모 컴포넌트로 전달한다.
ProfileForm 컴포넌트
ProfileForm에서는 useRef를 사용하여 입력된 값을 추출한다. submitHandler 함수에서 event.preventDefault()로 브라우저 기본 동작을 막고, ref를 통해 현재 값을 가져온다. 유효성 검사를 추가할 수 있지만, 여기서는 생략하고 추출한 데이터를 props.onChangePassword를 통해 부모 컴포넌트로 전달한다.
// components/profile/profile-form.js
import { useRef } from 'react';
import classes from './profile-form.module.css';
function ProfileForm(props) {
const oldPasswordRef = useRef();
const newPasswordRef = useRef();
function submitHandler(event) {
event.preventDefault();
const enteredOldPassword = oldPasswordRef.current.value;
const enteredNewPassword = newPasswordRef.current.value;
// 선택사항: 유효성 검사 추가
// 부모 컴포넌트로 데이터 전달
props.onChangePassword({
oldPassword: enteredOldPassword,
newPassword: enteredNewPassword
});
}
return (
<form className={classes.form} onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='new-password'>New Password</label>
<input type='password' id='new-password' ref={newPasswordRef} />
</div>
<div className={classes.control}>
<label htmlFor='old-password'>Old Password</label>
<input type='password' id='old-password' ref={oldPasswordRef} />
</div>
<div className={classes.action}>
<button>Change Password</button>
</div>
</form>
);
}
export default ProfileForm;
UserProfile 컴포넌트
UserProfile 컴포넌트에서는 ProfileForm에 전달할 changePasswordHandler 함수를 정의한다. 이 함수는 fetch API를 사용하여 /api/user/change-password로 요청을 보낸다.
- Method: PATCH로 설정한다.
- Body: passwordData 객체를 JSON 문자열로 변환하여 전송한다. 이때 객체의 키 이름(oldPassword, newPassword)이 API 라우트에서 추출하는 이름과 일치해야 한다.
- Headers: Content-Type을 application/json으로 설정한다.
// components/profile/user-profile.js
import ProfileForm from './profile-form';
import classes from './user-profile.module.css';
// (useEffect, getSession 등 로딩 처리 로직 생략)
function UserProfile() {
async function changePasswordHandler(passwordData) {
const response = await fetch('/api/user/change-password', {
method: 'PATCH',
body: JSON.stringify(passwordData),
headers: {
'Content-Type': 'application/json'
}
});
const data = await response.json();
console.log(data); // 응답 데이터 확인용
}
return (
<section className={classes.profile}>
<h1>Your User Profile</h1>
<ProfileForm onChangePassword={changePasswordHandler} />
</section>
);
}
export default UserProfile;
테스트 및 검증
이제 브라우저에서 테스트를 진행한다.
- 잘못된 기존 비밀번호 입력: oldPassword를 틀리게 입력하면 API3에서 403 오류("Invalid password")를 반환한다.
- 정상적인 변경: 올바른 기존 비밀번호와 새 비밀번호를 입력하면 "Password updated" 메시지를 받는다.
- 데이터베이스 확인: MongoDB Atlas에서 해당 사용자 문서를 확인하면 해시된 비밀번호 문자열이 변경된 것을 확인할 수 있다.
- 로그인 테스트: 로그아웃 후 이전 비밀번호로 로그인을 시도하면 실패하고, 새 비밀번호로 시도하면 성공적으로 로그인된다.
이로써 서버 사이드에서 인증을 확인하고 보호된 API 라우트를 통해 데이터를 변경하는 기능을 완성했다.
모듈 요약 및 최종 단계
이 모듈에서는 Next.js와 NextAuth를 사용하여 인증의 핵심적인 부분들을 다루었다.
- 페이지 및 라우트 보호 (클라이언트/서버 사이드)
- 사용자 생성 (회원가입)
- 로그인 및 로그아웃 처리
- 인증 상태 확인
- API 라우트 보호
마지막으로 남은 중요한 작업은 배포 시 환경 변수 설정이다. 개발 중 콘솔에 NEXTAUTH_URL 환경 변수가 설정되지 않았다는 경고가 뜰 수 있다.
NEXTAUTH_URL 설정
공식 문서에 따르면, 프로덕션 배포 시 NEXTAUTH_URL 환경 변수를 반드시 설정해야 한다. 이 값은 애플리케이션이 호스팅되는 실제 도메인 주소여야 한다. Vercel과 같은 호스팅 제공업체를 사용할 경우 대시보드에서 환경 변수를 추가할 수 있다. 개발 모드에서는 설정하지 않아도 무방하다.
또한 NextAuth는 우리가 사용한 Credentials 제공자 외에도 Google, GitHub, Facebook 등 다양한 타사 제공자(OAuth)를 지원한다. [...nextauth].js 파일의 providers 배열에 여러 제공자를 추가하여 사용자에게 다양한 로그인 옵션을 제공할 수 있다.
'NextJS' 카테고리의 다른 글
| [NextJS] Module not found: Can't resolve 'net', Module not found: Can't resolve tls (0) | 2025.10.29 |
|---|---|
| [NextJS] SSR과 Hydration 에러 해결 방법 (0) | 2025.10.21 |
| NextJS 로딩 상태 처리 (0) | 2025.10.20 |
| [NextJS] 서버 컴포넌트에서 DB 직접 접근 (0) | 2025.10.19 |
| [NextJS] 서버 컴포넌트에서 데이터 직접 가져오기 (Server-Side Fetching) (0) | 2025.10.18 |