https://codesmoothie.tistory.com/79
[NextAuth] 로그인 API 구현 [...nextauth].js - 4
사용자 생성(회원가입)에 이어, 이제 실제 인증 기능(로그인)을 구현할 차례입니다. 사용자를 로그인시키고, 로그인된 사용자를 위한 토큰(권한)을 발급받아야 합니다. 또한, 사용자가 로그인했
codesmoothie.tistory.com
이전 블로그에서 로그인 API를 작성했지만, 현재 프론트엔드 컴포넌트 로직은 없습니다. 이번 포스팅에서는 React 컴포넌트에서 어떻게 NextAuth와 연동하여 로그인, 로그아웃 기능을 만들 수 있는지 작성하겠습니다.
NextAuth에서는 직접 HTTP 요청을 보낼 필요가 없습니다. 대신 NextAuth v3의 경우 next-auth/client에서, v4의 경우 next-auth/react에서 signIn 함수를 임포트합니다. signIn 함수를 컴포넌트에서 호출하여 로그인 요청을 보낼 수 있는 함수이며, 요청은 자동으로 전송됩니다.
1. signIn 함수를 이용한 로그인 구현
auth-form.js의 submitHandler 함수 내에서 isLogin이 true일 때 signIn을 호출합니다.
v3: auth-form.js
import { signIn } from 'next-auth/client';
// ...
async function submitHandler(event) {
event.preventDefault();
// ...
if (isLogin) {
const result = await signIn('credentials', {
redirect: false,
email: enteredEmail,
password: enteredPassword,
});
console.log(result);
if (!result.error) {
// 인증 상태 설정
router.replace('/profile');
}
}
// ...
}
v4: auth-form.js
import { signIn } from 'next-auth/react'; // 경로 변경
// ...
async function submitHandler(event) {
event.preventDefault();
// ...
if (isLogin) {
const result = await signIn('credentials', { // 사용법 동일
redirect: false,
email: enteredEmail,
password: enteredPassword,
});
console.log(result);
if (!result.error) {
// 인증 상태 설정
router.replace('/profile');
}
}
// ...
}
signIn 함수는 몇 가지 인자를 받습니다.
- 첫 번째 인자 (Provider): 로그인에 사용할 공급자(provider)를 설명합니다. 한 애플리케이션에 여러 공급자(구글, 페이스북 등)가 있을 수 있습니다. 여기서는 'credentials'를 사용합니다.
- 두 번째 인자 (Configuration Object): 로그인 프로세스를 설정하는 객체입니다.
중요: signIn 함수의 이 두 번째 인자 객체({ redirect: false, email: ..., password: ... })는 백엔드 API 라우트([...nextauth].js)의 authorize 함수가 받는 credentials 매개변수와 정확히 일치합니다.
로그인 테스트 결과
- 유효한 이메일, 잘못된 비밀번호: result 객체에 error: "Could not log you in"이 포함되어 반환됩니다.
- DB에 없는 이메일: result 객체에 error: "No user found"가 포함됩니다.
- 유효한 이메일과 비밀번호: result 객체에 error: null이 포함되며, 이는 성공을 의미합니다.
이제 result.error가 falsy(거짓)인지 확인하여 성공 여부를 판단할 수 있습니다. 성공 시 router.replace('/profile') 등을 사용하여 페이지를 이동시킬 수 있습니다.
하지만 이 방식은 페이지를 새로고침하면 모든 상태(State)가 사라진다는 문제가 있습니다. React Context나 Redux를 사용하더라도 이는 메모리에만 저장됩니다. 우리는 토큰 개념을 사용하여 이 상태를 영구적으로 유지해야 합니다.
2. useSession 훅을 이용한 세션 관리
로그인 상태에 따라 헤더의 네비게이션 옵션을 변경해야 합니다. NextAuth는 성공적으로 로그인하면 자동으로 쿠키를 추가합니다. 브라우저 개발자 도구의 'Application' > 'Cookies' 탭에서 도메인을 선택하면 NextAuth가 생성하고 관리하는 쿠키(예: session-token라는 JWT)를 확인할 수 있습니다.

NextAuth는 이 토큰을 자동으로 사용하여 화면에 표시되는 내용을 변경하거나 보호된 리소스에 요청을 보낼 때 사용합니다. 사용자가 로그인했는지(즉, 유효한 토큰이 있는지) 확인하기 위해 수동으로 쿠키를 서버에 보내 검증할 수도 있지만, NextAuth는 useSession 훅을 통해 편리한 방법을 제공합니다.
main-navigation.js 컴포넌트에서 useSession을 사용할 수 있습니다.
v3: main-navigation.js
import Link from 'next/link';
import { useSession, signOut } from 'next-auth/client';
// ...
function MainNavigation() {
const [session, loading] = useSession();
// ... (logoutHandler)
return (
<header className={classes.header}>
{/* ... (로고) */}
<nav>
<ul>
{!session && !loading && (
<li>
<Link href='/auth'>Login</Link>
</li>
)}
{session && (
<li>
<Link href='/profile'>Profile</Link>
</li>
)}
{/* ... (로그아웃 버튼) */}
</ul>
</nav>
</header>
);
}
v4: main-navigation.js
import Link from 'next/link';
import { useSession, signOut } from 'next-auth/react'; // 경로 변경
// ...
function MainNavigation() {
const { data: session, status } = useSession(); // v4 방식
const loading = status === 'loading'; // v3의 loading과 호환
// ... (logoutHandler)
return (
<header className={classes.header}>
{/* ... (로고) */}
<nav>
<ul>
{!session && !loading && ( // v3와 동일한 로직 사용
<li>
<Link href='/auth'>Login</Link>
</li>
)}
{session && (
<li>
<Link href='/profile'>Profile</Link>
</li>
)}
{/* ... (로그아웃 버튼) */}
</ul>
</nav>
</header>
);
}
- v3에서는 useSession이 [session, loading] 배열을 반환합니다.
- v4에서는 useSession이 { data: session, status } 객체를 반환합니다.
페이지를 새로고침하면 loading 상태가 true이고 session은 undefined였다가, 잠시 후 loading이 false가 되고 session 객체가 채워지는 것을 콘솔에서 확인할 수 있습니다. 이 session 객체에는 토큰에 인코딩한 사용자 데이터와 세션 만료 날짜가 포함됩니다. (참고: 이 세션은 사용자가 활성 상태이면 자동으로 연장됩니다.)
이제 session과 loading 상태를 기반으로 UI를 조건부 렌더링할 수 있습니다.
- Login 링크: 세션이 없고 로딩 중이 아닐 때만 표시.
- Profile 링크: 세션이 있을 때만 표시.
- Logout 버튼: 세션이 있을 때만 표시.
이렇게 하면 로그인 상태에 따라 UI가 올바르게 변경됩니다. (페이지 로드 시 'Profile' 링크가 잠시 깜박일 수 있는데, 이는 로딩 상태 때문이며 나중에 최적화할 수 있습니다.)
3. signOut 함수를 이용한 로그아웃
'Logout' 버튼이 작동하도록 onClick 핸들러를 추가합니다. 로그아웃을 위해서는 v3의 next-auth/client 또는 v4의 next-auth/react에서 제공하는 signOut 함수를 사용합니다.
v3 / v4: main-navigation.js
// (v3) import { useSession, signOut } from 'next-auth/client';
// (v4) import { useSession, signOut } from 'next-auth/react';
function MainNavigation() {
// ... (useSession 훅)
function logoutHandler() {
signOut();
}
return (
<header>
{/* ... (로고 및 링크) */}
<nav>
<ul>
{/* ... (로그인/프로필 링크) */}
{session && ( // session이 있을 때 (로그인 상태일 때)
<li>
<button onClick={logoutHandler}>Logout</button>
</li>
)}
</ul>
</nav>
</header>
);
}
logoutHandler 함수 내에서 signOut()을 호출하기만 하면 됩니다. signOut은 프로미스를 반환하지만, 굳이 await이나 .then()을 사용할 필요가 없습니다. 이유는 우리가 이미 useSession 훅을 사용하고 있기 때문입니다. signOut이 완료되어 세션 상태가 변경되면, useSession이 이를 감지하고 컴포넌트를 자동으로 다시 렌더링합니다.
signOut이 호출되면 NextAuth는 세션 쿠키(session-token)를 지웁니다. 이제 로그인과 로그아웃이 모두 작동하며 UI가 적절하게 업데이트됩니다. 물론 지금은 로그인한 상태에서도 로그인 페이지(/auth)를 방문할 수 있습니다. 반대로 프로필 페이지(/profile)에 있다가 로그아웃해도 그 페이지에 머물러 있게 됩니다. 이는 NextAuth의 라우트 보호 기능을 통해 해결할 수 있습니다.
전체 코드 정리
[auth-form.js] (v3)
import { useState, useRef } from 'react';
import { signIn } from 'next-auth/client';
import { useRouter } from 'next/router';
import classes from './auth-form.module.css';
async function createUser(email, password) {
const response = await fetch('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Something went wrong!');
}
return data;
}
function AuthForm() {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLogin, setIsLogin] = useState(true);
const router = useRouter();
function switchAuthModeHandler() {
setIsLogin((prevState) => !prevState);
}
async function submitHandler(event) {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
// optional: Add validation
if (isLogin) {
const result = await signIn('credentials', {
redirect: false,
email: enteredEmail,
password: enteredPassword,
});
if (!result.error) {
// set some auth state
router.replace('/profile');
} else {
console.log(result.error);
}
} else {
try {
const result = await createUser(enteredEmail, enteredPassword);
console.log(result);
} catch (error) {
console.log(error);
}
}
}
return (
<section className={classes.auth}>
<h1>{isLogin ? 'Login' : 'Sign Up'}</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='email'>Your Email</label>
<input type='email' id='email' required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor='password'>Your Password</label>
<input
type='password'
id='password'
required
ref={passwordInputRef}
/>
</div>
<div className={classes.actions}>
<button>{isLogin ? 'Login' : 'Create Account'}</button>
<button
type='button'
className={classes.toggle}
onClick={switchAuthModeHandler}
>
{isLogin ? 'Create new account' : 'Login with existing account'}
</button>
</div>
</form>
</section>
);
}
export default AuthForm;
[auth-form.js] (v4)
import { useState, useRef } from 'react';
import { signIn } from 'next-auth/react'; // v4: 임포트 경로 변경
import { useRouter } from 'next/router';
import classes from './auth-form.module.css';
async function createUser(email, password) {
const response = await fetch('/api/auth/signup', {
method: 'POST',
body: JSON.stringify({ email, password }),
headers: {
'Content-Type': 'application/json',
},
});
const data = await response.json();
if (!response.ok) {
throw new Error(data.message || 'Something went wrong!');
}
return data;
}
function AuthForm() {
const emailInputRef = useRef();
const passwordInputRef = useRef();
const [isLogin, setIsLogin] = useState(true);
const router = useRouter();
function switchAuthModeHandler() {
setIsLogin((prevState) => !prevState);
}
async function submitHandler(event) {
event.preventDefault();
const enteredEmail = emailInputRef.current.value;
const enteredPassword = passwordInputRef.current.value;
// optional: Add validation
if (isLogin) {
// v4: signIn 호출 로직 자체는 v3와 동일
const result = await signIn('credentials', {
redirect: false,
email: enteredEmail,
password: enteredPassword,
});
if (!result.error) {
// set some auth state
router.replace('/profile');
} else {
console.log(result.error);
}
} else {
try {
const result = await createUser(enteredEmail, enteredPassword);
console.log(result);
} catch (error) {
console.log(error);
}
}
}
return (
<section className={classes.auth}>
<h1>{isLogin ? 'Login' : 'Sign Up'}</h1>
<form onSubmit={submitHandler}>
<div className={classes.control}>
<label htmlFor='email'>Your Email</label>
<input type='email' id='email' required ref={emailInputRef} />
</div>
<div className={classes.control}>
<label htmlFor='password'>Your Password</label>
<input
type='password'
id='password'
required
ref={passwordInputRef}
/>
</div>
<div className={classes.actions}>
<button>{isLogin ? 'Login' : 'Create Account'}</button>
<button
type='button'
className={classes.toggle}
onClick={switchAuthModeHandler}
>
{isLogin ? 'Create new account' : 'Login with existing account'}
</button>
</div>
</form>
</section>
);
}
export default AuthForm;
[main-navigation.js] (v3)
import Link from 'next/link';
import { useSession, signOut } from 'next-auth/client';
import classes from './main-navigation.module.css';
function MainNavigation() {
const [session, loading] = useSession();
function logoutHandler() {
signOut();
}
return (
<header className={classes.header}>
<Link href='/'>
<a>
<div className={classes.logo}>Next Auth</div>
</a>
</Link>
<nav>
<ul>
{!session && !loading && (
<li>
<Link href='/auth'>Login</Link>
</li>
)}
{session && (
<li>
<Link href='/profile'>Profile</Link>
</li>
)}
{session && (
<li>
<button onClick={logoutHandler}>Logout</button>
</li>
)}
</ul>
</nav>
</header>
);
}
export default MainNavigation;
[main-navigation.js] (v4)
import Link from 'next/link';
import { useSession, signOut } from 'next-auth/react'; // v4: 임포트 경로 변경
import classes from './main-navigation.module.css';
function MainNavigation() {
// v4: useSession 반환 값이 객체로 변경
const { data: session, status } = useSession();
const loading = status === 'loading'; // v3의 'loading' 변수와 호환되도록
function logoutHandler() {
signOut(); // v4: signOut 사용법은 동일
}
return (
<header className={classes.header}>
<Link href='/'>
<a>
<div className={classes.logo}>Next Auth</div>
</a>
</Link>
<nav>
<ul>
{/* v4: v3와 동일한 로직을 위해 'loading' 변수 사용 */}
{!session && !loading && (
<li>
<Link href='/auth'>Login</Link>
</li>
)}
{session && (
<li>
<Link href='/profile'>Profile</Link>
</li>
)}
{session && (
<li>
<button onClick={logoutHandler}>Logout</button>
</li>
)}
</ul>
</nav>
</header>
);
}
export default MainNavigation;
'NextJS > NextAuth' 카테고리의 다른 글
| [NextAuth] 서버 사이드 페이지 가드 (라우트 보호) 추가하기 - 7 (0) | 2025.11.18 |
|---|---|
| [NextAuth] 클라이언트 사이드 페이지 가드 (라우트 보호) 추가하기 - 6 (0) | 2025.11.18 |
| [NextAuth] 로그인 API 구현 [...nextauth].js - 4 (0) | 2025.11.16 |
| [NextAuth] 회원가입 컴포넌트 및 API 연동 - 3 (0) | 2025.11.12 |
| [NextAuth] 회원가입 API 구현하기 - 2 (1) | 2025.11.10 |