일상의 기록/🌷DAILY 회고록 : 코드스테이츠

프론트엔드에서 하는 Google oAuth 로그인 로직

감귤밭호지차 2023. 7. 19. 01:26

구글 oAuth 로그인 로직을 구현하기 앞서 기본적인 핑퐁핑퐁에 대해서 정리해보자면,,, 

 

         : 클라이언트 파트 

 

1. 클라이언트 -> OAuth Server  ( 사용자 인증 )

   : 클라이언트가 OAuth server에  허가 코드를 요청 

   : 

2. OAuth Server -> 클라이언트

   : OAuth Server가  "허가코드를( Authorization Code ) " Redirect URL을 이용해서응답 

         - OAuth로 사이트에서 발급받은 "ID/Password/Credit"등을 넣는다. 

         - 대부분의 OAuth는 Developer Tap에서 API와 ID 등을 발급 받을 수 있다. 

 

* Authorization Code - Redirect URL

구글 OAuth의 승인된 redirection URL 은 정상적인 요청이 OAuth 서버에 도달 했을 때, OAuth 가 보내주는 허가 코드를 받을 수 있는 URL .

==> 프론트 배포 주소나 개발환경 주소를 미리 설정해두면 이곳으로 허가 코드나 응답을 받을 수 있는 URL.

      ( 따로 Google oAuth 사이트에서 설정해주어야 합니다. )

 

 

3. 클라이언트 -> Back Server : 허가코드를 서버에 전달

4. Back Server -> OAuth : 서버는 OAuth로 허가 코드를 전달하고 필요한 해당 유저 정보를 요청할 수 있음 

5. OAuth -> Back Server : 접근 코드(Accces Token) 전달 및 요청한 유저 정보를 응답

6. Back Server -> 클라이언트

    : 자체 암호화된 AccessToken 과 Refresh Token 을 전달.

 


- [FE] 구글에게 접근 허가 코드를 요청하기

 

위 방식이 정석인 것으로 알고 있으나, 프로젝트를 진행할 당시 구글에게 받은 코드를 가지고 백 서버 분이 어떻게 구글 유저 정보를 요청할 수 있는지 모르겠다고 하셔서...!  나름 서칭서칭을 해보았습니다만...

구글에게 받은 code를 가지고 scope에 있는 유저 정보를 얻을 수 있는 url 로 요청을 하면 해당하는 유저 정보를 얻을 수 있다는 것을 알았습니다. 우선은 프론트 쪽에서 구글에게 받은 코드를 가지고 유저 정보까지 얻은 다음에 백 서버에게 보내기로 했습니다. 

* 백 서버에게 보내는 1차 정보 

     - 유저 정보 ( 이름, 사진, 이메일 )

 

Google OAuth 2.0 사용 공식 문서 
 - Google의 API의 OAuth 2.0 범위 
 - Google oAuth 흐름 설명

 

아래 코드를 보시면 useGooglelogin 이라는 메서드를 통해( 구글 oAuth 허가 코드 얻는 방식입니다요)

onSuccess: 성공할 시, 구글로부터 허가 코드를 받고 scope에 있는 주소로 유저의 정보를 얻을 수 있는 axios 요청을 진행하도록 구현하였습니다. 

 

 

 

Authorization 의 코드는 구글의 인증 코드입니다. 요청을 받아오는 것에 성공하면 저 sendAccessToken() 메서드 안에 구글로부터 받아온 유저의 이름, 이메일, 사진을 props 로 넣어서 실행합니다. 

 

 

sendAccessToken() 함수는 최초로그인인지 기존 유저인지를 판별하는 메서드 입니다. 

 

 

import { useGoogleLogin } from '@react-oauth/google';
import { GoogleWrapper } from './LoginGoogleForm.styled';

export default function LoginGoogleForm({ children, type, alert }: LoginForm) {
//생략

  const getUserData = useGoogleLogin({
    onSuccess: (tokenResponse) => {
      const ACCESS = tokenResponse.access_token;

      axios
        .get('https://www.googleapis.com/oauth2/v2/userinfo', {
          headers: {
            Authorization: `Bearer ${ACCESS}`,
          },
        })
        .then((response) => {
          console.log('구글이랑 토큰 받아옵니다.');

          sendAccessToken(response.data.name, response.data.email, response.data.picture, ACCESS);
        })
        .catch((err) => console.log(err));
    },
  });

//생략
}

 

 

 

 

 

- [FE] 최초로그인 유저인지 기존 회원 로그인 유저 인지 판별하기 

 

저희 프로젝트 특성 상 최초 회원의 경우는 반드시 memberRole( Client | Partner ) 역할을 선택하고 이 역할에 따라 활동하거나 작업할 수 있는 부분이 달랐기 때문에 굉장히 중요했습니다. 자체 로그인을 먼저 구현하지 않고 구글 oAuth로만 구현을 했기 때문에 중간에 어떻게 해야 이 역할 선택 부분이 들어갈 수 있게 구현할지 고민이 굉장히 많았습니다. 

 

그렇게 나온 것이 구글 유저 정보를 얻는 그 순간을 이용하자였습니다. 

 

사실 유저 정보를 받는 순간은 구글 인증에 성공하여 로그인이 되는 상태인데 이 때의 찰나를 이용해서 백엔드의 서버에도 회원을 저장하고 저희가 필요한 역할 선택 데이터 값도 함께 저장을 할 수 있도록 하는 것이지요. 

 

 const sendAccessToken = (name: string, email: string, picture: string, token01: string) => {
    //최초로그인
    axios
      .post(
        'https://api.portfolly.site/oauth/signup ',
        {
          name: name,
          email: email,
          imageUrl: picture,
        },
        {
          headers: {
            Authorization: `Bearer ${token01}`,
          },
        }
      )
      .then((response) => {
        console.log('서버한테서 토큰과 아이디 리프레시를 받아옵니다.');
        const accessToken = response.headers.authorization;
        const memberId = response.headers.id;
        const refreshToken = response.headers.refreshtoken;
        
        //localStorage 에 액세스 토큰 저장
        window.localStorage.setItem('memberId', memberId);
        window.localStorage.setItem('accessToken', accessToken);
        window.localStorage.setItem('refreshToken', refreshToken);
        //store에 상태 저장
        dispatch(setCredentials({ accessToken: accessToken }));
        dispatch(setCredentials({ refreshToken: refreshToken}));
        dispatch(setCredentials({ memberId: memberId }));
        //역할 선택 페이지로 이동
        navigate('/signup/role');

        console.log(`토큰` + accessToken);
        console.log(`아이디` + memberId);
      })
      .catch((error: AxiosError<ErrorResponse>) => {
        if (axios.isAxiosError(error)) {
          const axiosError = error as AxiosError<ErrorResponse>;
          if (axiosError.response) {
            const errorCode = axiosError.response.data.errorCode;
            const errorMessage = axiosError.response.data.errorMessage;
            if (
              errorCode === 409 &&
              errorMessage === '이메일 중복 오류 입니다. [Action] : 다른 이메일을 사용하여 주십시오.'
            ) {
              console.log('중복 데이터 입니다. 기존 로그인으로 갑니다.');

              //별도의 로그인 get 요청 처리 - id / 서버 access token (header)
              getAccessToken();
            } else {
              console.log('다른 에러 발생', axiosError.response.data);
            }
          } else {
            //서버 응답이 없는 경우
            console.log('서버 응답 없음 ', axiosError.message);
          }
        }
      });
  };

 

코드를 보시면  axios 요청을 통해 유저의 정보를 서버에 전달합니다. 여기서 주요 분기점이 나타나게 되고 성공에러로 신규회원과 기존 회원을 구분할 수 있도록 구현하였습니다. 

 

1. 해당 이메일이 기존 DB에 없으면 신규 회원     =>  200 성공 응답 + accessToken / RefreshToken 
2. 유저 정보를 받은 서버는 해당 이메일이 기존 DB 에 있으면 기존 회원        => 409 에러 응답 : 이메일 중복 오류 message 

 

#01. 해당 이메일이 기존 DB에 없으면 신규 회원     =>  200 성공 응답 + accessToken / RefreshToken 

axios 성공 후의 코드를 보시면 서버로부터 accessToken 과 RefreshToken을 받고 이를 localStorage 와 Redux-Toolkit을 통해 만들어둔 Store에 상태를 업데이트 및 저장합니다. 

 

그리고 역할 선택 페이지로 이동하도록 구현하였습니다. 이곳에서 역할 선택까지 진행 한 후에 서버에 해당 유저의 역할을 넘기고 로그인 상태로 전환하여 메인 페이지로 이동할 수 있게 구현하였습니다. 

 

 

### 역할 선택 컴포넌트 

//import 생략

export default function SelectRole() {
  const [role, setRole] = useState('');
  const navigate = useNavigate();
  const dispatch = useDispatch();
  
  const ACCESS_TOKEN = window.localStorage.getItem('accessToken');
  const MEMBER_ID = window.localStorage.getItem('memberId');
  console.log('파트너/클라이언트 선택 페이지로 이동합니다.');

  const selectRole = (res: string) => {
    setRole(res);
    console.log(res);
  };

  const sendRole = () => {
    try {
      axios.post(
        `https://api.portfolly.site/members`,
        {
          id: MEMBER_ID,
          member_role: role,
        },
        {
          headers: {
            Authorization: `${ACCESS_TOKEN}`, 
          },
        }
      );
      console.log('선택지가 잘 전달 되었습니다.');
      dispatch(login({ isLogin: true }));
      window.localStorage.setItem('memberRole', role);
      navigate('/main');
    } catch (err) {
      console.log(err);
    }
  };

  return (
    <RoleWrapper>
      <ButtonBox>
        <TitleText>Choose your Role</TitleText>
        <ButtonContainer>
          <SignBtn type={'client'} onClick={(chooseRole) => selectRole(chooseRole)}>
            Client
          </SignBtn>
          <SignBtn type={'partner'} onClick={(chooseRole) => selectRole(chooseRole)}>
            Partner
          </SignBtn>
        </ButtonContainer>
        <NextButton onClick={sendRole}>Next</NextButton>
      </ButtonBox>
    </RoleWrapper>
  );
}

 

 

 

#02. 유저 정보를 받은 서버는 해당 이메일이 기존 DB 에 있으면 기존 회원        => 409 에러 응답 : 이메일 중복 오류 message 

여기서는 에러 코드를 세분화로 체크해서 특정 에러 코드 ( 409) 와 메세지를 받았을 경우가 서버로부터 " 기존 회원 " 임을 파악하는 방식으로 구현했습니다. 이제 이 경우에는 단순 로그인 회원 처리를 위해 getAccessToken() 메서드를 실행하도록 하였습니다. 

 

 

 

### 기존 회원 로그인 요청

//기존 로그인
  const getAccessToken = async () => {
    const accessToken = window.localStorage.getItem('accessToken');
    const memberId = window.localStorage.getItem('memberId');
    console.log('이메일 중복 기존 회원으로 서버한테 로그인 요청합니다.');

    try {
      const res = await axios({
        method: 'get',
        url: 'https://api.portfolly.site/oauth/login',
        headers: {
          authorization: `${accessToken}`,
          id: memberId,
        },
      });
      console.log('기존 회원 로그인 성공', res);
      navigate('/main');
      dispatch(login({ isLogin: true }));
    } catch (err) {
      console.log(err, 'refreshToken 새로 발급 필요합니다');
      console.log('새 발급을 위해 함수 이동합니다.');
      await getRefreshToken();
    }
  };

위 코드를 보시면 서버에 accessToken을 가지고 axios 요청을 통해 알맞게 전역 상태를 로그인 상태로 바꾸고 메인 화면으로 넘어갈 수 있게 구현하였습니다. 

 

 

혹시 accessToken이 만료되어 refreshToken으로 재 발급 받아야 할 경우 axios 요청의 실패로 넘어가 accessToken을 다시 발급 받을 수 있는 getRefreshToken 메서드를 실행하도록 진행됩니다. 

const getRefreshToken = () => {
    const refresh = window.localStorage.getItem('refreshToken');
    console.log('[주목] getRefreshToken 함수 실행 ');
    axios
      .post(
        'https://api.portfolly.site/oauth/regeneration/token',
        {},
        {
          headers: {
            RefreshToken: `${refresh}`,
          },
        }
      )
      .then((Res) => {
        console.log(Res, '재발급 성공');
        const newAccess = Res.headers.authorization;
        console.log('기존 accessToken 없애고 새로운 토큰 저장합니다!');
        window.localStorage.setItem('accessToken', newAccess);
        getAccessToken();
      })
      .catch((err) => console.log('refreshToken 재발급 실패', err));
  };

 

 

 

유저 정보나 Token을 저장하는 방식에 대해서는 >  브라우저 저장소 3종 세트 모음집  <  블로그 글에서 정리해두었으니 참고해주세요.

 

 

 

 

구현 소감 

 

사실 블로그로 다시 정리하면서 떠오르는 문제점 들(?) 이나 개선하고 싶은 부분들이 많았지만... ㅜㅜ 열심히 구현하려고 백엔드 분과 깨지고 부서지고 해결하려고 노력했던걸 생각하면 구현했다는데에 의의를 두고 싶(?)습니다. 

 

중간에 각종 헤더의 상호 대문자, 소문자, 이름 엇갈려서 나온 에러, 무슨 Bearer 앞에 공백을 인식 못해서 원인을 찾으려고 여기저기 서칭해서 백엔드 분께 공유드린 문제.. CORS 에러..  https와 http 통신 문제... 환장 파티였지만...ㅜㅜㅜ 그래도 재밌었습니다..

 

 

현재 개선해야 할 부분으로 크게 떠오르는 것은 

1. 구글 oAuth 자체로 로그인/회원가입이 이루어지는데 굳이 서버와 따로 회원가입/로그인 axios 요청이 필요한가..?에 대한 의문점. 

2. 토큰 문제(서버. .. 엣헴헴.. )로 현재 배포 환경에서 로그인이 안되는데 그 부분에 따른 알림 모달 창 추가 하기 

3. 토큰이나 유저 정보를 모두가 볼 수 있는 localStorage 말고 Cookie에 저장하고 싶은 마음?? 

4. 자체 로그인을 구현하지 못했던 점 

 

크게 네 가지가 있는 것 같습니다용. 특히나 1번에 대해서는 좀 더 공부를 해야 할 필요성을 크게 느꼈습니다. 원래 googleOAuth가 프론트 쪽에서 거진 모든 정보를 받아서 넘기는게 아니라고 알고 있는데.. 어쩌다보니 급한대로 프론트쪽에서 웬만한 로직을 처리해서.. 정식대로 하지 못했다는 아쉬움?? 도 남아있긴 하네용. 

 

 

 

# 참고 사이트 
1. 구글 oAuth 로그인 적용기
2. Google Open ID docs
3. react-oauth .github
4. Google oauth2 로그인 구현 with React

# 기타 
 1. 토큰을 사용할 때 Bearer는 무엇인가?
2. JWT 인증 토큰, Bearer 토큰에 관하여 
3. [에러] illegal base64url character: ''
4. oAuth를 구현하기 위해 알아야 할 것들 # Access Refresh Token



 

 

 

 


참고 예정 

 

# 파트너 선택 후 넘어 갈 때, 부드러운 WELCOME 애니메이션 

 

네, 페이지 전환 시 애니메이션 효과를 적용하여 부드럽게 랜더링되도록 할 수 있습니다. 이를 위해 React의 다양한 라이브러리 및 기술을 활용할 수 있습니다. 예를 들면 React Transition Group, React Router Transition 등의 라이브러리를 사용할 수 있습니다.

아래는 React Transition Group을 사용하여 페이지 전환 시 애니메이션 효과를 적용하는 예시입니다:

  1. 먼저, 애니메이션 효과를 적용할 페이지 컴포넌트를 정의합니다. 예를 들어, 'WelcomePage'라는 컴포넌트를 생성합니다.
import { CSSTransition } from 'react-transition-group';
import './WelcomePage.css'; // 애니메이션 효과를 정의하는 CSS 파일

function WelcomePage() {
  return (
    <CSSTransition
      classNames="welcome-transition"
      timeout={500}
    >
      <div className="welcome-page">
        {/* 페이지 컨텐츠 */}
      </div>
    </CSSTransition>
  );
}

'WelcomePage' 컴포넌트에 애니메이션 효과를 정의하는 CSS 파일을 작성합니다. 이 예시에서는 'WelcomePage.css'로 파일을 생성하고, 애니메이션 효과를 정의합니다.

 

.welcome-transition-enter {
  opacity: 0;
  transform: translateX(100%);
}

.welcome-transition-enter-active {
  opacity: 1;
  transform: translateX(0%);
  transition: opacity 500ms, transform 500ms;
}

.welcome-transition-exit {
  opacity: 1;
  transform: translateX(0%);
}

.welcome-transition-exit-active {
  opacity: 0;
  transform: translateX(-100%);
  transition: opacity 500ms, transform 500ms;
}
  1. 페이지 전환 시 애니메이션 효과를 적용하기 위해 라우터를 구성합니다. 예를 들어, React Router를 사용하여 라우터를 설정합니다.
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

function App() {
  return (
    <Router>
      <Switch>
        {/* 다른 라우트 설정 */}
        <Route path="/welcome" component={WelcomePage} />
        {/* 다른 라우트 설정 */}
      </Switch>
    </Router>
  );
}

navigate 함수를 사용하여 '/welcome' 경로로 이동하면, 'WelcomePage' 컴포넌트가 애니메이션 효과와 함께 랜더링됩니다.

 

const sendRole = () => {
  return call(`/members`, 'POST', {
    memberId: MEMBER_ID,
    memberRole: role
  })
  .then(() => {
    console.log('POST 성공');
    dispatch(login(true));
    navigate('/welcome');
  });
}

위와 같이 설정하면 '/welcome' 경로로 이동 시 'WelcomePage' 컴포넌트가 애니메이션과 함께 랜더링되며, 원하는 부드러운 전환 효과를 구현할 수 있습니다. 이 예시는 React Transition Group을 사용한 예시이며, 다른 애니메이션 라이브러리를 사용하여 유사한 효과를 구현할 수도 있습니다.