백엔드의 이해
JWT 기반 인증 흐름 구현: 로그인, 로그아웃 및 토큰 관리
startfront
2025. 4. 29. 09:17
- JWT 실제 활용하여 로그인 로그아웃 만들기
JWT 기반 인증 흐름 이해
JWT(Json Web Token) 인증 흐름
- 사용자가 로그인 요청 (아이디, 비밀번호)
- 서버가 아이디/비밀번호 검증 후 Access Token과 Refresh Token 발급
- 클라이언트는 Access Token을 저장해두고 API 요청 시 Authorization 헤더에 넣어 보냄
- Access Token이 만료되면, Refresh Token을 이용해 새로운 Access Token을 발급받음
- 로그아웃 시 모든 토큰을 무효화
- Access Token 빠르게 만료시켜 보안 유지
- Refresh Token은 서버에서 재검증해서 새로운 Access Token 발급
개발 구조 및 흐름 정리
- Types 정의: 로그인 상태를 명확히 표현
- Redux Toolkit을 이용한 Slice 생성: 전역 로그인 상태 관리
- Service Layer 구축: API 통신 분리
- Custom Hook 작성: 로그인 로직 캡슐화
- 컴포넌트에서 Hook 사용: UI에서 로그인/로그아웃 처리
1. 타입 정의 (Types)
인증 관련 데이터 구조를 명확히 타입으로 정의
// 사용자의 인증 정보를 표현
export interface Auth {
accessToken?: string; // 서버로부터 발급 받은 액세스 토큰
refreshToken?: string; // 리프레시 토큰
id: string; // 사용자 ID
isLoggedIn: boolean; // 로그인 여부
}
// Redux Store에 저장될 상태 타입
export interface MemberState {
memberState: Auth | null;
}
=> TypeScript로 데이터 구조를 엄격히 관리
2. Redux Toolkit으로 Slice 생성
import { createSlice, PayloadAction } from '@reduxjs/toolkit';
import { Auth, MemberState } from '../types/memberTypes';
const initialState: MemberState = {
memberState: null, // 초기에는 로그인 상태가 없음
};
const authSlice = createSlice({
name: "member", // slice 이름
initialState,
reducers: {
// 로그인 시 사용자 정보를 저장
login: (state, action: PayloadAction<Auth>) => {
state.memberState = action.payload;
},
// 로그아웃 시 인증정보 제거
logout: (state) => {
state.memberState = null;
},
// Access Token만 별도로 갱신할 때 사용
setAccessToken: (state, action: PayloadAction<string>) => {
if (state.memberState) {
state.memberState.accessToken = action.payload;
}
},
},
});
export const { login, logout, setAccessToken } = authSlice.actions;
export default authSlice.reducer;
- createSlice는
- Redux Toolkit에서 상태(state), 리듀서(reducer), 액션(action)을 한 번에 선언할 수 있게 도와주는 함수
3. API 요청 함수 작성 (Service Layer)
import axios from 'axios';
import { Member, Auth } from '../types/memberTypes';
// 로그인 API
export const loginMember = async (member: Member): Promise<Auth> => {
const response = await axios.post("/member/login", { id: member.id, password: member.password });
const accessToken = response.headers["authorization"];
const refreshToken = response.headers["refresh-token"];
return { accessToken, refreshToken, id: member.id, isLoggedIn: true };
};
// Access Token 재발급 API
export const refreshAccessToken = async (id: string): Promise<string> => {
const response = await axios.post("/member/refresh", { id });
const accessToken = response.headers["authorization"];
return accessToken;
};
- Axios에서는 HTTP 응답 헤더를 소문자로 접근 (authorization, refresh-token).
4. Custom Hook으로 인증 로직 캡슐화
import { useCallback } from "react";
import { useMutation } from "@tanstack/react-query";
import { useRouter } from "next/navigation";
import { useAppDispatch, useAppSelector } from "../store/hooks";
import { login, logout, setAccessToken } from "../slices/memberSlice";
import { loginMember } from "../service/member";
export const useAuth = () => {
const dispatch = useAppDispatch();
const router = useRouter();
const memberState = useAppSelector((state) => state.member.memberState);
// 로그인 요청
const loginMutation = useMutation({
mutationFn: loginMember,
onSuccess: (data) => {
dispatch(login(data)); // 로그인 성공 시 Redux 저장
router.push("/"); // 홈으로 이동
},
onError: (error) => {
console.error("로그인 실패:", error);
alert("로그인 실패");
},
});
// 로그인 함수
const signIn = useCallback(
(member: Member) => {
loginMutation.mutate(member);
},
[loginMutation]
);
// Access Token 갱신 함수
const setToken = useCallback(
(token: string) => dispatch(setAccessToken(token)),
[dispatch]
);
// 로그아웃 함수
const signOut = useCallback(() => dispatch(logout()), [dispatch]);
return { memberState, loginMutation, login: signIn, logout: signOut };
};
=> 인증 로직을 UI와 완전히 분리
5. 컴포넌트에서 Hook 사용
import { useAuth } from "@/store/hooks/memberHook";
import { useRouter } from "next/navigation";
export default function Profile() {
const { memberState, logout } = useAuth();
const router = useRouter();
const handleLogin = () => router.push("/member/login");
const handleLogout = () => {
logout();
router.push("/books");
};
if (!memberState?.isLoggedIn) {
return (
<div>
<button onClick={handleLogin}>로그인</button>
<Link href="/member/regist">
<button>회원가입</button>
</Link>
</div>
);
}
return (
<div>
<span>{memberState.id}님</span>
<Link href="/member">
<button>회원정보</button>
</Link>
<button onClick={handleLogout}>로그아웃</button>
</div>
);
}
6. 실제 APP Member 에서 로그인/ 로그아웃 처리!!
"use client";
import { useCallback, useRef } from "react";
import styles from "./login.module.scss";
import { useAuth } from "@/store/hooks/memberHook";
const MemberLogin = () => {
// 아이디 입력 필드를 위한 ref 선언
const idRef = useRef<HTMLInputElement>(null);
// 비밀번호 입력 필드를 위한 ref 선언
const passwordRef = useRef<HTMLInputElement>(null);
// 로그인 기능을 위한 useAuth 훅에서 함수 전달받기
const { login } = useAuth();
// 로그인 버튼 클릭 시 호출되는 함수
const handleLogin = useCallback(() => {
// 입력된 아이디와 비밀번호 값 가져오기
const id = idRef.current?.value.trim() || "";
const password = passwordRef.current?.value.trim() || "";
// 아이디가 입력되지 않으면 경고창 띄우고 포커스 이동
if (!id) {
alert("아이디를 입력하세요");
idRef.current?.focus();
return;
}
// 비밀번호가 입력되지 않으면 경고창 띄우고 포커스 이동
if (!password) {
alert("비밀번호를 입력하세요");
passwordRef.current?.focus();
return;
}
// 로그인 요청
login({ id, password });
}, [login]);
return (
<div className={styles.container}>
{/* 로그인 폼 테이블 */}
<table className={styles.table}>
<caption>로그인</caption>
<tbody>
<tr>
<td>아이디</td>
<td>
<input
type="text"
placeholder="아이디를 입력하세요"
ref={idRef} // 아이디 입력 필드 ref 연결
/>
</td>
</tr>
<tr>
<td>비밀번호</td>
<td>
<input
type="password"
placeholder="비밀번호를 입력하세요"
ref={passwordRef} // 비밀번호 입력 필드 ref 연결
/>
</td>
</tr>
</tbody>
</table>
{/* 로그인 버튼 */}
<div className={styles.buttonGroup}>
<button className={styles.registerButton} onClick={handleLogin}>
로그인
</button>
</div>
</div>
);
};
export default MemberLogin;