코드 출처 : https://github.com/Cluster-Taek/next14-boilerplate
GitHub - Cluster-Taek/next14-boilerplate
Contribute to Cluster-Taek/next14-boilerplate development by creating an account on GitHub.
github.com
login-form.tsx
'use client';
import Button from '@/components/common/button';
import { ILoginFormValue } from '@/lib/common/account';
import { sva } from '@/styled-system/css';
import { Box } from '@/styled-system/jsx';
import { signIn } from 'next-auth/react';
import { useSearchParams } from 'next/navigation';
import { Controller, useForm } from 'react-hook-form';
const LoginForm = () => {
const loginFormStyle = LoginFormSva();
const searchParams = useSearchParams();
const {
handleSubmit: formHandleSubmit,
formState,
control,
} = useForm<ILoginFormValue>({
defaultValues: {
login: 'test@gmail.com',
password: '1234',
},
});
const handleSubmit = formHandleSubmit(async (data) => {
await signIn('login-credentials', { ...data, callbackUrl: searchParams.get('callbackUrl') ?? '/' });
});
return (
<Box className={loginFormStyle.wrapper}>
<Box className={loginFormStyle.title}>Login</Box>
<form className={loginFormStyle.form} onSubmit={handleSubmit}>
<Controller
control={control}
name="login"
rules={{ required: 'Email is required' }}
render={({ field }) => <input type="text" {...field} className={loginFormStyle.input} placeholder="email" />}
/>
{formState.errors.login && <Box className={loginFormStyle.error}>{formState.errors.login.message}</Box>}
<Controller
control={control}
name="password"
rules={{ required: 'Password is required' }}
render={({ field }) => (
<input type="password" {...field} className={loginFormStyle.input} placeholder="password" />
)}
/>
{formState.errors.password && <Box className={loginFormStyle.error}>{formState.errors.password.message}</Box>}
<Button type="submit">Submit</Button>
</form>
{searchParams.get('error') === 'CredentialsSignin' && (
<Box>
<Box className={loginFormStyle.error}>Invalid email or password</Box>
</Box>
)}
</Box>
);
};
export default LoginForm;
const LoginFormSva = sva({
slots: ['wrapper', 'title', 'form', 'input', 'error'],
base: {
wrapper: {
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
borderRadius: 'md',
shadow: 'md',
padding: '4',
width: 'sm',
margin: '0 auto',
},
title: {
fontSize: '2xl',
fontWeight: 'bold',
},
form: {
width: 'full',
marginTop: '4',
},
input: {
width: 'full',
marginY: '2',
padding: '2',
borderRadius: 'md',
border: '1',
},
error: {
color: 'red.500',
},
},
});
- next-auth와 react-hook-form을 사용하여 폼 데이터를 처리하고, 인증을 처리하는 구조로 되어 있습니다.
전체 흐름
- 사용자가 폼에 이메일과 비밀번호를 입력하고 제출.
- signIn 함수가 NextAuth 서버로 요청을 보냄.
- CredentialsProvider의 authorize 함수에서 입력값 검증.
- 성공: JWT 생성 및 세션 설정 → 리다이렉트.
- 실패: 오류 메시지 표시.
1. LoginForm 컴포넌트
LoginForm은 사용자가 이메일과 비밀번호를 입력하고 로그인할 수 있는 폼을 렌더링합니다.
상태와 폼 설정
- useForm<ILoginFormValue>: react-hook-form을 사용하여 폼 데이터를 관리합니다. ILoginFormValue 타입을 통해 login(이메일)과 password(비밀번호) 값이 포함됩니다.
- defaultValues: 폼의 기본값으로 login과 password에 test@gmail.com과 1234를 미리 설정.
- handleSubmit: 폼 제출 시 실행될 함수. 제출된 데이터를 signIn 함수에 전달하여 인증을 처리합니다.
- formState: 폼의 상태를 추적하며, 오류 메시지 등을 표시하는 데 사용됩니다.
폼 제출
- handleSubmit: 폼 데이터를 제출할 때 호출되는 함수입니다.
- signIn('login-credentials', {...data}): next-auth의 signIn 함수를 호출하여 사용자 인증을 시도합니다. 인증이 성공하면, callbackUrl(리다이렉트 URL)로 이동합니다. 실패하면 error 파라미터가 URL에 포함되어 로그인 폼에 오류 메시지가 표시됩니다.
2. 폼 요소
Controller는 react-hook-form의 컴포넌트로, 폼 필드를 관리합니다. 각 필드는 다음과 같이 설정됩니다:
- 이메일 필드 (login):
- rules: 필수 입력 사항으로, 이메일이 없으면 오류 메시지를 표시합니다.
- render: 실제로 input 필드를 렌더링합니다.
- 오류가 발생하면 formState.errors.login.message를 통해 오류 메시지를 표시합니다.
- 비밀번호 필드 (password):
- rules: 비밀번호 필드 역시 필수 입력 사항입니다.
- render: 비밀번호 입력 필드를 렌더링합니다.
- 오류 메시지를 동일하게 처리합니다.
오류 메시지
- 만약 이메일이나 비밀번호 입력에 오류가 있으면, formState.errors에 오류 메시지가 포함되어 해당 메시지가 폼 아래에 표시됩니다.
- 로그인 시 CredentialsSignin 오류가 발생하면 "Invalid email or password"라는 오류 메시지가 나타납니다.
3. 로그인 실패 처리
- searchParams.get('error') === 'CredentialsSignin': 로그인 실패 시 URL에 포함된 error=CredentialsSignin을 확인하여 사용자에게 "Invalid email or password" 메시지를 표시합니다.
handleSubmit
const handleSubmit = formHandleSubmit(async (data) => {
await signIn('login-credentials', {
...data,
callbackUrl: searchParams.get('callbackUrl') ?? '/'
});
});
- 사용자가 이메일과 비밀번호를 입력한 후 폼을 제출하면 handleSubmit 함수가 실행됩니다.
- formHandleSubmit: react-hook-form에서 폼 데이터를 검증한 후 호출되는 함수.
- data: 사용자가 입력한 이메일(login)과 비밀번호(password).
- signIn: next-auth의 내장 함수로, 인증 요청을 서버로 보냅니다.
- 첫 번째 인자: 'login-credentials' → CredentialsProvider를 통해 정의된 인증 프로바이더 사용.
- 두 번째 인자: 사용자 입력 데이터와 리다이렉트 URL 포함.
NextAuth로 인증 요청
signIn 함수가 호출되면, NextAuth는 auth-options.ts에서 정의된 인증 핸들러로 요청을 보냅니다.
이 요청은 CredentialsProvider에 의해 처리됩니다.
CredentialsProvider 설정
credentialsProviderOption에 정의된 authorize 함수가 호출되어 사용자의 입력값을 확인합니다.
async authorize(credentials: Record<string, unknown> | undefined) {
try {
if (credentials?.login === 'test@gmail.com' && credentials?.password === '1234') {
const user = {
name: 'John Doe',
accessToken: 'jwt-token',
refreshToken: 'refreshToken',
};
return {
id: credentials?.login as string,
name: user.name,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
};
} else {
return null; // 인증 실패 시 null 반환
}
} catch (error) {
console.error(error);
return null;
}
}
- 입력 검증: 이메일과 비밀번호를 확인.
- 현재는 하드코딩된 값(test@gmail.com, 1234)과 비교.
- 실제 구현에서는 API 요청으로 인증 처리(login 함수 주석 참고).
- 성공: 사용자 정보를 반환(name, accessToken, refreshToken).
- 실패: null을 반환하여 인증 실패 처리.
JWT 생성 및 관리
authorize 함수가 성공적으로 사용자 데이터를 반환하면, jwt 콜백이 실행됩니다.
JWT 처리 과정
async jwt({ token, user, account, trigger, session }) {
if (account && user) {
return {
user,
accessToken: user.accessToken,
refreshToken: user.refreshToken,
exp: jwtDecode(user.accessToken as string).exp as number,
};
}
if (dayjs().isBefore(dayjs((token.exp as number) * 1000))) {
return token;
}
const { accessToken, refreshToken } = await tokenRefresh(token.refreshToken as string);
token.accessToken = accessToken;
token.refreshToken = refreshToken;
token.exp = jwtDecode(accessToken).exp as number;
return token;
}
- 최초 로그인:
- account와 user 값이 존재 → 사용자의 accessToken, refreshToken 및 만료 시간(exp)을 토큰에 저장.
- 토큰 유효성 검사:
- 토큰 만료 전(exp 확인): 기존 토큰 그대로 사용.
- 토큰 만료 후: tokenRefresh를 호출해 새로운 토큰을 발급받고 업데이트.
세션 설정
JWT가 생성된 후, session 콜백이 실행됩니다.
async session({ session, token }) {
if (token.user) {
session.user = token.user as User;
}
return session;
}
- 세션 객체에 사용자 정보를 추가(session.user).
- 이 세션은 클라이언트에서 useSession 훅 등을 통해 접근 가능.
로그인 완료
로그인이 성공하면 callbackUrl로 리다이렉트됩니다.
- 기본값: / (홈페이지).
- 지정된 URL이 있다면 해당 URL로 이동.
로그인 실패
로그인이 실패하면 searchParams.get('error')가 CredentialsSignin으로 설정되며, 사용자에게 오류 메시지가 표시됩니다.
{searchParams.get('error') === 'CredentialsSignin' && (
<Box className={loginFormStyle.error}>Invalid email or password</Box>
)}
'개발 공부 > React' 카테고리의 다른 글
| NextAuth.js 요약 (1) | 2025.01.19 |
|---|---|
| NextAuth.js 사용자 인증 로직 (3) NextAuth 동작 방식 (0) | 2025.01.19 |
| NextAuth.js 사용자 인증 로직 (1) (0) | 2025.01.11 |
| Next.js usePathname, useRouter, useSearchParams (0) | 2025.01.08 |
| React-query useQuery, useMutation (0) | 2025.01.06 |