useRouter은 못말려 - 2

In9_9yu·2023년 6월 20일
2

mobae

목록 보기
7/9

들어가기 전 & 지난 이야기

useRouter은 못말려 의 후속 글입니다.

일단 어찌저찌 이전 PR은 닫았습니다.

새로운 PR을 열어 문제를 해결했습니다.

지난 글에서 아래처럼 대충 눈속임으로 얼렁뚱땅 넘어가보려 했지만, 시간을 이렇게 들였는데 얼렁뚱땅은 참을 수 없었습니다.
눈속임

redirect

이게 왜 지금 생각났지

아니 아주 기가막히고 코가막히는 방법이 있었는데, 이걸 계속 외면하고 있었다니.

next 공식문서에 redirect를 검색하면, next.config.js 에서 redirects 함수를 사용하는 예제를 볼 수 있습니다.

하지만, 해결하고자 하는 문제는 로그인 여부에 따른 redirect이므로 적절하지 않았습니다.

아래로 쭉 내려가면 Other Redirects 가 있는데, getStaticPropsgetServerSideProps에서 redirect 할 수 있다는 설명이 나와있는 걸 보고, 이거다 싶었습니다.

사용자가 해당 페이지에 접속을 하는 경우에 로그인 여부를 체크해야하기 때문에, getServerSideProps 를 사용하는 것으로 결정했습니다.

처리해야할 케이스는 3가지 입니다.

처리해야할 케이스

세 가지 케이스 중 헤더에 리프레시 토큰이 없는 경우를 제일 쉽게 처리할 수 있습니다.

아래와 같은 코드를 통해 헤더에 쿠키가 존재하지 않는 경우 로그인 페이지로 redirect 시킬 수 있습니다.

const getServerSideProps = async(context: GetServerSidePropsContext) => {
  const { req } = context;

  if (!req.headers.cookie.refreshToken) {
    return {
      redirect: {
        destination: "/login",
        permanent: false,
      },
    };
  }

  return { props: {} };
}

참고 : 여기에서 return 하는 객체를 확인할 수 있습니다.

Q. 리프레시 토큰의 유효성을 검증해야 하는 경우는 어떻게 처리해야할까요?

  1. 쿠키의 유효성을 검증하기 위해서는, /validate-token 이라는 url로 api를 요청해야 합니다.

  2. /validate-token를 호출하는 함수를 react query의 prefetch를 이용해 해당 데이터를 queryClient에 받아놓습니다.

validate-token에 대한 응답은, 새로운 access token과, 기본적인 유저 정보입니다.

// 성공 케이스
const data = {
 accessToken : 'eyJhbGciOiJIUzI1NiI....',
 user:{
   email:'test@test.com',
   id: 1 
   ...
 }
}

// 실패 케이스
const data = null
  1. dehydrate 함수와 <Hydrate> 컴포넌트를 통해 클라이언트 측에ㅓ서 queryClient의 결과들을 사용할 수 있도록 합니다.

Q. 그렇다면 언제 리프레시 토큰의 유효성을 검증하는 것이 좋을까요?

개인적으로는 처음 페이지에 접근하는 경우 검사를 했으면 좋겠다고 생각했습니다.

처음에 한 번 검증을 하면 반환된 값이 바뀔 일이 크게 없기 때문입니다.

어느 페이지에 접근하더라도, 초기 딱 한번은 유효성을 검증하고 싶었기 때문에 _app 에서 SSR과 연관된 메소드를 사용하는 것으로 방향을 잡았습니다.

문제는 _app에서는 getServerSidePropsgetStaticProps을 사용하지 못한다는 것입니다.

Nextjs에서 최근에 나온 App folder를 기준으로 작업하는 경우에는 어느 정도 가능한 것으로 보이는데, 그 상황이 아니다보니 다른 방법을 찾아야 했습니다.

결국 deprecated 예정인 getInitialProps 메소드를 이용해서 다음과 같은 함수를 작성하였습니다.

MyApp.getInitialProps = async ({ ctx: { req } }: AppContext) => {
  const queryClient = new QueryClient();

  if (req === undefined) {
    return { props: { dehydratedState: dehydrate(queryClient) } };
  }

  await queryClient.fetchQuery({
    queryKey: queryKeys.auth.tokenState,
    queryFn: () => silentLogin(req.headers.cookie),
  });

  return { props: { dehydratedState: dehydrate(queryClient) } };
};

TMI) if(typeof req === undefined){...} 로는 타입가드를 할 수 없다.

해치웠나?

해치웠나?

언뜻 보면 완료된 것 같지만, 두 가지 작업이 더 필요합니다.

  • recoil userState에 의존적인 코드 수정
  • accessToken을 axios의 헤더에 넣는 코드

recoil userState에 의존적인 코드 수정

우선 지금까지 작성한 코드 중, recoil의 userState에 의존하는 코드들이 몇 개 있었는데요, (예를 들어, Header... )

userState에 의존적인 코드들을, queryClient에 있는 데이터를 바라보도록 바꿔주어야 합니다.

반복되는 코드들이 꽤나 많아서, 커스텀 훅으로 만들어 반복을 피하도록 하겠습니다.

const useUser = (): ReturnType => {
  const queryClient = useQueryClient();
  const loginResponse = queryClient.getQueryData<ILoginResponse>(
    queryKeys.auth.tokenState,
  );

  const isLogin = !isNil(loginResponse);

  return [isLogin, loginResponse?.user];
};
export default useUser;

accessToken을 axios의 헤더에 넣는 코드

서버 사이드에서 accessToken을 발급 받고, queryClient에만 넣어놓게 되면, 브라우저에서 accessToken이 필요한 요청을 하고 싶어도 할 수 없습니다.

따라서 클라이언트 측에서 hydrate를 할 때, axios의 헤더에 accessToken을 넣는 과정이 필요합니다.

const AccessTokenInject = () => {
  const queryClient = useQueryClient();

  const data = queryClient.getQueryData<ILoginResponse | null>(
    queryKeys.auth.tokenState,
  );

  if (data?.accessToken) {
    setBearerToken(data.accessToken);
  }
  return null;
};

이렇게 작성한 후에, _app에 넣어주면 그걸로 끝입니다.

결론

흐름을 대략 정리하면 이렇습니다.

흐름

고려해야할 사항이 생각보다 많아서 어려웠지만, SSR을 조금 더 이해하게 되는 이슈였던 것 같아 기분은 좋네요 깔깔

이제 불필요한 UI 변경이 없어집니다~

ssr1

ssr2

profile
FE 임니다

0개의 댓글