본문 바로가기

프로젝트 회고 🌊

오늘의집 클론코딩 🏠

1. React-Query

- 리액트 쿼리가 버전 업그레이드 되면서 @tanstack/react-query로 이름이 바뀜. 그걸로 설치해주면 된다.

 

1) useQuery, useMutation 커스텀 훅

//usePostQuery.js

export const PostQueryKey = "post";
//게시물 가져오기
export const usePostQuery = (postId) => {
  return useQuery([PostQueryKey, postId], () => getPost(postId), {
    enabled: !!postId,
    staleTime: 3000,
    keepPreviousData: true,
  });
};

//게시물 수정
export const useUpdatePostMutation = () => {
  const queryClient = useQueryClient();

  const { mutate } = useMutation(updatePost, {
    onSuccess: (data) => {
      queryClient.invalidateQueries([PostQueryKey]);
    },
  });
  return mutate;
};

//게시물 삭제
export const useDeletePostMutation = () => {
  const queryClient = useQueryClient();

  const { mutate } = useMutation(deletePost, {
    onSuccess: (data) => {
      queryClient.invalidateQueries([PostsQueryKey]); //전체 포스트 조회 query key로 invalidate ("/")
    },
  });
  return mutate;
};

//게시물 좋아요
export const useLikePostMutation = () => {
  const queryClient = useQueryClient();
  const { mutate } = useMutation(likePost, {
    onSuccess: () => {
      queryClient.invalidateQueries([PostQueryKey]);
    },
  });
  return mutate;
};

api를 호출하는 함수들을 import 해준 뒤 GET요청은 useQuery를 사용하고, 수정 및 삭제는 useMutation을 사용한다. useMutation의 리턴값에서 주로 사용하는 메소드는 mutate 또는 mutateAsync 이기 때문에 필요한 이 함수들을 바로 destructuring 해서 반환한다. 

 

게시물 삭제는 게시물을 삭제한 후, 삭제된 것을 제외한 리스트가 보여지는 "/"로 리다이렉트 되는데 이 때 삭제 이전에 캐싱되어있던 Posts를 보여주면 삭제했던 포스트도 그대로 보여진다.

 

그래서 PostQueryKey가 아닌 PostsQueryKey를 import해서 그거로  invalidate해줬다.

queryClient.invalidateQueries([PostsQueryKey]); //전체 포스트 조회 query key로 invalidate ("/")

 

 

2) 페이지네이션과 Prefetching

 

사용자들이 업로드한 게시물을 볼 수 있는 List로, size와 page를 넘겨줘서 서버에서 페이지네이션을 구현한다. 이때 next page의 데이터 1 사이즈를 미리 가져올 수 있는데 이걸 prefetching이라고 하며 react-query에서 제공한다.

 

https://tanstack.com/query/v4/docs/react/guides/prefetching

 

Prefetching | TanStack Query Docs

If you're lucky enough, you may know enough about what your users will do to be able to prefetch the data they need before it's needed! If this is the case, you can use the prefetchQuery method to prefetch the results of a query to be placed into the cache

tanstack.com

 

tanstack으로 변경된 이후부터는 쿼리키를 배열로 전달해야한다.

const [currentPage, setCurrentPage] = useState(0);

const { data, isLoading, isError } = usePostsQuery(SIZE, currentPage);

  useEffect(() => {
    if (currentPage <= MAXPAGE - 1) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery([PostsQueryKey, currentPage], nextPage, () => {
        getPosts(SIZE, nextPage);
      });
    }
  }, [currentPage, queryClient]);

원래는 각 페이지마다 쿼리키를 통해 데이터가 캐싱된다. 만약 캐싱된 데이터가 없다면 > 버튼 또는 < 버튼을 누르면 페이지가 이동되는데 이 때 계속 화면이 깜빡이게 된다. 이를 방지하기 위해 useEffect로 현재 로컬 상태인 currentPage가 변경될 때 이 queryClient의 prefetchQuery 메소드를 사용해 다음 페이지 포스트를 캐싱한다.

 

 

2. Axios

- Interceptors

특정 api는 request headers에 유효한 토큰을 필요로한다. (글 작성, 댓글 작성, 마이페이지 등) 이때 로컬스토리지에 있는 토큰을 가져와서 바로 보내줄 수 있지만 그 토큰이 유효해서 api 응답이 성공적으로 완료되어있는지에 따라 다르게 핸들링해줄 수 있다.

 

인터셉터의 필요성을 느끼게 된건

api 요청 이후 반환된 에러가 expired Access token인지, expired refresh token인지 또는 그 외의 에러인지에 각각 다르게 대응해야했기 때문이다. 

 

const authInstance = () => {
  const Access = localStorage.getItem("Access");
  const Refresh = localStorage.getItem("Refresh");
  const instance = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
    headers: {
      Access: `Bearer ${Access}`,
      Refresh: `Bearer ${Refresh}`,
    },
  });

  instance.interceptors.response.use(
    (res) => {
      if (res.headers.accesstokenerror) {
        localStorage.setItem(
          "Access",
          res.headers.access.replace("Bearer ", ""),
        );
      }
      return res;
    },
    async (error) => {
      //refresh token만료
      console.log(error);
      localStorage.removeItem("Access");
      localStorage.removeItem("Refresh");
      console.log("refresh token 만료");
      window.location.href = "/login";

      return Promise.reject(error);
    },
  );
  return instance;
};

 

1) access token expired

- 여러방법이 있지만 우리는 백엔드에서 original request를 저장하고, access token이 유효하다면 api 요청을 완료한다. 그런데 토큰 유효시간이 만료됬다면 response body에 새로운 access token을 전달하면서 original request를 수행한다.

그래서 만약 새로운 access 토큰이 날아왔다면 기존 토큰이 만료됬다는 뜻이니 localStorage에 있는 것을 새것으로 교체해주면 된다.

 

2) refresh token expired

- refresh 토큰이 만료됬다면 error를 던져준다. 이 error 메시지를 확인하고 localStorage의 Access token과 Refresh token을 모두 지워버린다. 그리고 "/login"으로 리다이렉트한 뒤 사용자에게 로그인을 요청한다.

 

그리고 이 응답 인터셉터를 여러 api에서 공유하도록 instance로 생성한다. headers에 token을 필요로 하지 않는 api는 baseURI만 들어있는 default instance를  사용한다.

 

const defaultInstance = () => {
  const instance = axios.create({
    baseURL: process.env.REACT_APP_API_URL,
  });
  return instance;
};