<aside> 💡 luvshort 프로젝트를 진행하면서 배웠던 지식, 아쉬웠던 점에 관한 내용입니다.

</aside>

Untitled

배운 지식

  1. Auth 흐름도

느낀점 협업으로써 프로젝트를 만들 때, 로그인 파트를 맡은 적이 없었다. 오직 책으로 가볍게 접했었다. 그래서, ‘회원가입, 로그인, 로그아웃’ 기능 세 개만 만들면 되겠지 가볍게 생각했었다. 그런데, 생각을 하면 할수록 세 개의 기능으론 예측불가한 사용자 경험에서 유저의 로그인 상태를 유지하기는 힘들었다. 그래서 한 개의 기능이 더 필요함을 느꼈다. ‘유저가 현재 로그인중인가 체크’ 하는 기능이 필요했다. 유저가 로그인을 했다면, 유저가 로그인했다는 걸 백엔드에서도 파악할 수 있어야한다. 또한, 브라우저가 종료되면 로그인의 지속상태가 종료되는가, 아니면 브라우저가 종료되어도 로그인의 상태는 계속 유지되고 있는가 등 제작하는 프로젝트의 특성에 맞게 적용해야한다. 방금 말한 내용은 이론으로써만 알고 있었다. 하지만, 이번 프로젝트에서 실제로 적용하면서 Auth의 흐름이 점점 뇌에 익기 시작했다. 넘블이 끝난 이 시점에선 이제 refresh 토큰과 로그인과 관련된 보안을 공부할 계획이다.

실제 코드

로그인 핵심기능

1. 카카오톡에서 가져온 액세스 토큰을 백엔드에 보내기
/*
사실 액세스 토큰을 가져오는 건 백엔드에서 해도 될 역할이였지만, 백엔드님의 역할을 덜 수 있게끔
프론트에서 가져와 백엔드에 전달했다. 기존 방법은 인가코드만 백엔드에 전달하는 것이다.
*/
const sendAccessToken = async function (authObj) {
    let result = await client.post("/api/auth/kakao-login", {
      access_token: authObj.access_token,
    });
    if (result.data.redirectUrl === "/") {
      dispatch(userCheck());
    } else if (result.data.redirectUrl === "/step1") {
      console.log(result.data);
      dispatch(setEmail(result.data.user_email));
      navigate("/step1");
    }

2. 백엔드에서 전달받은 쿠키를 통한 userCheck(유저가 로그인중인가 체크하는 함수)를 한다.
/
*위의 1에서 result.data.redirectUrl==='/'일 경우 응답값으로써 헤더에서 쿠키값을  
백엔드로부터 받아온다. 그리고, 쿠키에 담긴 유저값의 존재 여부를 응답값(user)으로 받아온다. 
그렇게 받아온 user를 로컬스토리지에 저장한다. localStorage를 이용한 이유는 새로고침을 해도 
로그인한 유저의 정보를 잊지 않기 위함이다. 리덕스 스토어나 컴포넌트 state값에 넣어두면, 
새로고침을 할 때 초기화된다. 하지만, user 정보를 로컬스토리지에 저장시키면 새로고침이 일어나도
유저값을 저장시킨다. 사실, 이 부분은 토큰 검증과 유저 정보를 어디에 저장해야 보안적으로 좋을지
정리해봐야겠다.
*/
 
useEffect(() => {
    if (user) {
      navigate("/");
      try {
        localStorage.setItem("user", JSON.stringify(user));
      } catch (e) {
        console.log("로컬스토리지가 작동 안해요.");
      }
    }
  }, [user]);
  1. 소셜 서비스 연동 로그인

느낀점

로그인 기능 구현도 어색한데, 소셜 서비스 연동은 어떻겠는가. 더 긴장됐다. 그런데 소셜 서비스 로그인의 소스는 인터넷에 워낙 많아서, 익히는 데 크게 어렵진 않았다. 굳이 헷갈렸다면, ‘js-sdk 방식을 이용할까?’ 아니면 ‘restAPI 방식으로 이용할까’였다. 처음엔 js-sdk 방식을 이용했다. 왜냐하면, 백엔드에서 결과적으로 필요한 데이터는 카카오의 access_token 였고, js-sdk는 카카오에서 로그인을 한 후 access_token을 바로 전달할 수 있었다. 그에 반해 rest-api는 인가 코드를 백엔드에 전달하고 그 인가코드를 받은 백엔드는 인가코드를 통해서 카카오로부터 access_token을 받아온다. 그리고, 그 access_token을 통해서 카카오로부터 유저 정보를 가져와서 DB의 유저 정보를 조회하여 유저의 회원가입 여부를 파악한다. 처음엔, js-sdk 를 이용해서 로그인 기능을 구현하는 것이 간편할 거라고 생각했다. 그렇게 js-sdk 방식을 이용했고, 실제로도 간편했다. 하지만, 나중에 알게 된 사실은 js-sdk로 구현한 로그인 기능은 모바일 뷰(안드로이드 크기의 뷰)에서 작동하지 않는다는 것이였다(정확히는 intent 에러가 떴다). 그래서 로그인을 할 수 없는 상황에 직면하였고, 나랑 비슷한 문제를 겪은 사람이 꽤 있었다. 그래서 restAPI 방식으로 이용하면 해결할 수 있다는 글을 보고서 js-sdk 방식을 ⇒ restAPI 방식으로 수정했다.

실제코드


import React, { useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import Spinner from "@/components/common/Spinner";
import { client } from "@/lib/api";
import { useNavigate } from "react-router";
import { setEmail, userCheck } from "@/redux/reducers/user";
import axios from "axios";

const KakaoRedirect = () => {
  const dispatch = useDispatch();
  const user = useSelector(({ user }) => user.user);
  const navigate = useNavigate();
  let code = new URL(window.location.href).searchParams.get("code");

  const sendAccessToken = async function (authObj) {
    let result = await client.post("/api/auth/kakao-login", {
      access_token: authObj.access_token,
    });
    if (result.data.redirectUrl === "/") {
      dispatch(userCheck());
    } else if (result.data.redirectUrl === "/step1") {
      console.log(result.data);
      dispatch(setEmail(result.data.user_email));
      navigate("/step1");
    }
  };

  function requestToken(code) {
    const JS_APP_KEY = "42f138356c44e8bdcbcae522929a5117";
    const REDIRECT_URI = "<http://localhost:3000/oauth/callback/kakao>";
    const makeFormData = (params) => {
      const searchParams = new URLSearchParams();
      Object.keys(params).forEach((key) => {
        searchParams.append(key, params[key]);
      });

      return searchParams;
    };

    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: JS_APP_KEY,
        redirect_uri: REDIRECT_URI,
        code,
      }),
    }).then((res) => {
      sendAccessToken(res.data);
    });
  }
  useEffect(() => {
    requestToken(code);
  }, []);

  useEffect(() => {
    if (user) {
      navigate("/");
      try {
        localStorage.setItem("user", JSON.stringify(user));
      } catch (e) {
        console.log("로컬스토리지가 작동 안해요.");
      }
    }
  }, [user]);

  return (
    <>
      <Spinner />
    </>
  );
};

export default KakaoRedirect;