[TIFY 개발일지 #3] react-query로 카카오 로그인 구현하기

김유진·2023년 7월 28일
5

개발을 할 때 넘어야 하는 가장 큰 산은 바로 유저의 인증/인가이다.
리액트를 처음 배웠을 때 카카오 로그인 구현에 아주 큰 상처를 받은 기억이 있기 때문에(ㅠㅠ) 이번에는 제대로 구현하고 싶었다. 그래도 저번에 삽질을 하면서 배웠던 것이 도움이 되었는지, 카카오 로그인 구현의 원리를 제대로 알고 구현할 수 있었던 것 같다. 먼저, 간단하게 카카오 로그인 로직을 살펴보고 가자.

React-Query 세팅하기

React Query를 사용하는 이유

React Query를 사용하게 된 계기는 간단하다.
두둥 프로젝트를 진행하였을 때, 프론트 리드 팀원이 React-Query를 선택하여 무작정 따라하게 되었는데, 데이터가 오래 되었다고 판단되면 다시 데이터를 자동으로 받아와주는 invalidateQueries 기능에 감동을 받아.. 정말 편리하고 혁신적이라고 생각하여 다음 프로젝트에서는 꼭 내 손으로 스스로 세팅하여 사용해봐야겠다는 생각이 들어 이번 프로젝트에 꼭 도입하고 싶었다.

하지만 단순히 간지나니까! 멋있으니까! 이렇게 이유를 말하는 것은 타당하지 못하다.

server state와 client state를 효과적으로 관리할 수 있다.

개발을 할 때 client에서 관리하는 state들이 있다. 작게 시작해서 useState로 관리하는 것부터 시작하여 redux, recoil등으로 관리할 수 있는 데이터들이 있다. 그리고 client의 요청에 따라서, 아니면 서버가 자체적으로 생성해내서 client에게 넘겨주는 서버의 state가 존재한다.

서버에서 넘어오는 state를 client state로 어떻게 관리할 것이며, 기존에 존재하는 client state를 잘 가공할 것인지 설계하는 것이 프론트엔드 개발자의 소양이라고 할 수 있다. 만약 react-query를 이용하지 않았더라면 이 두 가지 정보를 뒤죽박죽 섞어 사용하기도 하고, 로직처리가 복잡해질 수 있다. 하지만 이러한 문제를 react-query는 간단하게 해결해버린다.

hook 기반으로 동작한다.

상태 관리 도구인 redux, mobx와 같은 도구들은 사실, 배울 것이 너무 많다 (..) 현재 React에 대해 완벽하게 다 공부했다고 하기도 벅찬 상태인데, 이 친구들과 함께 딸려오는 부가적인 개념들까지 완벽하게 흡수하기가 어렵다.

하지만, React query는 hook을 기반으로 동작하는 로직으로 되어 있어서, 현재 함수형 컴포넌트에서 상태 관리나 변경을 직관적으로 쉽고 빠르게 할 수 있다는 것이 장점이다.

캐시 관리

아직 개발에 대해서 깊게 공부하지 않았던 시절, 서버에게 api 받아오는 것도 어려운데 내가 받아온 state 의 유효성 검증까지 하라고 그러면 막막했었다. 하지만 지금 react-query는 매우 쉽게 내가 표현하고 있는 데이터가 유효하지 않은, refetching이 필요한 데이터라고 쉽게 관리할 수 있다. 앱인 만큼 유저의 인증/인가 및 네트워크 상태를 쉽게 관리하기 위해서는 이보다 더 좋은 툴이 있을까! 라는 생각이 들어, 대담하게 도전해보기로 하였다.

staleTime, cacheTime 설정하기

먼저 useQueryClient 를 이용하여 전역에 사용할 쿼리를 세팅해주어야 한다. 가장 먼저 세팅을 하면서 알아야 하는 것은 아래와 같다.

기본값이 어떻게 세팅되어 있는가???

react-query에서 staleTimecacheTime에 대한 개념 없이 막 사용하다 보면, 어 왜 fetch가 안돼!!! 하면서 소리를 지를 수 있는 불상사가 발생할 수 있기 때문에 .. 필요한 부분은 먼저 세팅을 해야 한다고 생각하게 되었다.
항상 staleTimecacheTime 의 차이를 잘 인지하지 못하고 있었기 때문에 이번 기회에 확실히 정리해야겠다는 생각이 들기도 했던 것 같다.

다양한 블로그 글과 공식문서를 찾아보았지만, 결국.. staleTime이나 cacheTime 둘 다 설정해둔 시간이 지나지 않으면 fetch를 수행하지 않는다는 거 아닌가? 왜 굳이 이렇게 분리해두었지???라는 생각이 들면서 이해가 가지 않아 뚱한 얼굴로 여러 글을 찾아보았다. 그러다가 머리를 탁 치는 챗 gpt의 답변을 얻을 수 있었는데..

Imagine you have a magic drawer where you can keep your favorite toys and snacks (data). This drawer is like the cache in React Query. When you put something in the drawer, you can quickly access it later without going anywhere else (like fetching data from a server).

여기서 이야기하기를, cacheTime은 내가 서랍에 얼마나 간식을 보관할지 결정하는 것이고, staleTime은 서랍에 있는 간식을 더 이상 사용할 수 없는 시간을 정하는 것 이라고 설명하였다. 이렇게 해석해 보는 관점에서 접근하니 두 가지가 명확하게 다르다는 것을 깨달을 수 있었다.

만약, cacheTime을 1일이라고 설정하면, 나는 하루 동안 간식을 다시 사러 가는 행위 (fetch)를 하지 않고, 서랍에서 간식을 계속 꺼내 쓸 수 있다. 하지만, 하루가 지나고 나면 나는 간식을 사러 편의점에 다녀오는 행동을 해야 한다.

staleTime도 이와 같이 생각해보자. 내가 서랍에 넣어 둔 간식의 유통기한이 10분이다. 10분 동안은 서랍에 있는 간식을 계속 꺼내 먹어도 문제가 없지만, 10분이 지나게 되면 신선한 간식을 구매하러 다시 사러 가야(fetch) 한다. 또 그 간식을 먹고 싶으면 어쩔 수 없겠지!

잠깐!!! 또 이해가 안되는데?
그럼 staleTime은 10분, cacheTime은 하루라고 했을 때, 내가 먹고 있는 간식 유통기한이 10분이니까 어짜피 하루라는 시간 동안 보관한다고 해도, 상했으니까 다시 사와야 하는 거 아냐?? 그럼 cacheTime을 설정하는 의미가 없는데..

과거의 나는 여기까지만 생각하고 다음에 공부해야지~ 하고 미뤘지만, 이제는 더이상 물러날 곳이 없다. 완벽히 이해하고 넘어가자.

카카오 로그인 로직

TIFY 서비스에서 카카오 로그인을 하는 로직은 다음과 같다.
1. 프론트에서 카카오 로그인 버튼을 눌러 로그인 요청을 보내는 순간 인가코드에 대한 요청을 보낸다. 그리고 redirect 페이지로 이동한다.
2. 프론트에서 인가 코드를 파싱하여 백엔드에게 전송한다. 인가 코드로 토큰 발급 요청을 하는 것이다. 이 요청에 대한 결과로 백엔드는 accessToken , refreshToken, idToken을 보내준다. 여기서 idToken은 카카오 로그인 사용자의 인증 정보를 제공하는 토큰이다.

이제 여기까지만 하면 카카오를 통한 로그인 로직은 완료된다. 이제, TIFY 서비스에 가입해야 한다.

현재 가지고 있는 idToken을 기반으로 현재 로그인한 사람이 기존에 가입을 했는지, 신규 가입인지 알아보아야 한다.
신규 등록이 가능한 회원인지 아닌지에 대한 검증을 idToken을 통하여 마친 이후, 백엔드에서 true로 답변해주면 register API를 통하여 신규 회원으로 등록해주고, userId를 발급해준다. 만약 false 이면 단순 로그인 처리를 하여 로그인 하는 동안 사용할 토큰을 발급해준다.

직접 구현해보자!

먼저, 요청을 보내는 axois 에 대한 모듈을 생성해 주어야 한다.

import axios from 'axios';
export const BASE_URL = 'http://xxx.xxx.xxx/api'

export const axiosApi = axios.create({
    baseURL: BASE_URL,
    headers: { 'Content-Type' : 'application/json' }
});

미리 이렇게 axios 모듈을 만들어 놓고 시작하면 일일이 baseURLheader를 넣어주지 않아도 되어서 좋다.

로그인 버튼을 누르면, 인가 코드 요청을 하자.

const Login = () => {
    const kakaoLogin = async () => {
        const data = await AuthApi.KAKAO_LINK();
        window.location.href = data.link;
    }
    return (
        <div>
            <Button variant="kakao" onClick={kakaoLogin}>카카오로 로그인하기</Button>
        </div>
    );
};

export default Login;

로그인을 진행하는 로그인 페이지이다. 인가 코드 발급을 요청하는 KAKAO_LINK api가 호출된다.

    KAKAO_LINK: async() => {
        const response = await axiosApi.get('/auth/oauth/kakao/link')
        return response.data.data
    },

해당 API를 호출하고 나면 redirect url로 자동으로 전환된다. 그럼 Routing을 맞춰 놓은 Redirect 컴포넌트로 이동하여 인가 코드를 받은 이후 어떻게 해야 하는지 살펴보자.

인가 코드를 처리해보자.

받아온 인가 코드는 url이고, code 라는 쿼리 스트링에 저장되어 있으므로, 이를 파싱해와야 한다.

const query = useLocation().search
const code = new URLSearchParams(query).get('code');

그럼 code에 인가 코드가 담겼으므로, mutation을 이용하여 요청을 보내 보자.
그 중에서 현재 필요한 것은 인가 코드를 바탕으로 세션 토큰을 발급받는 것이므로, 상황에 맞는 mutation을 작성해보자

const kakaoTokenMutation = useMutation(AuthApi.KAKAO_TOKEN, {
  onSuccess: (data: KakaoCodeResponse) => {
    setToken(data);
  },
});

useEffect(() => {
  if (code) {
    kakaoTokenMutation.mutate(code);
  }
}, [code]);

이렇게 토큰을 받아올 수 있는 api를 호출해주자.

KAKAO_TOKEN: async(code: string) => {
  const response = await axiosApi.get(`/auth/oauth/kakao?code=${code}`);
  return response.data.data;
},

이제 response data로 세션을 이용할 수 있는 토큰들이 발급될 예정이다.( accessToken, refreshToken, idToken )

TIFY 서비스 로그인 처리

현재 로그인을 api 호출 로직을 편하게 관리하기 위하여 mutation 함수를 custom hook으로 분리하여 저장해두었다.

const useAuthMutate = ({idToken}: KakaoCodeResponse) => {
    //회원가입 mutation
    const ouathKakaoRegisterMutation = useMutation(AuthApi.KAKAO_REGISTER, {
        ...
    })

    //로그인 mutation
    const ouathKakaoLoginMutation = useMutation(AuthApi.KAKAO_LOGIN, {
       ...
    })

이제 발급된 idToken을 기준으로 이미 TIFY 서비스에 회원가입 한 적 있는 유저인지 아닌지 구별할 수 있어야 한다.
만약, 현재 전역 상태에 idToken이 존재한다면 이 유저가 로그인을 시도하고 있다는 것이므로, 만약 idToken의 길이가 0보다 크다면 이미 가입한 유저인지 아닌지 관리할 수 있는 valid mutation을 실행시켜주자. 우리가 필요한 것은 현재 검증과 관련한 뮤테이션이므로 구조 분해 할당을 이용하여 해당 함수를 불러오자.

const { ouathValidMutation } = useAuthMutate(token)
useEffect(() => {
  if(token.idToken.length > 0){
    ouathValidMutation.mutate(token.idToken)
  }
}, [token])

이제 valid를 체크할 수 있는 회원가입 검증 여부 뮤테이션 구현부로 넘어가보자.

const ouathValidMutation = useMutation(AuthApi.KAKAO_VALID, {
  onSuccess: (data: { canRegister : boolean }) => {
    //너 회원가입 해야 겠는걸? 
    if (data.canRegister){
      ouathKakaoRegisterMutation.mutate({idToken, payload:{
        email:"abc:@example.com",
        profileImage:"http://aaa",
        name:"김예시",
        phoneNumber:"010-123-456"
      }})
    } else {
      //그냥 로그인 하셈  
      ouathKakaoLoginMutation.mutate(idToken)
    }
  }
})

ouathValidMutation 을 통하여 받아온 register 가능 여부가 true라면, 예시로 적어둔 payload 정보를 보내 주며 회원가입을 시켜주고, 만약 이미 존재하는 회원이라면 그냥 카카오 로그인 처리를 해 준다.

만약, 회원가입과 로그인에 성공하였다면 onSuccessLogin 함수를 실행시켜 주는데,

const onSuccessLogin = (loginData: KakaoLoginResponse) => {
  axiosApi.defaults.headers.common[
    'Authorization'
  ] = `Bearer ${loginData.accessToken}`
  setCookie('refreshToken', loginData.refreshToken, {
    maxAge: 2592000,
    path: '/'
  })
  setCookie('accessToken', loginData.accessToken, {
    maxAge: 20000,
    path: '/'
  })
  setAuth({
    isAuthenticated: true,
    callbackUrl: '/',
    accessToken: loginData.accessToken,
    userId: loginData.userId
  })
}

로그인을 완료하였을 때의 state 를 설정해주는 함수이다.
로그인에 성공하였기 때문에 유저가 검증된 상태이므로,

  • axios 헤더에 access Token 장착
  • refreshToken, accessToken 을 cookie에 저장하여 필요할 때 꺼내 쓸 수 있도록 함
  • 전역 상태관리인 recoil store에 유저의 토큰정보, 유저의 id를 담을 수 있도록 함

이제 이렇게 설정하였으므로, 인증이 필요한 부분에서 ⭐️무적 access header ⭐️ 덕분에 드나들 수 있다.
다음 글에 이어서 유저 로그인 유지, 인증 관리에 대한 글을 써보고자 한다.

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

정보 감사합니다.

답글 달기