Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FE] 로그인 구축으로 인한 인증 전략 변경 #837

Open
wants to merge 19 commits into
base: fe-dev
Choose a base branch
from

Conversation

jinhokim98
Copy link
Contributor

@jinhokim98 jinhokim98 commented Nov 21, 2024

issue

블로그에서도 아래 내용을 확인할 수 있습니다.

기존 인증 전략

먼저 유저가 관리 페이지에 진입하게 되면 PostAuthenticate 함수 호출을 하게 됩니다. 이는 백엔드로 "내가 이 행사의 접근 권한이 있나요?"를 물어보는 기능으로 백엔드에서 쿠키를 확인한 후 결과를 알려주게 됩니다. 쿠키가 있다면 행사의 주최자가 맞으므로 관리 페이지에 정상 진입하게 되고, 쿠키가 없거나 변조된 값이면 주최자가 아니기 때문에 관리 페이지 대신 비회원 로그인 페이지로 가도록 하며, 비밀번호 4자리를 입력하고 로그인을 해야 주최자로 승인 받게 됩니다.

행사마다 회원 소유가 없으며 비밀번호를 정상적으로 입력하는 유저는 모두 주최자로 인정받을 수 있기 때문에 단순한 플로우를 구축할 수 있었고 프론트엔드에서 인증 절차를 복잡하게 가져가지 않았습니다.

로그인 기능으로 추가된 인증 요구사항

하지만 이제 회원가입이 생겨 행사의 소유자가 생기고 로그인 한 유저는 비밀번호 4자리를 입력하지 않으므로 위 전략대로 가져갈 수가 없습니다. 행사 주최자가 비밀번호 없이 행사를 만들었는데 관리 페이지 접근 시 비밀번호 입력 화면을 보여주면 안 되기 때문입니다. 그리고 로그인으로 카카오 OAuth를 사용하게 되어 서비스를 이탈했다가 돌아와야하는 요구사항도 생겼습니다. 그래서 관리 페이지에서 카카오 로그인으로 이동한 뒤 관리 페이지로 다시 돌아갈 수 있는 기능도 추가 되어야 합니다. 요구사항을 짧게 정리해보면

  1. 행사 소유자에 따른 로그인 기능 분리 (비밀번호 / 카카오)
  2. 인증 실패 후 로그인을 성공했을 때 관리 페이지로 바로 접속할 수 있는 기능

변경된 인증 전략

동일하게 먼저 유저가 관리 페이지에 진입합니다. 이 때 원래는 바로 PostAuthenticate 함수를 호출하여 백엔드에게 인증을 요청했지만 이제는 행사의 소유자를 먼저 알아야하므로 행사 정보 조회 API를 먼저 불러옵니다. 그리고 PostAuthenticate 함수를 호출하여 백엔드에게 인증을 요청한 후에 바로 세션스토리지에 행사 소유자 정보를 저장합니다. 만약 쿠키가 있어서 정상적으로 인증이 완료되면 관리 페이지로 정상 진입하게 됩니다. 그러나 쿠키가 없거나 변조되어 정상적으로 인증이 완료되지 않은 경우에 아래 흐름을 따릅니다.

먼저 현재 URL을 세션스토리지에 저장합니다. 즉 관리 페이지 URL이 세션스토리지에 저장됩니다. 그리고 아까 세션스토리지에 저장한 행사 소유자 정보를 가져와서 회원 / 비회원 생성을 구분하게 됩니다. 회원이 생성한 행사라면 카카오 로그인 페이지로, 비회원이 생성한 행사라면 기존의 비회원 로그인(비밀번호 입력) 페이지로 이동하게 됩니다. 각자의 방법으로 로그인을 성공했다면 이전에 세션스토리지에 저장해 둔 URL로 이동시키게 되면 관리 페이지에 접속하여 인증에 실패한 유저가 다시 관리 페이지로 자동으로 돌아올 수 있게 됩니다.

변경된 인증 전략을 코드로 살펴보기

  1. 행사 정보 조회 API 호출
// query hooks
const useRequestGetEvent = ({...props}: WithErrorHandlingStrategy | null = {}) => {
  const eventId = getEventIdByUrl();

  const {data, ...rest} = useSuspenseQuery({
    queryKey: [QUERY_KEYS.event],
    queryFn: () => requestGetEvent({eventId, ...props}),
  });

  return {
    eventName: data.eventName,
    bankName: data.bankName,
    accountNumber: data.accountNumber,
    createdByGuest: data.createdByGuest,
    ...rest,
  };
};

export default useRequestGetEvent;

백엔드에서 행사 소유자 정보인 createdByGuest를 추가해줘서 이를 반영해줬습니다. 그리고 변경된 코드가 하나 더 있는데 바로 useQuery에서 useSuspenseQuery로의 변경입니다. 행동대장에서는 GET 메서드 호출을 할 때 useQuery를 사용해왔습니다. 제가 생각한 그 이유는 GET 메서드를 호출하는 동안 대체로 보여줄 로딩과 스켈레톤을 만들지 않았으며 반드시 값이 보장되어야하는 요구사항이 없었기 때문에 SuspenseQuery를 사용해야하는 근거가 없었다고 생각합니다. 하지만 이 기능을 구현하며 두 번째의 이유가 생겼습니다. 인증에 실패한 사용자가 로그인을 하는 과정에서 행사 소유자 정보를 보장받을 수 없다면 회원으로 생성된 행사에서 비회원 로그인 방식을 보여줄 수 있게 되는 최악의 경우가 발생할 수 있습니다. 그래서 값이 보장되어야하는 이유가 생겼고 useSuspenseQuery를 사용해서 값을 보장할 수 있도록 변경해줬습니다.

여기서 SuspenseQuery를 사용하는 주의점으로 SuspenseQuery를 호출하는 부모 컴포넌트에 Suspense로 감싸줘야하는 규칙이 있지만 행동대장에서 이미 Lazy import를 하며 Suspense를 라우트 최상단에 감싸주고 있기 때문에 따로 처리를 해주지 않았습니다. 추후에 로딩 화면이 필요하다는 요구사항이 들어오면 이벤트 페이지를 감싸는 Suspense를 추가해줄 수 있을 것 같습니다.

  1. 백엔드로 인증 요청하는 PostAuthenticate
const useRequestPostAuthentication = () => {
  const eventId = getEventIdByUrl();
  const {updateAuth} = useAuthStore();

  const {createdByGuest} = useRequestGetEvent();

  const {mutate, ...rest} = useMutation({
    mutationFn: () => requestPostAuthentication({eventId}),
    onSuccess: () => updateAuth(true),
    onMutate: () => {
      SessionStorage.set<boolean>(SESSION_STORAGE_KEYS.createdByGuest, createdByGuest);
    },
  });

  return {
    postAuthenticate: mutate,
    ...rest,
  };
};

여기서 postAuthenticate 함수 호출로 mutate가 실행되어 백엔드로 인증을 요청하게 됩니다. 여기서 추가된 것은 onMutate 콜백입니다. mutate가 실행될 때, 즉 백엔드로 인증 요청을 할 때 세션스토리지에 행사 소유자 정보를 저장하게 되며 이는 추후에 로그인에 실패할 때 사용하게 됩니다.

  1. 에러를 잡아주는 ErrorCatcher에서 401 에러에 대한 대응 추가
const ErrorCatcher = ({children}: React.PropsWithChildren) => {
  const {appError: error} = useAppErrorStore();
  const navigate = useNavigate();
   
  useEffect(() => {
    ...
    if (
      error.errorCode === 'TOKEN_NOT_FOUND' &&
      !window.location.pathname.includes('/guest/login') &&
      !window.location.pathname.includes('/member/login')
    ) {
      const createdByGuest = SessionStorage.get<boolean>(SESSION_STORAGE_KEYS.createdByGuest);
      SessionStorage.set<string>(SESSION_STORAGE_KEYS.previousUrlForLogin, window.location.pathname);

      const currentPath = window.location.pathname;

      if (createdByGuest) {
        navigate(`${currentPath}/guest/login`);
      } else {
        navigate(`${currentPath}/member/login`);
      }
    }
  }, [error, navigate]);
  
  return children;
};

export default ErrorCatcher;

로그인에 실패하게 되면 백엔드에서 인증 에러인 401 TOKEN_NOT_FOUND 에러를 발생시킵니다 그러면 ErrorCatcher에서 useEffect 구문에 의해 에러 변경이 감지되면 기존의 에러 처리를 한 후에 if문을 실행하게 됩니다.

TOKEN_NOT_FOUND 에러가 발생한 경우에 세션스토리지에서 행사 소유자 정보를 먼저 가져옵니다. 여기서 행사 소유자 정보에 따라 카카오 / 비밀번호 페이지로 이동하기 때문입다. 그리고 바로 다시 세션스토리지에 현재 url path 정보를 저장해주는데 이는 나중에 로그인에 성공했을 때 사용하기 위해 저장해둡니다.

createByGuest가 true라면 비회원이 만든 행사이므로 비회원 로그인 페이지로 라우트 시키고 false일 때는 회원이 만든 행사이므로 카카오 로그인 페이지로 라우트 시킵니다.

여기서 처음 if문이 복잡한 이유는 React.StrictMode 때문입니다. StrictMode에서는 useEffect를 두 번 씩 실행시키기 때문에 navigate도 역시 두 번 실행되어 url이 https://~~~/admin/guest/login/guest/login이 되기 때문이며 두 번 째 Effect가 실행될 때 navigate가 실행되지 않기 위한 가드 역할을 해줍니다.

  1. 비회원 로그인 후 관리 페이지로 이동하기
const useRequestPostLogin = () => {
  const eventId = getEventIdByUrl();
  const {updateAuth} = useAuthStore();
  const navigate = useNavigate();

  const {mutate, ...rest} = useMutation({
    mutationFn: ({password}: RequestPostToken) => requestPostToken({eventId, password}),
    onSuccess: () => {
      const previousUrlForLogin = SessionStorage.get<string>(SESSION_STORAGE_KEYS.previousUrlForLogin);
      if (previousUrlForLogin) {
        SessionStorage.remove(SESSION_STORAGE_KEYS.previousUrlForLogin);
        navigate(previousUrlForLogin, {replace: true});
      }
      updateAuth(true);
    },
  });

  return {postLogin: mutate, ...rest};
};

export default useRequestPostLogin;

여기서 비회원 로그인 api가 호출되고 요청에 성공할 때 기능이 추가되었습니다. 먼저 이전 단계에서 저장한 이전 페이지 정보를 세션스토리지에서 가져옵니다. 그리고 이전 페이지 정보가 있다면 그 페이지로 이동시켜주고 세션스토리지에서 이전 페이지 정보를 지워줍니다. 여기서 navigate 옵션으로 replace: true를 준 이유는 로그인에 성공했으니깐 페이지를 대체해버리기 위함입니다.

  1. 회원 로그인 후 관리 페이지로 이동하기
const LoginRedirectPage = () => {
  useEffect(() => {
    if (location.search === '') return;

    const code = new URLSearchParams(location.search).get('code');
    const previousUrlForLogin = SessionStorage.get<string>(SESSION_STORAGE_KEYS.previousUrlForLogin);

    const kakaoLogin = async () => {
      if (!code) return;

      await requestGetKakaoLogin();
      updateAuth(true);
      trackStartCreateEvent({login: true});

      if (previousUrlForLogin) {
        navigate(previousUrlForLogin, {replace: true});
      } else {
        navigate(ROUTER_URLS.createMemberEvent);
      }
    };

    kakaoLogin();
  }, [location.search]);
  
  return (
  ...
  )
}

export default LoginRedirectPage;

카카오 OAuth 서비스에 등록해 둔 Redirect Uri에 해당하는 컴포넌트입니다. 카카오 OAuth에서 유저가 아이디와 비밀번호를 입력하게 되면 인가코드(code)라는 것을 주게 되는데 우리는 이 code를 백엔드에 전달하여 카카오 로그인을 하게 됩니다. 여기서도 우선 이전 페이지 정보를 세션스토리지에서 가져오게 됩니다. 여기는 분기가 조금 다른 것이 카카오 로그인을 관리 페이지에서도 하지만 서비스를 처음 시작할 때도 사용하기 때문에 조건을 나누어 처리를 해줬습니다. 동일하게 로그인에 성공하게 되면 이전 페이지 정보를 확인하고 정보가 있다면 이전 페이지(관리 페이지)로 이동합니다. 이전 페이지 정보가 없다면 회원 행사 생성으로 이동하게 됩니다.

여기까지 블로그에 기록한 내용입니다. 추가로 이 아래는 기타로 변경된 사항을 적어보려합니다.

카카오 Redirect Uri 변경

카카오 Redirect Uri를 변경했습니다. 그 이유는 처음 플로우에서 로그인만 생각했었는데 관리 페이지에서 인증에 실패하여 카카오 로그인을 요구할 때 동일한 UI를 사용하면 안 될 것 같아 관리 페이지 카카오 로그인과, 서비스 시작 로그인을 분리해서 만들어줬습니다. 그래서 Redirect Uri도 한 페이지에 의존하지 않고 다른 컴포넌트를 만들어 분리했습니다.

TopNav navigate 로직 수정

이전에는 마지막 path만 지우고 추가해줬는데 이제는 /admin/guest/login 과 같이 sub path가 길어지게 되어 이런 경우에 정상적으로 처리해줄 수 없었습니다. 그래서 유틸 함수 getDeletedLastPath를 getEventBaseUrl이라는 함수로 변경하고 event/eventId까지 얻어와서 그 뒤에 상황에 맞게 붙여주는 기능으로 변경했습니다.

추가한 화면들

image

추가 변경사항

createdByGuest 값을 굳이 세션스토리지에 저장하지 않고 useRequestPostAuthentication에서 책임을 지는 방식으로 변경했습니다. ErrorCatcher에 특정 에러에 대한 navigate 책임까지 넣기에 과한 책임을 진다고 생각했으며 인증을 책임지는 함수에서 이를 책임지도록 변경하는 것이 더 적절하다고 생각하여 변경했습니다.

변경사항 : createdByGuest를 세션스토리지에 저장하여 ErrorCatcher에서 사용한다 -> createdByGuest를 호출하여 postAuthenticate 함수 내 onError 콜백 안에서 navigate를 책임진다.

중점적으로 리뷰받고 싶은 부분(선택)

인증 과정에 대해서 리뷰 부탁 드려요!! 더 간편한 방식이 있을지 궁금합니다!

🫡 참고사항

@jinhokim98 jinhokim98 added this to the v2.2.0 milestone Nov 21, 2024
@jinhokim98 jinhokim98 self-assigned this Nov 21, 2024
Copy link

@woowacourse-teams woowacourse-teams deleted a comment from github-actions bot Nov 25, 2024
@jinhokim98 jinhokim98 linked an issue Nov 25, 2024 that may be closed by this pull request
2 tasks
Copy link
Contributor

@soi-ha soi-ha left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

코드 읽다가 단순히 궁금한게 생겨서 질문 드렸어요!
멋지게 구현해줘서 고마워요 쿠키~

const queryResult = await requestGetClientId();
const clientId = queryResult.data?.clientId;

if (typeof previousUrl !== 'undefined') {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정말 별거 아닌 호기심입니다만.. previousUrl의 타입이 undefined가 아닐 때에만 세션 스토리지를 set하도록 해주셨잖아요. previousUrl === 'string'이 아닌 previousUrl !== 'undefined'로 해주신 이유가 궁금합니다!

previousUrl에는 타입이 string이나 undefined가 아니면 컴파일시에 타입 오류가 발생하기 때문에 해당 goKakaoLogin 함수가 실행되지 않을 것 같단 말이죠? 그래서 저라면 부정문 보다는 긍정문이 코드 이해에 더 좋을 것 같고, previousUrl은 무조건 string이어야 하기 때문에 if문 조건을 string으로 뒀을 것 같은데.. undefined로 하신 이유가 궁금합니다.

해당 질문은 정말 별거 아니고.. 저랑 다른 생각 구조(?)를 가지신 것 같아서 궁금해서 여쭤보는 겁니다!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

파라미터를 넘길 때 previousUrl이 있는 경우가 있고 없는 경우가 있어서 undefined 체크를 했어요.
인증에 실패해서 돌아가야할 곳이 없는 경우를 체크하기 위해서였는데 "파라미터가 없지 않을 경우"에 실행한다 다시 생각해보니 어색하네요.

소하 의견대로 파리미터가 없을 경우 얼리리턴이나 파라미터가 있을 경우가 더 좋은 것 같아요

<FunnelLayout>
<Top>
<Top.Line text={`행사 관리 접근을 위해`} />
<Top.Line text="카카오 계정 로그인을 해주세요." emphasize={['카카오 계정 로그인을 해주세요.']} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

단순 디자인 피드백입니다!
강조 글씨를 카카오 계정 로그인을 해주세요. 모두가 아닌 카카오 계정 로그인에만 두면 이상할까요??
여태 저희 디자인에서 문장 전체에 강조를 준 적은 없는 것 같아서, 핵심 단어에만 강조를 주는 것이 더 좋을 것 같아서요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오 좋은 의견입니다! 반영해둘게요👍

Copy link

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
Status: 🤼 In Review
Development

Successfully merging this pull request may close these issues.

[FE] 로그인 구축으로 인한 인증 전략 변경
2 participants