Front-end/React

[TIL] 카카오 소셜 로그인 구현하기

성중 2022. 11. 29. 17:13

카카오 소셜 로그인은 구현이 어렵지 않고 친숙한 소셜 로그인 중 하나이다. 최근 자체 회원가입 없이 소셜 로그인 만을 제공하는 서비스가 늘어나고 있으며 사용자 입장에서도 부담 없이 서비스를 이용할 수 있어 간단한 서비스는 대부분 소셜 로그인을 채택하는 추세이다

 

그 중에서도 가장 보편적인 React + REST API 환경에서 로컬스토리지와 JWT 토큰을 활용해 카카오 소셜 로그인을 구현한 경험을 블로그에 남겨보도록 하겠다

 

우선 kakao developers 사이트에서 몇 가지 설정을 해준 후 클라이언트단에서 인가 코드를 추출해 서버로 전달해줘야 한다. 해당 과정까지의 설명은 카카오 공식 문서와 아래 글에 상세하게 정리되어 있다

 

[React] 카카오 로그인 구현하기 - REST API🔽

 

[React] 카카오 로그인 구현하기 - REST API

지금 진행하고 있는 프로젝트에서 카카오 로그인과 구글 로그인을 진행하기로 하였다. 먼저 카카오 로그인에 대해 정리하면서 진행해보려고 한다. 아, 그리고 이 글은 프론트엔드의 입장에서

velog.io

 

클라이언트단에서 카카오 토큰까지 받아 서버로 전달하는 경우도 있지만 보통 클라이언트는 인가 코드까지만 추출해 서버로 보내준다. 서버는 인가 코드를 사용해 카카오 서버로부터 엑세스 토큰을 받아오고 이를 기반으로 서비스에서 사용할 JWT 토큰과 리프레시 토큰을 자체적으로 생성한다

* 카카오 서버의 엑세스 토큰은 서비스에서 그대로 사용 불가

 

[src/api/authApi.ts]

...
  async getServiceToken(code: string) {
    try {
      const res = await fetcher(METHOD.GET, "v1/auth/kakao", {
        params: { code },
      });
      return res.headers["authorization"].split(" ")[1];
    } catch {
      throw ERROR.API;
    }
  },
...

인가 코드를 GET 요청 쿼리 파라미터로 전달하고 서버에서 응답 헤더에 담아준 JWT 토큰을 받는다

* fetcher는 임의로 정의한 함수로 내부적으로 axios를 사용

 

[src/components/auth/Kakao.tsx]

...
  const kakaoLogin = async () => {
    if (code) {
      try {
        const serviceToken = await authApi.getServiceToken(code);
        localStorage.setItem(TOKEN, serviceToken);
        setUser({ ...user, isLoggedIn: true });
      } catch (error) {
        alert(error);
      }
    } else alert(ERROR.KAKAO_CODE);
    navigate("/");
  };
...

받아온 JWT 토큰을 로컬스토리지에 저장하고 사용하는 전역 상태관리 라이브러리로 사용자의 로그인 상태를 즉시 업데이트 해준다

 

[src/utils/logout.ts]

...
const logout = () => {
  localStorage.removeItem(TOKEN);
  location.reload();
};
...

로그아웃은 단순히 로컬스토리지를 비우고 새로고침하는 것이다

 

성공적으로 로컬스토리지에 JWT 토큰을 저장했다면 클라이언트단에서 수시로 JWT 토큰의 유효성을 검증해줘야 한다. 보안상 JWT 토큰의 유효기간은 아주 짧게 잡기 때문에 서버의 리프레시 토큰을 함께 활용해 JWT 토큰을 교체하고, 리프레시 토큰까지 만료되었을 때 사용자의 로그인 상태를 만료(강제 로그아웃)시킬 것이다

 

1. 클라이언트에서 로컬스토리지에 저장된 JWT 토큰을 GET 요청 헤더에 넣어서 전달
2. 서버에서 JWT 토큰의 유효기간을 확인

A. 유효기간이 지나지 않은 경우 응답을 보내 로그인 상태 유지 (OK)
B. 유효기간이 지난 경우 서버의 리프레시 토큰 확인
 
B - 1. 서버의 리프레시 토큰이 유효한 경우 응답으로 새로운 JWT 토큰 반환 및 로그인 상태 유지 (RENEW)
B - 2. 서버의 리프레시 토큰이 유효하지 않은 경우 로그아웃 (EXPIRED)

 

JWT 토큰의 유효기간과 리프레시 토큰의 유효기간에 따라 나올 수 있는 경우는 위와 같다

 

{
  // `data`는 서버가 제공한 응답(데이터)입니다.
  data: {
    status: "OK",
  },
};

{
  data: {
    status: "RENEW",
  },

  // `headers` 서버가 응답 한 헤더는 모든 헤더 이름이 소문자로 제공됩니다.
  headers: {
    Authorization: `Bearer ${token}`,
  },
};

{
  data: {
    status: "EXPIRED",
  },
};

서버 개발자와 각각의 경우에 따른 axios 응답을 정의했다

 

[src/api/authApi.ts]

...
  async isTokenValid(token: string) {
    try {
      const res = await fetcher(METHOD.GET, "v1/auth/token", {
        headers: {
          Authorization: `Bearer ${token}`,
        },
      });
      return res;
    } catch {
      throw ERROR.TOKEN;
    }
  },
...

GET 요청 헤더에 JWT 토큰을 담아 전송해준다

 

[src/components/Router.tsx]

...
const Router = () => {
  const [user, setUser] = useRecoilState(userState);

  const checkStorageToken = async () => {
    const token = localStorage.getItem(TOKEN);
    if (!token) return setUser({ ...user, isLoggedIn: false });

    try {
      const res = await authApi.isTokenValid(token);

      switch (res.data.status) {
        case "OK":
          setUser({ ...user, isLoggedIn: true });
          break;
        case "RENEW": {
          const serviceToken = res.headers["authorization"].split(" ")[1];
          localStorage.setItem(TOKEN, serviceToken);
          setUser({ ...user, isLoggedIn: true });
          break;
        }
        case "EXPIRED":
          logout();
          break;
      }
    } catch (error) {
      logout();
      alert(error);
    }
  };

  useEffect(() => {
    checkStorageToken();
    addEventListener("storage", checkStorageToken);
    return () => removeEventListener("storage", checkStorageToken);
  }, []);

  return (
    <BrowserRouter>
      <Suspense fallback={<Loading />}>
        <Routes>
          {user.isLoggedIn ? (
            <Fragment>
              <Route path={PATH.HOME} element={<Home />} />
              <Route path={PATH.INFO} element={<Info />} />
            </Fragment>
          ) : (
            <Fragment>
              <Route path={PATH.HOME} element={<Auth />} />
              <Route path={PATH.KAKAO} element={<Kakao />} />
            </Fragment>
          )}
          <Route path={PATH.ALL} element={<Navigate to={PATH.HOME} replace />} />
        </Routes>
      </Suspense>
    </BrowserRouter>
  );
};
...

토큰 검증 로직은 페이지별 접근 권한을 관리하기 위해 라우터 최상단에서 구현한다. useEffect로 storage에 이벤트리스너를 달아 컴포넌트 마운트 및 로컬스토리지의 모든 변화를 감지해 checkStorageToken 함수가 동작하도록 했다. 해당 함수는 토큰 검증의 3가지 경우에 따라 로그인 상태를 분기 처리해 전역 상태를 업데이트하며 동시에 Router 컴포넌트는 최상단에서 전역 상태로 사용자의 접근 권한을 관리한다

 

전체적으로 이렇게 카카오 소셜 로그인을 구현할 수 있다. 전부 구현한 시점에 글을 한 번에 작성하다 보니 두서가 없고 생략된 부분도 많지만, 그리고 개발 환경과 서비스 조건에 따라 구현 방법이 달라지겠지만, 늘 그렇듯 나를 포함한 누군가에게는 이 글이 도움이 될 것이라고 생각한다🙂

 

Reference🔽

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

 

JWT는 어디에 저장해야할까? - localStorage vs cookie

이번에 지하철 미션을 만들면서 JWT를 클래스 property에 저장했었는데 리뷰어 분께 해당 부분을 피드백 받으면서 어디에 JWT를 저장하는 것이 좋을까 에 대해 고민해보게 되었다. 0. 기본 지식 JWT Js

velog.io