개발/프로젝트

Next.js에서 RTK Query로 카카오 로그인 구현하기

xuwon 2025. 1. 9. 03:16

카카오 소셜 로그인은

1. /social/kakao/login 으로 이동하여 인증 진행
2. 받아온 code/social/kakao/callback에 GET 요청 보내서 토큰 받기
3. 토큰은 쿠키에 저장
4. 로그인 성공 후 토큰으로 유저 정보 GET 요청으로 받아옴
5. 유저 정보 Redux에 저장

이런 흐름으로 진행하였다.

 


// sign-in
const handleLogin = (provider: string) => {
  switch (provider) {
    case 'kakao':
      // 서버의 카카오 로그인 엔드포인트로 이동
      window.location.href = 'kakao login endpoint'; // 해당 페이지로 리디렉션
      break;
    case 'google':
      // 서버의 구글 로그인 엔드포인트로 이동
      window.location.href = 'google login endpoint';
      break;
  }
};

우선 provider의 종류에 따라 각 엔드포인트로 이동할 수 있게 해주었다.

handleLogin은 각 소셜 로그인 버튼에 연결!
리디렉션 되면 인증이 진행되고, 인증이 끝나면 리다이렉트 uri로 연결된다.
(미리 uri 등록해야 함.)

나는 /kakao/callback 에다가 콜백 컴포넌트를 만들었다.

Next.js파일 기반 라우팅을 사용하기 때문에, /kakao/callback 여기에 콜백 컴포넌트를 만들면
해당 경로의 엔드포인트 역할도 하게 된다!

따라서 인증이 끝난 후엔 code를 담아서 /kakao/callback으로 이동한다.


인증이 끝났으니 /social/kakao/callback 여기에 code를 담아 GET 요청을 보내야 한다.
RTK Query를 활용하여 만들어 주었다.

// userApi.ts

// 서버로 GET 요청으로 인증 코드 전달
sendKakaoCode: builder.query({
   query: (code: string) => ({
    url: `/social/kakao/callback?code=${code}`,
    method: 'GET', // 기본값이 GET이므로 생략 가능
    headers: {
      Accept: 'application/json',
    },
  }),
}),


이제 콜백 컴포넌트를 보자.

'use client';

import { useGetUserInfoMutation, useSendKakaoCodeQuery } from '@/api/userApi';
import LoadingSpinner from '@/components/common/loadingSpinner/LoadingSpinner';
import { setUser } from '@/store/userSlice';
import { useRouter } from 'next/navigation';
import { setCookie } from 'nookies';
import { useEffect } from 'react';
import { useDispatch } from 'react-redux';

const KakaoCallback = () => {
  const router = useRouter();
  const params = new URLSearchParams(window.location.search);
  const code = params.get('code'); // 인증 코드 추출
  const dispatch = useDispatch();
  const [getUserInfo] = useGetUserInfoMutation();

  const { data, error, isLoading } = useSendKakaoCodeQuery(code || '');

  useEffect(() => {
    if (!code) {
      router.push('/sign-in'); // 인증 코드가 없으면 로그인 페이지로 이동
    }
  }, [code, router]);

  useEffect(() => {
    if (data) {
      console.log('토큰 정보:', data);
      setCookie(null, 'access_token', data.access_token, {
        maxAge: 30 * 24 * 60 * 60, // 쿠키 유효기간
        path: '/', // 쿠키 경로
      });
      setCookie(null, 'refresh_token', data.refresh_token, {
        maxAge: 30 * 24 * 60 * 60,
        path: '/',
      });

      // 로그인 성공 후 유저 데이터 받아오기
      getUserInfo({})
        .unwrap()
        .then(userData => {
          console.log('유저 정보:', userData);
          dispatch(setUser({ user: userData, loginType: 'kakao' })); // Redux에 유저 정보 저장
          router.push('/'); // 홈으로 이동
        })
        .catch(error => {
          console.error('유저 정보 요청 실패:', error);
          router.push('/sign-in');
        });
    }
    if (error) {
      console.error('카카오 로그인 실패:', error);
      console.log(code);
      router.push('/sign-in'); // 실패 시 다시 로그인 페이지로 이동
    }
  }, [data, error, router, code, dispatch, getUserInfo]);

  if (isLoading) return <LoadingSpinner className="hi" />;

  return <></>;
};

export default KakaoCallback;

 

 

const params = new URLSearchParams(window.location.search);
const code = params.get('code'); // 인증 코드 추출

카카오 로그인을 완료하면 카카오 서버가 설정된 리디렉션 URL로 인증 코드를 전달한다.
window.location.search에서 code 파라미터를 추출하면, code 값을 얻을 수 있다.

 

코드가 없다면 로그인이 완료되지 않은 것이므로 로그인 페이지로 리다이렉트.

useEffect(() => {
  if (!code) {
    router.push('/sign-in'); // 렌더링 완료 후 실행되므로 안전
  }
}, [code, router]);

useEffect를 사용하는 이유는 컴포넌트가 렌더링 된 다음에 code 값을 추출하여 확인해야 하기 때문이다.

렌더링 도중에 router.push()를 호출하면 오류가 발생할 수 있음.

 

인증 코드 전달
const { data, error, isLoading } = useSendKakaoCodeQuery(code || '');

아까 정의한 useSendKakaoCodeQuerycode를 담아 GET 요청을 보낸다.

- data: 응답 데이터 (토큰 정보)
- error: 에러 정보 (요청 실패)
- isLoading: 요청 진행 상태

GET 요청이 성공했다면, data에 액세스 토큰과 리프레시 토큰이 담긴다.

 

토큰을 쿠키에 저장
if (data) {
  setCookie(null, 'access_token', data.access_token, {
    maxAge: 30 * 24 * 60 * 60,
    path: '/',
  });
  setCookie(null, 'refresh_token', data.refresh_token, {
    maxAge: 30 * 24 * 60 * 60,
    path: '/',
  });
}

이제 반환된 액세스 토큰과 리프레시 토큰을 쿠키에 저장해준다.
nookies 라이브러리를 사용해서 편리하게 쿠키를 설정했다.

- maxAge: 쿠키 유효기간 설정
- path: 모든 경로에서 쿠키 접근이 가능하도록 설정

maxAge는 초 단위로 설정된다.
따라서 30 * 24(h) * 60(m) * 60(m) = 30일 이 된다.

 

사용자 정보 요청 및 Redux 상태 업데이트
getUserInfo({})
  .unwrap()
  .then(userData => {
    dispatch(setUser({ user: userData, loginType: 'kakao' }));
    router.push('/'); // 홈으로 이동
  })
  .catch(error => {
    router.push('/sign-in');
  });

저장된 액세스 토큰을 기반으로 사용자 정보를 요청한다.
(액세스 토큰은 헤더에 담겨서 전달됨)

프로미스 형태로 반환되므로 then 체이닝으로 받아와야 한다.
(결과 값은 작업이 완료된 이후에만 사용할 수 있으므로...)

성공 시 dispatch(setUser())를 사용해서 사용자 정보와 로그인 타입을 저장한다.
(로그인 타입은 새로 추가함)
실패 시 로그인 페이지로 리다이렉트 된다. ㅠ

 

쿠키에 토큰 저장, 유저 데이터 받아오기, Redux에 유저 데이터 저장
이 과정 역시 useEffect가 꼭 필요하다.

왜냐면 useSendKakaoCodeQuerycode를 전달한 후에, data(토큰)를 받아야만 다음 과정을 진행할 수 있기 때문에
data, error의 변경을 감지하고 다음 과정을 진행시킨다.

 

로딩 처리
if (isLoading) return <LoadingSpinner className="" />;

인증 코드 전달 및 서버 응답을 기다리는 동안엔 로딩 스피너 컴포넌트를 표시하도록 했다. (미리 만들어져 있음)

 


이렇게 하면 소셜 로그인 후 유저 정보까지 잘 받아와 지는 것을 볼 수 있다.
구글 로그인도 동일하게 진행하면 된당.

로그아웃 할 때는 쿠키 비우고, Redux 비우고, 로그아웃 API 요청 보내는 것 잊지 말기... ☆


아, 그리고 Redux에 저장된 정보는 변경될 때마다 알아서 업데이트가 되고, 리렌더링이 된다고 한다!
아주 편리한 녀석이군.