Context를 사용한 로그인 관리

✒️ 2025-05-28 10:35 내용 수정



1. Context 파일 작성

import { createContext, useCallback, useContext, useMemo, useState, useEffect } from "react";
import { getLoginUser, login, logout } from "../api/login";
import { getShortNotification } from "../api/alert";
import { useLocation } from "react-router-dom";

// 로그인 유저 context 생성
const LoginUserContext = createContext();

// provider
function AuthProvider({children}) {
    
    const [user, setUser] = useState(null); // 로그인 한 사용자
    const [notification, setNotification] = useState([]); // 현재 사용자의 알림 목록 최신순 10개
    const location = useLocation(); // location 객체

    async function getUser() { // 현재 로그인 한 사용자 가져오기
        const res = await getLoginUser(); // API 호출
        setUser(res);
    }

    async function getUserNotification() { // 현재 로그인 한 사용자의 알림 최신순 10개만 가져오기
        const res = await getShortNotification();
        setNotification(res);
    }

    useEffect(()=>{ // 요청이 바뀔때마다 사용자 가져오기
        getUser(); // 서버에서 로그인 한 사용자 정보를 가져오는 API를 호출
    }, [location]);

    useEffect(()=>{
        if (user) { // 로그인 해야만 알림 가져옴
            getUserNotification();
        }
    }, [user]);

    // 로그인 처리 - 이 당시 최적화를 위해 useCallback을 사용한 것으로 추정
    const handleLogin = useCallback(async (email, password)=> {
        email = email.trim(); // 공백을 제거해준다

        const res = await login(email, password); // API 호출
        
        if (res.message == 'success') {
            setUser(res.user);
        } 
        return res.message;
    }, []);

    // 로그아웃 처리 - 이 당시 최적화를 위해 useCallback을 사용한 것으로 추정
    const handleLogout = useCallback(async () => {
        const res = await logout(); // API 호출
        setUser();
    }, []);

    // 사용자와 함수를 memo로 처리해서 렌더링 최적화(?)
    const userContextValue = useMemo(()=>({
        user,
        setUser,
        handleLogin,
        handleLogout,
        notification,
        setNotification,
        getUserNotification
    }), [user, setUser, handleLogin, handleLogout, notification, setNotification, getUserNotification]);

	// 이제 위의 value를 Component에 전달해주기 위한 Provider를 작성한다.
    return (
        <LoginUserContext.Provider value={userContextValue}>
            {children}
        </LoginUserContext.Provider>
    );
}

export function useAuth() { // 자식 요소에선 이 Hook를 사용하면 user, setUser 등 사용 가능
    return useContext(LoginUserContext);
}

export default AuthProvider;
useEffect(()=>{ // 요청이 바뀔때마다 사용자 가져오기
	getUser();
}, [location]);

2. 부모 Component에서 Context 설정

import AuthProvider from './contexts/LoginUserContext.js';
import Header from './components/Header.js';
import Footer from './components/Footer.js';
import Main from './pages/Main.js'

function App() {
	return (
		{/* AuthProvider를 전체에 감싸기 */}
		<AuthProvider>
			<div className='app-wrapper'>
				<Header/>
					<div className='content-wrapper'>
						<Routes>
							<Route path="/" element={<Main/>}/>
							{/* 다른 라우트는 생략 */}
						</Routes>
					</div>
				<Footer/>
			</div>
		</AuthProvider>
	);
}

export default App;

3. 자식 Component에서 Context로 받은 value 사용

import { Link, useNavigate } from 'react-router-dom';
import { useAuth } from '../contexts/LoginUserContext.js';

function Header() {
	const { user, handleLogout } = useAuth(); // 로그인 한 사용자 정보 가져오기

	const navigate = useNavigate();

	async function handleLogoutClick() { // 로그아웃 처리
		const res = await handleLogout(); // 로그아웃 API 호출(Context 파일에 함수 있음)
		navigate(0); // 새로고침 실행
	}

	return (
	<>
		<div>
		{/* 사용자의 로그인 여부에 따라 표시할 내용을 선택 */}
		{
			!user ? 
			<>
			  <Link to="/login">로그인</Link>
			  <Link to="/join">회원가입</Link>
			</>
			: 
			<>
			  <span onClick={handleLogoutClick}>로그아웃</span>
			  <Link to="/mypage/">마이페이지</Link>
			</>
		}
		</div>
	</>
	);
}

export default Header;

context 관리 1.png
context 관리 2.png


Context와 loading state를 사용한 로그인 유지(JWT 사용)

새로 고침과 게스트 모드 문제

처음 시도한 해결 방법과 그로 인한 문제점

새 해결 방법

1. Context 설정 및 loading state 추가

import axios from 'lib/axios';
import { createContext, useContext, useState } from "react";
import { Navigate, useLocation, useNavigate } from "react-router-dom";
import { getImage } from 'api/image';

const AuthContext = createContext();

// 사용자 인증 및 사용자 정보 관리
function AuthProvider({children}) {
    const [user, setUser] = useState(null); // 로그인 한 사용자
    const [loading, setLoading] = useState(true); // 로딩 상태
    const navigate = useNavigate();
    const location = useLocation();

    // header 설정
    function setHeader(response) {
        // 응답에서 Access Token 가져와 로컬 변수에 저장
        const {access_token} = response.data;
        // Access Token을 axios의 header의 Authorization Bearer Schema에 적용
        axios.defaults.headers.common['Authorization'] = `Bearer ${access_token}`;
    }

	// ... 생략

    // 재발급
    async function refreshToken() {
        try {
            const res = await axios.post(
                '/auth/refresh-token', 
                {}, 
                {withCredentials: true}
            );

            if (res.status === 200) {
                setHeader(res);
            }

            return res.status;
        } catch (error) {
        }
		setLoading(false);
    }

    // access token 검증 후 axios 요청
    async function authFetch(action) {

        // token이 있을때만 요청
        if (axios.defaults.headers.common['Authorization']) {
            action();
        } else {
            // 재발급
            const token_status = await refreshToken();
            if (token_status === 200) {
                action();
            } else {
                alert("로그인 후 이용해주세요");
                navigate("/login");
            }
        }
    }

    // 사용자 정보 가져오기
    async function getUserInfo() {
        
        // 사용자 정보 가져오기
        async function getUser() {
            try {
                const res = await axios.get(`/user/current-user`);
    
                if (res.status === 200) {
                    setUser(res.data);
                }
            } catch (error) {
            } finally {
                setLoading(false);
            }
        }

        // token이 있을때만 요청
        if (axios.defaults.headers.common['Authorization']) {
            getUser();
        } else {
            // 재발급
            const token_status = await refreshToken();
            if (token_status === 200) {
                getUser();
            }
        }
    }

    return(
        <AuthContext.Provider
        value={{user, login, loading, register, refreshToken, logout, 
        getUserInfo, patchUser, patchUserPwd, deleteUser}}>
            {children}
        </AuthContext.Provider>
    )
}

export function useAuth() {
    return useContext(AuthContext);
}

export default AuthProvider;
  1. JWT 재발급
    • cookie에 저장된 Refresh Token으로 Access Token과 Refresh Token을 재발급 받는 부분이다.
    • Token 재발급이 끝나면 setLoading(false)로 수정하여 게스트가 보호된 라우트에 접근 시 무한 로딩으로 표시되지 않고 로그인 페이지로 접근하도록 한다.
async function refreshToken() {
	try {
		const res = await axios.post(
			'/auth/refresh-token', 
			{}, 
			{withCredentials: true}
		);

		if (res.status === 200) {
			setHeader(res);
		}

		return res.status;
	} catch (error) {
	}
	setLoading(false);
}
  1. 사용자 정보 가져오기
    • 현재 로그인 한 사용자의 정보를 가져오는 부분이다.
    • 사용자 정보를 가져오면 이 동작의 성공 실패 유무와 관계 없이 setLoading(false)로 로딩 상태를 false로 수정한다.
    • Header에 Access Token이 존재하면 바로 요청을 보내고, 없는 경우엔 refreshToken()으로 Token을 재발급한 후에 사용자 정보 요청을 보낸다.
      • 여기서 refreshToken()setLoading(false)를 넣지 않으면 게스트 모드에선 refreshToken()이 Error가 발생하여 getUser()가 호출되지 않으므로 loading state가 true인 상태로 남는다.
      • 이로 인해 게스트 모드에서 보호된 라우트에 접근 시 무한 로딩 페이지가 뜨기 때문에 refreshToken()setLoading(false)를 추가했다.
// 사용자 정보 가져오기
async function getUserInfo() {
	
	// 사용자 정보 가져오기
	async function getUser() {
		try {
			const res = await axios.get(`/user/current-user`);

			if (res.status === 200) {
				setUser(res.data);
			}
		} catch (error) {
		} finally {
			setLoading(false);
		}
	}

	// token이 있을때만 요청
	if (axios.defaults.headers.common['Authorization']) {
		getUser();
	} else {
		// 재발급
		const token_status = await refreshToken();
		if (token_status === 200) {
			getUser();
		}
	}
}

2. ProtectedRoute Component 추가

import { Navigate, Outlet } from "react-router-dom";
import { useAuth } from "./AuthContext";
import { Spinner } from "react-bootstrap";

function ProtectedRoute({ loading, redirectTo = "/login"}) {

    const {user} = useAuth();

    // 로딩 표시
    if (loading) {
        return <div><Spinner/></div>
    }

    // user가 없으면 로그인으로 이동
    if (!user) {
        return <Navigate to={redirectTo} replace/>
    }

    return <Outlet/>
}

export default ProtectedRoute;

3. index.js와 App.js 설정

  1. index.js
    • 사용자 정보를 관리하는 Context인 AuthContext는 index.js에서 설정했다.
    • App.js에서 AuthContext의 함수 및 state들을 사용하기 위해 index.js에서 ContextProvider를 적용하였다.
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import { BrowserRouter } from 'react-router-dom';
import AuthProvider from 'contexts/AuthContext';

const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <BrowserRouter>
    <AuthProvider>
      <App />
    </AuthProvider>
  </BrowserRouter>
);

reportWebVitals();
  1. App.js
    • App.js에서 AuthContext로부터 getUserInfo()를 가져와 페이지 최초 렌더링 시 이 함수를 호출한다.
      • Context에 함수 호출 내용이 반영되기에 App.js 하위 Component, 즉 모든 페이지 및 Component에서 사용자 정보를 공유할 수 있다.
    • ProtectedRoute를 보호할 Component 라우트에 적용하여 게스트와 로그인 유저의 접근을 제어한다.
import 'bootstrap/dist/css/bootstrap.min.css';
import 'styles/common.css'
import 'App.css';
import { Route, Routes } from 'react-router-dom';
import { useAuth } from 'contexts/AuthContext';
import { useEffect, useState } from 'react';
import ProtectedRoute from 'contexts/ProtectedRoute';

function App() {

  const {loading, getUserInfo} = useAuth();

  useEffect(()=>{
    const initializeUser = async () =>{
      await getUserInfo();
    }
    initializeUser();
  },[]);

  return (
    <div className='app-wrapper'>
        <div className='content-wrapper'>
          <Routes>
            {/* 일반 사용자 접근 가능 */}
            <Route path="/" Component={Main}/>
            <Route path="/login" Component={Login}/>
            <Route path="/register" Component={Register}/>
            <Route path="/resetpwd" Component={ResetPwd}/>
            
            {/* 보호된 라우트 */}
            <Route element={<ProtectedRoute loading={loading}/>}>
              <Route path="/user" Component={User}>
                <Route index Component={UserInfo}/>
                <Route path="edit" Component={UserEdit}/>
              </Route>
            </Route>
            <Route path="/error" Component={Error}/>
          </Routes>
        </div>
    </div>
  );
}

export default App;