Develop/React.js

[React] 카카오 로그인 구현하기(백엔드와의 협업)

dawonny 2023. 8. 11. 19:55
728x90
반응형

이번 프로젝트 로그인 부분을 내가 담당하게 됐는데,

로그인 방식 중에서 카카오 로그인을 구현하게 됐다.

 

리액트 카카오 로그인에 관련해서 정리된 글들이 많아 

개발할 때 참고하면서 진행할 수 있었다.

하지만 까먹지 않고자 한번 정리하려 포스팅해본다.


방식

프론트엔드와 백엔드가 협업하면서 카카오 로그인을 구현하는 방식이 다를 수 있다.

나는 이런 방식으로 했다- 라는 정도로 참고하시면 좋을 것 같다.

나는 프론트엔드 담당이어서 프론트엔드 입장에서 서술해보려고한다.

 

  1. 프론트엔드에서 카카오 로그인 요청 / 인가 코드 받기 요청을 한다
  2. 카카오에서 redirect url 로 인가코드를 프론트엔드로 보내준다.
  3. 받은 인가코드를 백엔드에게 보낸다.
  4. 백엔드는 이 인가코드를 받아 처리해서 AccessToken 을 응답으로 프론트엔드에게 보내준다.
  5. 프론트엔드는 AccessToken 을 이용해서 필요한 API 호출에 사용한다.

인가코드 요청하기

구현된 코드를 보자.

먼저 나는 로그인 버튼을 누르면 카카오 로그인을 진행할 수 있도록 구현했다.

아래는 로그인 버튼을 눌렀을 때 (onClick) 실행되는 함수이다.

    // 카카오 로그인
    const handleLogin = () => {
        window.location.href = KAKAO_AUTH_URL;
    };

KAKAO_AUTH_URL 라는 경로로 이동하는 코드이다.

 

KAKAO_AUTH_URL 을 보러가보자

    const Rest_api_key = process.env.REACT_APP_KAKAO_REST_API_KEY;
    const redirect_uri = process.env.REACT_APP_KAKAO_REDIRECT_URI;
    const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${Rest_api_key}&redirect_uri=${redirect_uri}&response_type=code`;

Rest_api_key 와 redirect_uri 는 본인의 프로젝트에 맞게 작성해주면된다.

노출되면 안되니까 나는 .env 파일에 적어놓았다.

 

Rest_api_key 와 redirect_uri 는 kakao developers 사이트에서 설정할 때 확인할 수 있으며

백엔드 팀원분들과 상의하에 정하면 될 것 같다.

(우리는 백엔드 쪽에서 관리하셨다.)

 

이때 Rest_api_key 는 내가 로그인 요청을 할 때 리다이렉트되는 주소이다.

나는 로컬에서 개발하고 있기 때문에 http://localhost:3000/kakaoLogin 이라고 지정했다.

백엔드팀원분에게도 이 주소를 redirect_uri 에 등록해달라고 요청드렸다.


redirect 페이지 등록

아까 리다이렉트 되는 주소를 '/kakaoLogin' 으로 설정했다고 했다.

나도 이 경로로 접속했을 때 보여줄 페이지를 따로 지정해주기 위해 먼저 라우트를 등록한다.

function App() {
	...
    return (
        <div className="App">
            <Routes>
				...
                <Route path="/kakaoLogin" element={<KakaoLogin />} />
                <Route path="*" element={<Navigate to="/" />} />
            </Routes>
        </div>
    );
}

export default App;

/kakaoLogin 으로 접속하면 KakaoLogin 이라는 컴포넌트를 보여주도록 했다.

그래서 KakaoLogin.js 를 만들었다.

// KakaoLogin.js

import React, { useEffect, useState } from "react";
import { useRecoilState } from "recoil";
import { userInfoState } from "../../recoil/atoms/userState";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./KakaoLogin.css";

function KakaoLogin() {
    const [userInfo, setUserInfo] = useRecoilState(userInfoState);
    const navigate = useNavigate();
    const PARAMS = new URL(document.location).searchParams;
    const KAKAO_CODE = PARAMS.get("code");
    const [accessTokenFetching, setAccessTokenFetching] = useState(false);

    console.log("KAKAO_CODE:", KAKAO_CODE);

    // Access Token 받아오기
    const getAccessToken = async () => {
        if (accessTokenFetching) return; // Return early if fetching

        console.log("getAccessToken 호출");

        try {
            setAccessTokenFetching(true); // Set fetching to true

            const response = await axios.post(
                "~~~/api/auth/kakao",
                {
                    authorizationCode: KAKAO_CODE,
                },
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                }
            );
            const accessToken = response.data.accessToken;
            console.log("accessToken:", accessToken);

            setUserInfo({
                ...userInfo,
                accessToken: accessToken,
            });

            setAccessTokenFetching(false); // Reset fetching to false
            navigate("/");
        } catch (error) {
            console.error("Error:", error);
            setAccessTokenFetching(false); // Reset fetching even in case of error
        }
    };


    useEffect(() => {
        if (KAKAO_CODE && !userInfo.accessToken) {
            getAccessToken();
        }
    }, [KAKAO_CODE, userInfo]);


    return (
        <div>
           Loading...
        </div>
    );
}

export default KakaoLogin;

redirect 되어서 /kakaoLogin 으로 접속하게 되면

redirect url 의 맨 뒤에 code=인가코드 이런식으로 인가코드가 오는 것을 확인할 수 있다.

이 인가코드를 주소에서 뽑은 다음에 백엔드에게 전달해주면 되는 것이다.

 

accessToken 을 받아온 후 원하는 페이지('/')로 이동하도록 했다


인가코드 주소에서 뽑아내기

    const PARAMS = new URL(document.location).searchParams;
    const KAKAO_CODE = PARAMS.get("code");

위와 같이 현재 URL에서 .get("code") 를 해보면 code 에 해당하는 값만 뽑혀서 KAKAO_CODE 에 저장되는 것을 확인할 수 있다.

이 KAKAO_CODE 를 백엔드에게 전송한다.

 

나의 경우 authorizationCode 의 값으로 KAKAO_CODE 를 전송했다.


사용자의 정보도 받아오기

로그인을 하면서 사용자의 정보도 받아와서 프로필을 보여줘야했기에

나는 로그인 과정에 사용자 정보를 받아오는 부분도 추가했다.

이 부분은 getProfile 이라는 함수에서 진행된다.

 

사용자 정보를 받아오는 api 를 호출하려면 accessToken 이 필요하기 때문에,

getAccessToken 함수를 실행시키고 난 후, getProfile 함수를 실행하도록 했다.

getProfile 이 포함된 전체 코드는 다음과 같다.

import React, { useEffect, useState } from "react";
import { useRecoilState } from "recoil";
import { userInfoState } from "../../recoil/atoms/userState";
import { useNavigate } from "react-router-dom";
import axios from "axios";
import "./KakaoLogin.css";

function KakaoLogin() {
    const [userInfo, setUserInfo] = useRecoilState(userInfoState);
    const navigate = useNavigate();
    const PARAMS = new URL(document.location).searchParams;
    const KAKAO_CODE = PARAMS.get("code");
    const [accessTokenFetching, setAccessTokenFetching] = useState(false);

    console.log("KAKAO_CODE:", KAKAO_CODE);

    // Access Token 받아오기
    const getAccessToken = async () => {
        if (accessTokenFetching) return; // Return early if fetching

        console.log("getAccessToken 호출");

        try {
            setAccessTokenFetching(true); // Set fetching to true

            const response = await axios.post(
                "~~~/api/auth/kakao",
                {
                    authorizationCode: KAKAO_CODE,
                },
                {
                    headers: {
                        "Content-Type": "application/json",
                    },
                }
            );
            const accessToken = response.data.accessToken;
            console.log("accessToken:", accessToken);

            setUserInfo({
                ...userInfo,
                accessToken: accessToken,
            });

            setAccessTokenFetching(false); // Reset fetching to false
            getProfile();
        } catch (error) {
            console.error("Error:", error);
            setAccessTokenFetching(false); // Reset fetching even in case of error
        }
    };

    const getProfile = async () => {
        try {
            console.log("getProfile 호출");
            // Check if accessToken is available
            if (userInfo.accessToken) {
                console.log("accessToken in getProfile:", userInfo.accessToken);
                const response = await axios.get(
                    "~~~/users/profile",
                    {
                        headers: {
                            Authorization: `${userInfo.accessToken}`,
                        },
                    }
                );
                console.log("message:", response.data.message);
                setUserInfo({
                    ...userInfo,
                    id: response.data.result.id,
                    name: response.data.result.name,
                    email: response.data.result.email,
                    nickname: response.data.result.nickname,
                    profileImage: response.data.result.profile_image_url,
                    isLogin: true,
                });
                navigate("/");
            } else {
                console.log("No accessToken available");
            }
        } catch (error) {
            console.error("Error:", error);
        }
    };

    useEffect(() => {
        if (KAKAO_CODE && !userInfo.accessToken) {
            getAccessToken();
        }
    }, [KAKAO_CODE, userInfo]);

    useEffect(() => {
        if (userInfo.accessToken) {
            getProfile();
        }
    }, [userInfo]);

    return (
        <div>
            <div class="spinner center">
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
                <div class="spinner-blade"></div>
            </div>
        </div>
    );
}

export default KakaoLogin;

사용자 정보를 받아오는 것 까지 완료하면 마찬가지로 원하는 페이지('/') 로 이동하도록 구현했다.

 


만났던 오류들

일단 api 요청이 두 번 가면 동일한 code 를 가지고 두번 보내는 건데,

code 를 두번 사용하면 유효하지 않다.

근데 내꺼는 자꾸 api 요청이 두번 간다고 뜨는 문제가 생겼었다.

관련한 오류를 어떻게 해결했는지는 아래에 정리했다.
https://dawonny.tistory.com/404

 

[오류] axios 중복 호출

문제 axios 로 post 요청을 보내는데 자꾸 api 호출을 두번하는 문제가 생겼다. 다른 api 면 모르겠지만 내가 사용하는 api 는 두번 호출하면 중복된 code 여서 유효한 code 로 동작하지 않았기 때문에 문

dawonny.tistory.com

 

그리고 나는 accessToken 을 받아온 뒤에 사용자 정보까지 이어서 받아와야하기 때문에

함수가 실행되는 순서가 중요했다.

신경쓰지 않고 코드를 짰다간 accessToken 이 없어서 사용자 정보를 못 불러온다는 오류가 떴다.

 

그래서 useEffect 의 의존성 배열 부분을 잘 작성해서, accessToken 이 업데이트 되면 그 때 getProfile 이 실행되도록 했고,

accessToken 을 fetching 하는 동안에는 다른 것들이 이루어지지 않도록 하려고 

accessTokenFetching 이라는 상태를 관리했다.

일반적인 방법인지는 모르겠지만,,,(피드백 환영)

 


Redirect 화면

카카오 로그인이 순식간에 일어나지만

그래도 하얀 화면에 'Loading...' 만 보여주기 뭐해서 아래의 사이트를 참고해 Loading 애니메이션을 추가했다.

https://uiverse.io/loaders 

 

554 Loaders made with CSS or Tailwind

Enhance your app's loading experience with a diverse collection of open-source loaders from Uiverse. Explore various styles, including spinners, progress bars, and animations, to create an engaging, user-friendly interface for web and mobile applications.

uiverse.io

다른 component 들도 많으니 참고하시면 좋을 것 같다.

728x90
반응형