이번 프로젝트 로그인 부분을 내가 담당하게 됐는데,
로그인 방식 중에서 카카오 로그인을 구현하게 됐다.
리액트 카카오 로그인에 관련해서 정리된 글들이 많아
개발할 때 참고하면서 진행할 수 있었다.
하지만 까먹지 않고자 한번 정리하려 포스팅해본다.
방식
프론트엔드와 백엔드가 협업하면서 카카오 로그인을 구현하는 방식이 다를 수 있다.
나는 이런 방식으로 했다- 라는 정도로 참고하시면 좋을 것 같다.
나는 프론트엔드 담당이어서 프론트엔드 입장에서 서술해보려고한다.
- 프론트엔드에서 카카오 로그인 요청 / 인가 코드 받기 요청을 한다
- 카카오에서 redirect url 로 인가코드를 프론트엔드로 보내준다.
- 받은 인가코드를 백엔드에게 보낸다.
- 백엔드는 이 인가코드를 받아 처리해서 AccessToken 을 응답으로 프론트엔드에게 보내준다.
- 프론트엔드는 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
그리고 나는 accessToken 을 받아온 뒤에 사용자 정보까지 이어서 받아와야하기 때문에
함수가 실행되는 순서가 중요했다.
신경쓰지 않고 코드를 짰다간 accessToken 이 없어서 사용자 정보를 못 불러온다는 오류가 떴다.
그래서 useEffect 의 의존성 배열 부분을 잘 작성해서, accessToken 이 업데이트 되면 그 때 getProfile 이 실행되도록 했고,
accessToken 을 fetching 하는 동안에는 다른 것들이 이루어지지 않도록 하려고
accessTokenFetching 이라는 상태를 관리했다.
일반적인 방법인지는 모르겠지만,,,(피드백 환영)
Redirect 화면
카카오 로그인이 순식간에 일어나지만
그래도 하얀 화면에 'Loading...' 만 보여주기 뭐해서 아래의 사이트를 참고해 Loading 애니메이션을 추가했다.
다른 component 들도 많으니 참고하시면 좋을 것 같다.