서비스를 개발하다 보면, 사용자의 로그인 유무에 따라 다른 화면을 보여줘야 하는 경우가 있다.
예를 들면, 로그인하지 않은 사용자에게는 로그인 버튼을 보여주고, 반대로 로그인한 사용자에게는 프로필이나 로그아웃 버튼을 보여주는 식이다.
이번 시간에는 SSR에서 로그인 유무를 확인하는 방법을 공유하고자 한다.
일러두기
해당 글에서는 원활한 소통을 위해 SSR을 Frontend의 Server, CSR을 Fronted의 Client의 의미로 사용할 것이다.
개발환경
- Typescript
- React
- Next.js
- JWT
- axios
- @tanstack/react-query
- nookies
기존 CSR에서 처리하는 방법
SSR에서 처리하는 방법을 알아보기 전에, 기존 CSR에서 어떻게 했는지 소개하겠다.
전통적인 방법
일반적으로 isLoading, user, error 변수를 두고, isLoading과 error에 따라 렌더링 하는 View를 다르게 하는 방식으로 구현한다.
import React, {useState, useEffect} from 'react'
function App() {
// ...
const [isLoading, setLoading] = useState(false);
const [me, setMe] = useState<User | null>(null);
const [error, setError] = useState<AnyError>();
useEffect(() => {
const fetchUser = async () => {
try {
setLoading(true);
const user = await checkMe(); // 로그인 확인 API
setMe(user);
setLoading(false);
} catch (err) {
setError(err);
}
};
fetchUser();
}, []);
// ...
}
export default App;
React-query로 간편하게 구현하기
요즘에는 서버 상태 관리에 react-query를 주로 사용하는데, 코드를 보면 왜 그런지 알 수 있다.
import { useQuery } from "@tanstack/react-query";
import React from "react";
function App() {
// ...
const { data, isLoading, error } = useQuery(["me"], checkMe); // 로그인 확인 API
// ...
}
export default App;
길었던 코드가 한 줄로 줄었다! wow
React-query를 이용한 방법으로 로그인 처리를 해도 문제가 없어 보인다.
그렇다면, 꼭 SSR에서 확인해야 할까?
왜 SSR에서 확인해야 할까?
위의 방식처럼 SSR이 아닌 CSR에서 확인해도 문제가 전혀 없어 보인다. 그렇다면 SSR에서 처리해서 얻을 수 있는 이점이 뭘까?
내 경험으로는 SSR을 이용하면 사용자에게 더 좋은 UX를 제공할 수 있었다.
CSR의 경우 페이지가 모두 렌더링 되고, 처리가 일어나기 때문에 SSR에 비해 UX 퀄리티가 낮다.
예를 들면, 로그인을 하지 않으면 접근할 수 없는 페이지가 있다고 해보자.
CSR의 경우, 로그인을 하지 않은 사용자는 다음의 과정을 겪는다.
- 해당 페이지에 접근한다.
- 다양한 뷰들이 렌더링 되기 시작한다.
- 로그인 유무 확인이 포함된 뷰가 렌더링 되기 시작한다.
- 접근 금지 되어, redirect 된다.
SSR의 경우는 다음과 같다.
- 해당 페이지에 접근한다.
- SSR에서 로그인 유무를 확인한다.
- 접근 금지 되어, redirect 된다.
이처럼 특정 상황에서 SSR이 더 좋은 UX를 제공할 수 있다.
꼭, SSR을 사용할 필요는 없지만 여건이 된다면 사용하는 것을 추천한다.
SSR에서 로그인 유무 확인하기 (Feat. Next.js)
Next.js를 이용해 SSR에서 로그인 유무를 확인하는 법을 소개하겠다.
어떤 방식으로 로그인을 하는가?
JWT를 이용한 방식을 선택했고, token을 cookie로 주고받는 형식으로 로그인을 처리한다.
로그인 확인은 cookie나 headers.autorization에 access token이 있는지 확인하고, validation을 하는 방식으로 진행한다.
예제 코드
// 로그인 유무 확인 API
...
export const checkMe = async (accessToken?: string) => {
const res = await axios.get<Profile>("/me", {
headers: {
Authorization: accessToken ? `Bearer ${accessToken}` : undefined,
},
});
return res.data;
};
// pages/index.tsx
import { GetServerSideProps } from "next";
import { checkMe } from 'api/me'
import nookies from 'nookies'
import React from "react";
function Home({me}) {
...
}
export default Home;
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const cookies = nookies.get(ctx) // cookie 가져오기
const accessToken = cookies.access_token
const me = await checkMe(accessToken) // 로그인 유무 확인 API
if (!me) return { // 로그인 안한 사용자 접근 금지
redirect:{
destination: 'redirect_uri'
}
}
return {
props: {
me
},
};
};
checkMe의 param으로 accessToken을 받는 이유가 궁금할 수 도 있는데,
nookies를 이용하면, cookie를 가져오는 것뿐 아니라 설정도 가능하다. 이를 이용해서 res 객체에 cookie를 설정해 줄 수 있다.
하지만, 그렇게 하는 것보단 param으로 전달하는 것이 더 편하고, 나중에 모바일 애플리케이션을 만들 수도 있어서 이 방식으로 구현했다.
위의 로직을 통해, SSR에서 로그인 유무 확인 API의 결과를 page의 props로 전달받을 수 있다.
props로 전달받게 되면 필연적으로 props-driling이 발생하기 때문에, 나는 주로 React-query와 함께 이용한다.
React-query랑 함께 쓰기
React-query를 이용하면 props-driling을 예방할 수 있다.
Next.js에서 React-query를 이용하는 방법은 React-query 공식 문서를 참고하면 된다.
재사용을 위해, 해당 로직만 따로 분리 작성했다.
// utils/prefetchAuthSSR.ts
import { GetServerSidePropsContext } from "next";
import nookies from "nookies";
import { QueryClient } from "@tanstack/react-query";
import { checkMe } from "api/auth";
export default async function prefetchAuthSSR(
ctx: GetServerSidePropsContext,
queryClient: QueryClient
) {
const cookies = nookies.get(ctx);
const accessToken = cookies.access_token;
await queryClient.prefetchQuery(["me"], () => checkMe(accessToken));
}
SSR을 사용하는 것은 개발자의 선택이다.
그렇지만, 무엇보다 가장 중요한 사용자를 생각하는 것이다.
사용자에게 더 좋은 결과를 가져다주는 선택이 무엇인지 고민해봐야 한다.
의견이나 피드백은 댓글 부탁드립니다!!