useRouter은 못말려 의 후속 글입니다.
일단 어찌저찌 이전 PR은 닫았습니다.
새로운 PR을 열어 문제를 해결했습니다.
지난 글에서 아래처럼 대충 눈속임으로 얼렁뚱땅 넘어가보려 했지만, 시간을 이렇게 들였는데 얼렁뚱땅은 참을 수 없었습니다.
이게 왜 지금 생각났지
아니 아주 기가막히고 코가막히는 방법이 있었는데, 이걸 계속 외면하고 있었다니.
next 공식문서에 redirect를 검색하면, next.config.js 에서 redirects 함수를 사용하는 예제를 볼 수 있습니다.
하지만, 해결하고자 하는 문제는 로그인 여부에 따른 redirect이므로 적절하지 않았습니다.
아래로 쭉 내려가면 Other Redirects 가 있는데, getStaticProps
와 getServerSideProps
에서 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. 리프레시 토큰의 유효성을 검증해야 하는 경우는 어떻게 처리해야할까요?
쿠키의 유효성을 검증하기 위해서는, /validate-token
이라는 url로 api를 요청해야 합니다.
/validate-token
를 호출하는 함수를 react query의 prefetch를 이용해 해당 데이터를 queryClient에 받아놓습니다.
validate-token에 대한 응답은, 새로운 access token과, 기본적인 유저 정보입니다.
// 성공 케이스
const data = {
accessToken : 'eyJhbGciOiJIUzI1NiI....',
user:{
email:'test@test.com',
id: 1
...
}
}
// 실패 케이스
const data = null
dehydrate
함수와 <Hydrate>
컴포넌트를 통해 클라이언트 측에ㅓ서 queryClient의 결과들을 사용할 수 있도록 합니다.Q. 그렇다면 언제 리프레시 토큰의 유효성을 검증하는 것이 좋을까요?
개인적으로는 처음 페이지에 접근하는 경우 검사를 했으면 좋겠다고 생각했습니다.
처음에 한 번 검증을 하면 반환된 값이 바뀔 일이 크게 없기 때문입니다.
어느 페이지에 접근하더라도, 초기 딱 한번은 유효성을 검증하고 싶었기 때문에 _app
에서 SSR과 연관된 메소드를 사용하는 것으로 방향을 잡았습니다.
문제는 _app에서는 getServerSideProps
및 getStaticProps
을 사용하지 못한다는 것입니다.
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에 의존적인 코드 수정
우선 지금까지 작성한 코드 중, 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 변경이 없어집니다~