react query 커스텀 훅으로 로그인 유지하기 (with. HOC - 고차 컴포넌트)

드엔트론프·2023년 5월 31일
2
post-thumbnail

들어가며

  • 유저가 로그인하게 되면 헤더에 있는 로그인 / 회원가입마이페이지 / 로그아웃 으로 바뀌어 보여야 했다.

하는 이유

  • 처음 헤더에 넣어 바로 유저 상태를 유지했는데, 헤더가 없다면 어떻게 하겠냐는 물음에 제대로 답하지 못했다.
  • 헤더에 넣기보단, 유저 상태를 유지해야하는 페이지에 훅을 불러오면 되겠다 생각했다.
  • 커스텀 훅을 바로 쓰기 전, HOC로도 가능 할 수 있지 않을까? 라는 물음에 HOC도 먼저 적용해보고 커스텀 훅으로 완성했다.

기존

  • useQuery로 헤더에 담아, 지속적으로 로그인 정보를 확인하고 있었다.
const { data } = useQuery(
        "userInfo",
        async () => {
          const response = await getUserInformation();
          setUserInfor(response);
        }
      );

HOC란,

HOC(고차 컴포넌트)는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.

  • 동일한 패턴이 반복적으로 발생한다고 가정해보자. 그렇게 된다면 이 로직을 한 곳에서 정의하고 많은 컴포넌트에서 로직을 공유할 수 있게 하는 추상화가 필요하게 된다. 이러한 경우에 고차 컴포넌트를 사용하면 좋다.

단점

  • 이런 패턴의 사용은 컴포넌트의 재구성을 강요하며, 코드의 추적을 어렵게 만든다. React 개발자 도구에서 React 애플리케이션을 본다면, providers, consumers, 고차 컴포넌트, render props 그리고 다른 추상화에 대한 레이어로 둘러싸인 “래퍼 지옥(wrapper hell)“을 볼 가능성이 높아진다.
  • HOC를 사용하면 매번 렌더링 될 때마다 HOC가 적용된 컴포넌트가 새로운 인스턴스로 감싸지기 때문에 성능 문제가 발생할 수 있다. 반면에 Custom Hook을 사용하면 더 많은 자유도를 가지며 성능 문제도 적어진다.

HOC를 사용한 코드

  • 위 설명처럼 컴포넌트를 감싸 새 컴포넌트를 반환하는 것이다.
  • 나는 Enrolleds라는 컴포넌트를 withauth로 감싸 반환하는 식으로 구현했다.
//Enrolleds
import withAuth from "@/_helpers/withauth";
import { ContentsTable } from "@/components/mypage/ContentsTable";
import ContentsTableSideBar from "@/components/mypage/ContentsTableSideBar";
import styled from "styled-components";

const Enrolleds = () => {
  return (
    <Container>
      <InnerContainer>
        <ContentsTableSideBar />
        <ContentsTable />
      </InnerContainer>
    </Container>
  );
};

export default withAuth(Enrolleds);

withAuth는 아래와 같이 구현 돼 있다.
크게 보자면 4단계다.

  1. cookie에 담겨있는 accessToken을 확인
  2. useQuery로 api 통신하여 결과값 response 반환
  3. response 값을 recoil set 값으로 전달
  4. WrappedComponent 반환
    • withauth가 props로 받아오는 컴포넌트를 말한다. 여기서는 withauth로 감싸여진 Enrolleds 컴포넌트이다.
import { useRouter } from "next/router";
import { getCookie } from "./getcookie";
import { useQuery } from "react-query";
import { getUserInformation } from "./axiosapi";
import { useResetRecoilState, useSetRecoilState } from "recoil";
import { userInformation } from "../../shared/atoms";

const withAuth = (WrappedComponent: any) => {
  return () => {
    const Router = useRouter();

    const setUserInfor = useSetRecoilState(userInformation);
    const resetUerState = useResetRecoilState(userInformation);

    const accessToken = getCookie("accessToken");
    if (!accessToken) {
      resetUerState();
    }
    const { data } = useQuery(["userInfo"], async () => {
      const response = await getUserInformation();
      setUserInfor(response);
    });
    return <WrappedComponent />;
  };
};

export default withAuth;

문제 1

  1. react-query를 통해 hoc를 구현 → hydration 에러가 발생.

해결

if (typeof window !== "undefined") {} 을 상단에 설정했기에 hydration 오류가 났었다.

  • GPT의 답변: 토큰이 필요하지 않은 경우, HOC는 클라이언트 브라우저에서만 실행되도록 하는 것이 일반적입니다. 이를 위해 if (typeof window !== "undefined") 를 사용하여 클라이언트 브라우저에서만 코드가 실행되도록 구성합니다. 반면, 토큰이 필요한 경우에는 서버에서도 검증해야 하기 때문에 이러한 경우 HOC는 서버에서도 실행되어야 합니다.

문제 2

  1. getServerSideProps 에 적용하기가 어렵다. SSR을 사용하는 부분에서는 에러가 발생한다.
  • 유저에 대한 정보를 리코일로 실행중인데, withAuth로 감싼 getServerSideProps가 리코일을 먼저 알아차리지 못해서인듯하다(맞나?).
    • useState나 useQuery도 같은 문제가 나타난다.
  • 해결방법 ?
    SSR을 쓰는 페이지가 많지 않으니(않다면), 해당 함수를 쓰는 곳에서는 withAuth를 쓰지 않고 직접 적용을 해본다.
    이 방법을 생각했지만, 그렇다면 굳이 withAuth를 쓸 필요가 있을까 싶었다. 더욱이, 위에서도 적었지만 커스텀 훅을 더 추천하는 내용도 봐 왔기에, 커스텀훅으로 다시 작성하였다.

Custom hook

import { useQuery } from "react-query";
import { useResetRecoilState, useSetRecoilState } from "recoil";
import { userInformation } from "../../shared/atoms";
import { getUserInformation } from "@/_helpers/axiosapi";
import { getCookie } from "@/_helpers/getcookie";

export const useUser = () => {
  const setUserInfor = useSetRecoilState(userInformation);
  const resetUerState = useResetRecoilState(userInformation);
  const accessToken = getCookie("accessToken");

  //어디서든 쓸건데,
  //유저정보가 있다면
  //리코일 벨류에 저장해주고
  //유저정보가 없다면
  //리코일 밸류를 리셋해주고

  if (!accessToken) {
    resetUerState();
  }

  return useQuery(
    ["userInfo"],
    async () => {
      const response = await getUserInformation();
      setUserInfor(response);
    },
    {
      enabled: !!accessToken, //accessToken이 있을때만 useQuery가 동작하게 설정
    }
  );
};

enabled: !!accessToken를 설정한 이유
다음의 코드에는 해당 프로젝트에서 설정한 부분이 있기에 추가적으로 설정한 내용들이 있는데, 그 이유는 다음과 같다.

  • 토큰이 있을때만 useQuery를 동작하게 하였다.
    • 로그인이 필요하지 않은 메인페이지 같은 곳에서도 useQuery가 실행된다면, getUserInformation가 401에러를 잡고 로그인을 하라고 요청하기 때문
    • getUserInformation 은 axios로 구현된 api호출 함수인데, 불러올 때 인터셉터를 통해 response, request 모두 동작한다. 그때 불필요한 부분에서도 동작하는 내용이 있어 enable: !!accessToken 을 추가했다.

useUser로 동시접속여부까지 잡아보려 했으나, 위와같은 문제가 발생해 동시접속은 따로 잡을 수 있게 했다.

느낀점

  • HOC가 사실 이해가 잘 가지 않아, 리액트 공식 홈페이지와 블로그글을 참고하며 좀 더 이해할 수 있었다.
    (공식문서에도 나와있지만, HOC는 리액트에서 많이 쓰이는 방식은 아니다)
  • 리팩토링 비슷하게 작업하며, 뭐가 더 나은 방법인지를 고민하는 것에서 더 배워가고 있다고 느낄 수 있었다.

참고

Implement Protected Routes in NextJS
고차 컴포넌트 - react 공식문서(legacy)

profile
왜? 를 깊게 고민하고 해결하는 사람이 되고 싶은 개발자

0개의 댓글