개발/프로젝트

자동 로그인 구현하기

xuwon 2025. 1. 12. 02:02

현재 로그인 시 액세스 토큰과 리프레시 토큰을 받아 쿠키에 저장 중이다.
액세스 토큰은 유효기간이 1시간이며, 리프레시 토큰은 7일이다.

그래서 자동 로그인 시, 액세스 토큰이 만료됐을 때 리프레시 토큰으로 재발급을 받아서 다시 쿠키에 넣어주어야 한다.

우선 로그인 상태를 확인하는 함수를 만들어 주었다.

import { destroyCookie, setCookie } from 'nookies';
import { clearUser } from '@/store/userSlice';
import { useDispatch } from 'react-redux';
import { useAccessTokenRefreshMutation } from '@/api/userApi';

// 로그인 상태 확인 함수
export const useCheckLoginStatus = () => {
  const [accessTokenRefresh] = useAccessTokenRefreshMutation();
  const dispatch = useDispatch();

  // 액세스 토큰 재발급 함수
  const refreshAccessToken = async (refreshToken: string) => {
    try {
      const result = await accessTokenRefresh(
        JSON.stringify({ refresh_token: refreshToken }),
      ).unwrap();

      // 액세스 토큰 다시 쿠키에 저장
      setCookie(null, 'access_token', result?.access_token, {
        maxAge: 60 * 60, // 쿠키 유효기간
        path: '/',
      });

      console.log('토큰 재발급 성공, 새로고침 1초 후 실행');
    } catch (err) {
      console.error('액세스 토큰 재발급 실패:', err);
      handleLogout('로그인이 만료되었습니다.');
    }
  };

  // 로그아웃 처리 함수
  const handleLogout = (message: string) => {
    console.log(message);

    // 로컬 스토리지 초기화
    localStorage.removeItem('persist:user');
    localStorage.removeItem('auto_signin');

    // Redux 상태 초기화
    dispatch(clearUser());

    // 쿠키 삭제
    destroyCookie(null, 'access_token', { path: '/' });
    destroyCookie(null, 'refresh_token', { path: '/' });
  };

  // 로그인 상태 확인 함수
  const checkLoginStatus = () => {
    const cookies = document.cookie.split('; ');
    const accessTokenCookie = cookies.find(row => row.startsWith('access_token'));
    const refreshTokenCookie = cookies.find(row => row.startsWith('refresh_token'));
    const refreshToken = refreshTokenCookie?.split('=')[1];
    const autoSignin = localStorage.getItem('auto_signin');

    if (autoSignin === 'true' && !accessTokenCookie && refreshTokenCookie) {
      // 자동 로그인 O
      // 리프레시 토큰으로 액세스 토큰 재발급
      refreshAccessToken(refreshToken as string);
    } else if (!accessTokenCookie && refreshTokenCookie) {
      // 자동 로그인 X
      // 리프레시 토큰은 있지만 액세스 토큰이 없을 때 로그아웃 처리
      handleLogout('로그인이 만료되었습니다.');
    } else if (!refreshTokenCookie) {
      // 리프레시 토큰도 만료
      handleLogout('로그인이 만료되었습니다.');
    }
  };

  return { checkLoginStatus };
};

로그인 상태를 확인하는 함수, 액세스 토큰을 재발급 해주는 함수, 로그아웃 처리를 해주는 함수

이렇게 구성되어 있다.

const [accessTokenRefresh] = useAccessTokenRefreshMutation();
const dispatch = useDispatch();

서버에 POST 요청을 보내 리프레시 토큰으로 액세스 토큰을 재발급하는 mutation 함수를 정의해서 받아왔다.
그리고 Redux에 유저 정보가 저장되어 있는데, 로그아웃 시 이 정보도 비워주어야 하므로 useDispatch도 받아왔다.

 


로그인 상태 확인 함수

 

const checkLoginStatus = () => {
  const cookies = parseCookies();
  const accessToken = cookies['access_token']; // 'access_token' 쿠키 값
  const refreshToken = cookies['refresh_token']; // 'refresh_token' 쿠키 값
  const autoSignin = localStorage.getItem('auto_signin');

  if (autoSignin === 'true' && !accessToken && refreshToken) {
    // 자동 로그인 O
    // 리프레시 토큰으로 액세스 토큰 재발급
    refreshAccessToken(refreshToken as string);
  } else if (!accessToken && refreshToken) {
    // 자동 로그인 X
    // 리프레시 토큰은 있지만 액세스 토큰이 없을 때 로그아웃 처리
    handleLogout('로그인이 만료되었습니다.');
  } else if (!refreshToken) {
    // 리프레시 토큰도 만료
    handleLogout('로그인이 만료되었습니다.');
  }
};

먼저 브라우저에 저장된 쿠키를 nookiesparseCookies 함수로 읽어온다.
그리고 로컬 스토리지에 저장되어 있는 자동 로그인 여부도 함께 읽어온다.

1. 자동 로그인 O, 액세스 토큰은 없고 리프레시 토큰만 존재하는 경우
refreshAccessToken 함수로 액세스 토큰 재발급

2. 자동 로그인 X, 액세스 토큰은 없고 리프레시 토큰만 존재하는 경우
handleLogout 함수로 로그아웃 처리

3. 자동 로그인 여부와 상관 없이 리프레시 토큰 X
handleLogout 함수로 로그아웃 처리

 


액세스 토큰 발급 함수

 

const refreshAccessToken = async (refreshToken: string) => {
  try {
    const result = await accessTokenRefresh(
      JSON.stringify({ refresh_token: refreshToken }),
    ).unwrap();

    // 액세스 토큰 다시 쿠키에 저장
    setCookie(null, 'access_token', result?.access_token, {
      maxAge: 60 * 60, // 쿠키 유효기간
      path: '/',
    });
  } catch (err) {
    console.error('액세스 토큰 재발급 실패:', err);
    handleLogout('로그인이 만료되었습니다.');
  }
};

서버에 JSON 형태로 리프레시 토큰을 보내고, 응답 데이터로 result를 받아온다.
result에는 액세스 토큰이 담겨서 온다.

그럼 이 resultsetCookie로 다시 쿠키에 저장해준다!


setCookie(context, name, value, options);

1. context
- 서버사이드 렌더링에서 사용되는 컨텍스트 객체
- null은 클라이언트 환경에서 쿠키를 설정하는 경우 사용

2. name
- 생성하거나 업데이트할 쿠키 이름

3. value
- 쿠키에 저장할 값

4. options
- 쿠키의 속성을 설정하는 객체
- ex) 유효기간, 경로, 보안 설정 등


 

그리고 에러가 발생한 경우엔 토큰 발급이 실패되고, 로그인이 만료된다.

 


로그아웃 처리 함수

 

const handleLogout = (message: string) => {
  console.log(message);

  // 로컬 스토리지 초기화
  localStorage.removeItem('persist:user');
  localStorage.removeItem('auto_signin');

  // Redux 상태 초기화
  dispatch(clearUser());

  // 쿠키 삭제
  destroyCookie(null, 'access_token', { path: '/' });
  destroyCookie(null, 'refresh_token', { path: '/' });
};

메시지는 그냥 콘솔에 출력해서 확인하기 위함이고,
로그아웃 처리는 모든걸 다 초기화 해주면 된다.

1. 로컬 스토리지 (removeItem())
2. Redux 상태 (clearUser())
3. 쿠키 (destroyCookie())

이렇게 하면 끝!

 


CheckLoginStatus 컴포넌트

 

'use client';

import { useCheckLoginStatus } from '@/utils/auth';
import { useEffect } from 'react';
import { usePathname } from 'next/navigation';

export const CheckLoginStatus = () => {
  const { checkLoginStatus } = useCheckLoginStatus();
  const pathname = usePathname(); // 현재 경로 가져오기

  useEffect(() => {
    // 초기 렌더링 시 실행
    checkLoginStatus();

    // 5분 간격으로 확인
    const interval = setInterval(() => {
      checkLoginStatus();
    }, 5 * 60);

    return () => clearInterval(interval); // 컴포넌트 언마운트 시 타이머 정리
  }, [checkLoginStatus]);

  useEffect(() => {
    // 경로 변경 시 실행
    checkLoginStatus();
  }, [pathname, checkLoginStatus]);

  return null; // UI 렌더링 없음
};

 

이 컴포넌트는 로그인 상태를 체크하는 역할만 담당한다. 이 외의 UI 렌더링 같은 건 없다.

 

로그인 상태를 확인하는 함수는

1. 초기 렌더링 시
2. 5분마다
3. 페이지 이동 시

이렇게 확인이 진행된다.

useEffect(() => {
  // 초기 렌더링 시 실행
  checkLoginStatus();

  // 5분 간격으로 확인
  const interval = setInterval(() => {
    checkLoginStatus();
  }, 5 * 60);

  return () => clearInterval(interval); // 컴포넌트 언마운트 시 타이머 정리
}, [checkLoginStatus]);

useEffect를 사용하여 초기 렌더링이 될 때 로그인 상태를 확인하도록 했고,
오래 로그인 상태를 유지하기 위해 setInterval 함수로 5분(5 * 60)마다 확인하는 코드도 넣어줬다.

타이머는 컴포넌트가 언마운트 될 때 정리하도록 해서 메모리 누수를 방지한다.

useEffect(() => {
  // 경로 변경 시 실행
  checkLoginStatus();
}, [pathname, checkLoginStatus]);

next/navigationusePathname으로 현재 경로를 불러온 뒤,
경로가 변경될 때마다(페이지 이동) 로그인 상태를 확인하도록 했다.


그리고 종속성 배열에 checkLoginStatus가 있는 이유는,
최신 상태의 checkLoginStatus를 사용하기 위해서이다.

함수의 참조값이 바뀌면 checkLoginStatus도 변경되는 것!

checkLoginStatus은 이 컴포넌트 내부에서만 호출하고 있기도 하고,
정의된 함수들이 외부 상태에 의존하지 않기 때문에 참조값이 바뀔 일이 거의 없다.


 

// layout.tsx
const RootLayout = ({
  children,
}: Readonly<{
  children: React.ReactNode;
}>) => {
  return (
    <html lang="ko">
      <body>
        <Providers>
          <Header />
          <CheckLoginStatus />
          <main className="flex justify-center w-full mt-16 md:mt-20">{children}</main>
        </Providers>
      </body>
    </html>
  );
};

그리고 이렇게 만든 컴포넌트를 layout 컴포넌트에서 호출해 주면 된다.

layout 컴포넌트에서 호출하는 이유는 모든 페이지에서 로그인 상태를 관리하기 위함이다! :>