Front : React + Next.js + TypeScript
Back : Spring
카카오 로그인 적용 이유?
: (진행하고 있는 영화 커뮤니케이션 프로젝트에서) 등록된 리뷰들은 로그인 없이도 볼 수 있게 하고, 리뷰 등록/수정/삭제는 로그인을 해야만 가능하도록 하기 위해 카카오 로그인을 적용하고자 했다.
먼저 카카오 로그인을 적용했을 때의 과정은 다음과 같다. (REST API 활용)
[ 카카오 ]
카카오 로그인 요청 → 인가 코드 받기 → 카카오 로그인 토큰 발급 → 토큰으로 카카오 유저 정보 받기 →
[ 개인 서비스 ]
개인 서비스 내에서 유저 조회 → 기존 유저일 경우: 로그인 진행 / 신규 유저일 경우: 유저 등록(회원가입) 후 로그인 진행
[ 카카오 및 개인 서비스 로그아웃 ]
카카오 로그아웃 + 개인 서비스 로그아웃 (토큰 만료 에러 or 재발급 과정)
이 과정에서 '개인 서비스 내에서 유저 조회'와 '로그인(토큰 발급)', '개인 서비스 로그아웃'만 백에서 처리하고, 나머지 과정은 모두 프론트에서 처리하였다.
보통 카카오 로그인 토큰을 발급 받기 위해 인증키(CLINET_ID)를 보내는데, 이 인증키를 숨기기 위해 백에서 해당 과정을 진행하는 것이 좋다고 한다.
하지만 이번 프로젝트에선 카카오 로그인 과정도 경험해볼겸 프론트에서 거의 모든 과정을 처리해보기로 했다.
이 글에서는 카카오 로그인, 서비스 내 로그인, 카카오 로그아웃을 하는 과정을 정리해보겠다.
꽤나 긴 글이 될 것 같다😅
카카오 로그인
: kakao developers에서 CLINET_ID, Redirect URI, 플랫폼 등록하는 과정은 생략하겠다.
1. 카카오 로그인 요청 및 인가 코드 받기
1-1. 카카오 로그인 버튼 생성
// Header.tsx
<Link href={KAKAO_AUTH_URL}>
<Image src={KakaoLogo} alt="카카오 로그인"/>
</Link>
1-2. 직접 지정한 redirect uri (REDIRECT_URI) 로 인가 코드 받기
export const CLIENT_ID = ""
export const REDIRECT_URI = "http://localhost:3006/auth/kakao"
export const KAKAO_AUTH_URL = `https://kauth.kakao.com/oauth/authorize?client_id=${CLIENT_ID}&redirect_uri=${REDIRECT_URI}&response_type=code`
정리해 보자면,
- 로그인 버튼 클릭 시, KAKAO_AUTH_URL로 이동 (아래 화면 접근)
- 위 화면에서 이메일&비밀번호를 입력
- 필요한 동의항목 체크
- REDIRECT_URI로 이동 (여기서 REDIRECT_URI는 본인이 직접 지정한 경로이다. 나는 auth/kakao로 지정했다.)
- REDIRECT_URI(=kakao.tsx)에서 카카오 로그인 및 서비스 회원가입 및 로그인 과정 진행
// kakao.tsx
const Kakao = () => {
// 카카오 로그인 과정
// 서비스 회원가입 및 로그인 과정
return (
<div className="spinner">
<Image src={Spinner} alt="로딩중" />
<h2>잠시만 기다려 주세요!<br/>로그인 중입니다.</h2>
</div>
)
}
→ 이 과정들을 겪을 때 spinner를 표시해, 모든 과정이 완료될 때까지 (사용자가) 빈 화면을 보는 것을 방지했다.
참고로 인가코드 발급을 위해 카카오 페이지로 이동 후, 리다이렉트하는 이유는 Ajax 방식으로 요청했을 경우 CORS 에러가 발생하기 때문이다. CORS(교차 출처 리소스 공유)는 HTTP 헤더를 사용하여 실행중인 웹사이트가 다른 도메인의 자원에 접근할 수 있도록 브라우저에 알려주는 체제이다. 즉, A도메인의 프론트엔드 스크립트가 B도메인을 호출하면 브라우저에서는 기본적으로 요청을 제한한다.
따라서 Ajax 방식으로 요청하면 안되고, a태그로 이동해야 한다.
(* 인가 코드 요청은 CORS가 닫혀 있지만, 카카오 로그인 토큰 발급은 CORS가 열려 있어서 Ajax 요청이 가능하다.)
2. 인가 코드로 카카오 로그인 토큰 발급 (/oauth/token)
- REDIRECT_URI의 뒤에는 ?code=####~## 이런 식으로 인가 코드가 넘어옴 (ex. http://localhost:3006/auth/kakao?code=####~##)
useSearchParams
로 code 값을 가져와 변수에 저장- 필요로 하는 파라미터(+인가코드(=code) 포함)를 실어서 POST 요청(/oauth/token)
- access_token 및 refresh_token 등 받기
const fnGetKakaoOauthToken = async () => {
const makeFormData = (params: {[key: string]: string}) => {
const searchParams = new URLSearchParams()
Object.keys(params).forEach(key => {
searchParams.append(key, params[key])
})
return searchParams
}
try {
const res = await axios({
method: 'POST',
headers: {
'content-type': 'application/x-www-form-urlencoded;charset=utf-8',
},
url: 'https://kauth.kakao.com/oauth/token',
data: makeFormData({
grant_type: 'authorization_code',
client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI,
code // 인가 코드
})
})
// sessionStorage/localStorage에 결과값 저장
// state에 kakao accesstoken 저장
} catch (err) {
console.warn(err)
}
}
( 출처 : https://kakao-tam.tistory.com/59 )
3. 카카오 유저 정보 받기 (/v2/user/me)
- 2번에서 받은 access_token을 header에 실어서 GET 요청
- 현재 카카오 로그인한 유저의 정보를 받을 수 있음
const fnGetKakaoUserInfo = async () => {
try {
const res = await axios({
method: 'GET',
headers: {
"Authorization": `Bearer ${kakaoAccessToken}` // 카카오 토큰 api로 얻은 accesstoken 보내기
},
url: "https://kapi.kakao.com/v2/user/me",
})
// sessionStorage/localStorage에 사용자 정보 저장
fnUserInfoCheck(res.data.id.toString(), res.data.kakao_account.profile.nickname) // 서비스 내 유저 조회를 위해 kakaoId, nickname 전달
} catch (e) {
console.log('e : ', e)
}
}
개인 서비스 로그인
개인 서비스 로그인 과정은 다음과 같이 설계했다.
- 개인 서비스 내에서 유저가 존재하는지 조회한다.
- 카카오 유저 정보 중 id 값 활용 (GET 요청 시, id를 parameter로 보냄)
- 만약 기존 유저일 경우 로그인 과정(refresh token + access token 발급)을 겪는다.
- 카카오 유저 정보 중 nickname 값 활용 (POST 요청 시, nickname을 parameter로 보냄)
- 신규 유저일 경우 id 값과 nickname 값을 데이터베이스에 저장하여, 유저 등록(=회원가입) 후 로그인 과정을 겪도록 한다.
- 카카오 유저 정보 중 id, nickname 값 활용 (POST 요청 시, id와 nickname을 parameter로 보냄)
카카오 및 개인 서비스 로그아웃
카카오 로그아웃에는 '로그아웃', '카카오 계정과 함께 로그아웃' 총 2가지 종류가 있다.
여기서 '로그아웃'을 적용해보기로 했다.
카카오 및 개인 서비스 로그아웃 과정은 다음과 같이 설계했다.
- 로그아웃 버튼 클릭 시, 카카오 로그아웃을 진행한다. (카카오 유저의 access token 과 refresh token 모두 만료시킴)
- 로그인이 필요한 서비스를 이용하고자 할 때, 개인 서비스 token 만료 여부를 체크해
- access token이 만료되었으면, (자동으로) 재발급을 진행하고
- refresh token이 만료되었으면, 재로그인이 필요함을 알리고 로그인 페이지로 튕긴다.
카카오 로그아웃
export const kakaoLogout = () => {
axios({
method: 'POST',
url: 'https://kapi.kakao.com/v1/user/logout',
headers: {
"Content-Type": "application/x-www-form-urlencoded",
"Authorization": `Bearer ${카카오 로그인 access token 값}`
},
}).then(() => {
window.location.href = '/'
}).catch((e) => {
console.log('e : ' , e)
// 이미 만료된 토큰일 경우
if (e.response.data.code === -401) {
window.location.href = '/'
}
})
}
→ 이미 만료된 토큰으로 로그아웃 요청을 했을 경우, 에러로 화면이 멈추는 것을 방지하기 위해 (해당하는 에러 코드인) -401으로 조건을 주어 로그인 화면으로 튕겨내는 로직을 추가하였다.
개인 서비스 로그아웃
- 로그인이 필요한 서비스(리뷰 등록/수정/삭제)의 api option에
private: true
를 추가해 놓았다. - 위 api를 요청했을 경우 (개인 서비스의) access-token이 만료되었는지 체크하고
만료되었으면 token을 재발급하고, 유효하면 header에 access token을 실어 보낸다.
// api 요청 함수
export const GetApi = async ($api: Api, $param?: object) => {
// ... 코드 생략 ...
if ($api.private) {
await fnAuthCheck()
option.headers = { 'Authorization': 'Bearer ' + accessToken 값 }
}
// ... 코드 생략 ...
}
// 토큰 만료 체크 및 토큰 재발급 요청 함수
const fnAuthCheck = async () => {
if(!tokenExpireCheck()) {
await getAuthenticate() // 토큰이 만료되었을 경우, 토큰 재발급 api 요청
}
}
// access token 만료 체크 함수
export const tokenExpireCheck = () => {
const $t = Data.get('login') // 개인 서비스 토큰(refresh, access) 값
let $tData
if ($t !== 'undefined' && $t !== null) {
const now = moment().format('X') // moment 라이브러리 사용해 현재 시간 가져옴
$tData = parseJwt($t.accessToken)
return now < $tData.exp
} else {
return false
}
}
- access token 만료 여부는 프론트에서도 체크할 수 있기 때문에 설계한 로직대로 동작하였는데
refresh token 만료 여부는 프론트에서 체크하기가 어려웠다.
따라서 백에서 refresh token이 만료되었을 경우, 협의한 특정 errorCode를 보내주면 그것으로 구분하여 재로그인이 필요함을 알리고 로그인 페이지로 튕기도록 코드를 짰다.
export const GetApi = async ($api: Api, $param?: object) => {
// ... 코드 생략 ...
if ($api.private) {
await fnAuthCheck()
option.headers = authHeader()
}
return await axios(option)
.then((res) => {
return res.data
}).catch((err) => {
if (err.response.data.errorCode === "INTERNAL_SERVER_ERROR" || err.response.data.errorCode === "EXPIRED_TOKEN") {
sAlert({ // sweetalert 라이브러리 사용
html: '로그인 대기 유효 시간이 만료 되었습니다.<br>다시 로그인 시도해 주시기 바랍니다.',
didClose: () => {
kakaoLogout()
}
})
} else {
sAlert({
text: err.response.data.errorMessage ? err.response.data.errorMessage : err.response.data.message,
icon: 'error'
})
}
return 'FAIL'
})
}
→ err.response.data.errorCode === "INTERNAL_SERVER_ERROR"
과 err.response.data.errorCode === "EXPIRED_TOKEN"
이 token 관련 에러가 발생했을 때 백에서 던져주는 errorCode이다. 이 부분은 백엔드와 협의해서 정하면 된다.
카카오 로그인이 다른 소셜 로그인 중에서 가장 쉽다고 하는데 처음 적용해보는 터라 쉽지 않게 느껴졌다..
하나하나 추가하는 과정에서 접한 CORS 에러, header에 실어보낼 것들,
refresh token과 access token 개념, 프론트에서 해야 하는 범위, 토큰 만료 시 어떤 식으로 화면을 설계해야 사용자 입장에서 불편함을 느끼지 않을 수 있을지 등등 고민하느라 적용하기까지 시간이 오래걸렸던 것 같다.
완벽하진 않지만 결국엔 카카오 로그인을 적용시킬 수 있어서 뿌듯했다😭
이 글에서 보인 코드는 아래 링크에서 볼 수 있다! (많이 부족ㅎㅎ)
https://github.com/MovieApplication/cinemate
댓글