JWT 기반 인증 흐름 구현: 로그인, 로그아웃 및 토큰 관리

2025. 4. 29. 09:17·백엔드의 이해
목차
  1. - JWT 실제 활용하여 로그인 로그아웃 만들기
  2. JWT 기반 인증 흐름 이해
  3. 개발 구조 및 흐름 정리

- JWT 실제 활용하여 로그인 로그아웃 만들기

JWT 기반 인증 흐름 이해

JWT(Json Web Token) 인증 흐름

  1. 사용자가 로그인 요청 (아이디, 비밀번호)
  2. 서버가 아이디/비밀번호 검증 후 Access Token과 Refresh Token 발급
  3. 클라이언트는 Access Token을 저장해두고 API 요청 시 Authorization 헤더에 넣어 보냄
  4. Access Token이 만료되면, Refresh Token을 이용해 새로운 Access Token을 발급받음
  5. 로그아웃 시 모든 토큰을 무효화
  • Access Token 빠르게 만료시켜 보안 유지
  • Refresh Token은 서버에서 재검증해서 새로운 Access Token 발급

개발 구조 및 흐름 정리

  1. Types 정의: 로그인 상태를 명확히 표현
  2. Redux Toolkit을 이용한 Slice 생성: 전역 로그인 상태 관리
  3. Service Layer 구축: API 통신 분리
  4. Custom Hook 작성: 로그인 로직 캡슐화
  5. 컴포넌트에서 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;

'백엔드의 이해' 카테고리의 다른 글

JWT 인증 방식  (0) 2025.04.28
Node.js 에 대하여 이해하기  (0) 2025.03.31
SQL Mapper XML  (1) 2025.03.10
  1. - JWT 실제 활용하여 로그인 로그아웃 만들기
  2. JWT 기반 인증 흐름 이해
  3. 개발 구조 및 흐름 정리
'백엔드의 이해' 카테고리의 다른 글
  • JWT 인증 방식
  • Node.js 에 대하여 이해하기
  • SQL Mapper XML
startfront
startfront
startfront 님의 블로그 입니다.
  • startfront
    startfront 님의 블로그
    startfront
  • 전체
    오늘
    어제
    • 분류 전체보기 (42)
      • 프로젝트 (5)
        • 프로젝트 해보기 (1)
        • 디자인 알아보기 (2)
        • 프로젝트의 기본 (2)
      • 백엔드의 이해 (4)
      • React (14)
      • 프론트엔드 기본 (2)
        • Html (1)
        • Css (0)
        • JavaScript (1)
      • Cs기본지식 (14)
        • 알고리즘 (8)
        • 데이터베이스 (6)
      • Java (3)
        • Java의 이해 (3)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
startfront
JWT 기반 인증 흐름 구현: 로그인, 로그아웃 및 토큰 관리

개인정보

  • 티스토리 홈
  • 포럼
  • 로그인
상단으로

티스토리툴바

단축키

내 블로그

내 블로그 - 관리자 홈 전환
Q
Q
새 글 쓰기
W
W

블로그 게시글

글 수정 (권한 있는 경우)
E
E
댓글 영역으로 이동
C
C

모든 영역

이 페이지의 URL 복사
S
S
맨 위로 이동
T
T
티스토리 홈 이동
H
H
단축키 안내
Shift + /
⇧ + /

* 단축키는 한글/영문 대소문자로 이용 가능하며, 티스토리 기본 도메인에서만 동작합니다.