개발자를 희망하는 초보의 자기개발 이야기

[카카오 로그인] 리액트로 카카오 로그인 구현하기 - 프론트엔드 (2) 본문

프론트엔드(Front-end)

[카카오 로그인] 리액트로 카카오 로그인 구현하기 - 프론트엔드 (2)

클라우드아실 2025. 3. 17. 01:54
반응형

카카오 로그인 구현하기 - 프론트엔드 (2)

이전 글에서는 카카오 로그인 버튼을 만들고 로그인 페이지로 이동하는 과정을 다루었다. 이번 글에서는 인가 코드 처리 및 백엔드 연동을 이어서 다룬다.

 

1. 인가 코드 가져오기 (useSearchParams)

카카오 로그인 후 사용자는 인가 코드(authorization code) 를 Query String으로 받게 된다.

http://localhost:3000/callback?code=nuAFZmJvzl2a5Oz5acQckm8RzTmfSTea9y1SiczkbKjZNN9XyVDNeQAAAAQKPCPnAAABlXhW9uSi-KZYUq23DA

이제 이 인가 코드(nuAFZmJ...)를 백엔드에 전달하여 JWT 토큰을 받아야 한다.

import { useSearchParams } from "next/navigation";

const searchParams = useSearchParams();
const code = searchParams.get("code");

console.log("카카오 인가 코드:", code);

 

2. Axios 설정

백엔드와 API 요청을 할 수 있도록 axios를 설정한다.

import axios from "axios";

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    "Content-Type": "application/json",
  },
  withCredentials: true, // 쿠키를 통한 토큰 처리시
});

export default api;
  • withCredentials: true를 설정하면, 백엔드에서 쿠키를 통한 인증이 가능하다.

참고자료 : CORS로 쿠키 전송하기 (withCredentials 옵션)

 

3. 인가 코드 백엔드로 보내기

인가 코드를 백엔드로 보내서 JWT 토큰을 받아오는 API 요청 함수를 작성한다.

// src/services/authService.ts
import api from "@/lib/axiosInstance";
import { useAuthStore } from "@/store/authStore";

export const kakaoLogin = async (code: string) => {
  try {
    const response = await api.get(`/auth/kakao?code=${code}`);
    const accessToken = response.headers["access"];
    
    useAuthStore.getState().setAuth(accessToken);
    return response.data;
  } catch (error) {
    console.error("카카오 로그인 실패:", error);
    throw error;
  }
};
  • JWT 토큰을 Zustand를 이용해 저장할 예정이다.
  • 우리팀은 토큰을 받을 때  헤더에 'access' 라는 키로 받기로 했었다.

 

4. Zustand로 로그인 상태 관리

Zustand를 이용하여 로그인 상태를 저장한다.

// src/store/authStore.ts
"use client";

import { create } from "zustand";
import { persist, createJSONStorage } from "zustand/middleware";

interface AuthState {
  accessToken: string | null;
  setAuth: (accessToken: string) => void;
  logout: () => void;
}

export const useAuthStore = create<AuthState>()(
  persist(
    (set) => ({
      accessToken: null,
      setAuth: (accessToken) => set({ accessToken }),
      logout: () => set({ accessToken: null }),
    }),
    {
      name: "token",
      storage: createJSONStorage(() => sessionStorage),
    }
  )
);
  • Zustand의 persist 미들웨어를 사용하면 상태에 따라 세션 스토리지에 자동 저장된다.

 

5. Callback 페이지 구현 (KakaoCallback.tsx)

"use client";

import { useEffect } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import { kakaoLogin } from "@/services/authService";

const KakaoCallback = () => {
  const router = useRouter();
  const searchParams = useSearchParams();
  const code = searchParams.get("code");

  useEffect(() => {
    if (!code) {
      console.error("카카오 로그인 실패: code 값 없음");
      router.push("/main");
      return;
    }

    const handleKakaoLogin = async () => {
      try {
        await kakaoLogin(code);
        router.push("/home");
      } catch (error) {
        console.error("카카오 로그인 요청 실패:", error);
        router.push("/main");
      }
    };

    handleKakaoLogin();
  }, [code, router]);

  return <div>로그인 처리 중...</div>;
};

export default KakaoCallback;
  • 로그인 성공 시 /home으로 이동, 실패 시 /main으로 돌아간다.

 

6. axios 인터셉터 설정

// src/lib/axiosInstance.ts
import axios from "axios";
import { useAuthStore } from "@/store/authStore";

const api = axios.create({
  baseURL: process.env.NEXT_PUBLIC_API_URL,
  headers: {
    "Content-Type": "application/json",
  },
  withCredentials: true,
});

api.interceptors.response.use(
  (response) => response,
  async (error) => {
    const originalRequest = error.config;
    if (error.response?.status === 401 && !originalRequest._retry) {
      originalRequest._retry = true;
      try {
        await api.post("/auth/refresh-token");
        return api(originalRequest);
      } catch (refreshError) {
        console.error("토큰 갱신 실패, 로그아웃 필요:", refreshError);
        useAuthStore.getState().logout();
        window.location.href = "/main";
      }
    }
    return Promise.reject(error);
  }
);

export default api;
  • 인터셉터를 통해 JWT 토큰이 만료되면 자동으로 갱신 요청을 한다.

 

7. 로그인 상태에 따른 UI 변경

"use client";

import { useAuthStore } from "@/store/authStore";
import KakaoLoginButton from "@/components/KakaoLoginButton";

export default function MainPage() {
  const accessToken = useAuthStore((state) => state.accessToken);

  return (
    <div className="w-full flex justify-center items-center">
      {accessToken ? <p>환영합니다!</p> : <KakaoLoginButton />}
    </div>
  );
}
  • 로그인 여부에 따라 UI를 변경할 수 있다.

하지만 위의 방법은 세션 스토리지를 통해서 토큰을 관리하기 때문에 보안에 취약하다. 
이어서 다음 글에서 withCredentials 설정과 HttpOnly 쿠키를 이용한 로그인 유지 방식을 진행한다.

반응형