CreateBrowserRouter와 횡단 관심사 3 - HOC 사용 리펙토링

myung hun kang·2023년 6월 4일
1

서론


오랜만에 글을 작성한다.
지난 달 외주 프로젝트를 맡게되며 한동안 글을 쓸 정신이 없었다.

이번 글에서는 지난 글에 이어서 3탄으로 라우트와 관련된 관심사 분리를 HOC(Higher Order Component)로 개선한 이야기를 작성해보도록 하겠다.

HOC


class 컴포넌트로 React 개발을 하던때에 많이 사용한 관심사 분리 방식이라고 알고 있다.

상위 컴포넌트가 컴포넌트를 변수로 받아 특정 작업을 통해 변수로 받은 컴포넌트를 조작하고 해당 컴포넌트를 반환하는 구조를 가지고 있다.

함수형 컴포넌트로 개발을 하게되고, 훅을 사용하면서 근래에는 사용하는 예제를 본 적이 별로 없다.

뭐 실무에서는 모르겠다...  그냥 취준생으로서는 많이 접해보지 못했다. 

여하튼 HOC은 authentication이나 style과 같이 횡단으로 처리 할 수 있는 관심사들을 분리하기 편하게 해줘서 매우 쓸모가 있다.

최근까지는 그리 사용할 필요성이 없었지만, 이번 프로젝트를 진행하면서 한 번 사용해봤다.

구조는 현재까지 처리한 관심사 분리와 크게 다르지는 않다.

router.tsx 수정


우선 router.tsx 에서 선언했던 RouterBase 타입을 수정할 필요가 있었다.
기존의 타입은 다음과 같이 선언되어있었다. ( 1탄의 코드 )


interface RouterBase {
  id: number;
  path: string;
  label: string;
  element: React.ReactNode;
  withAuth: boolean;
}

여기서 element 부분이 이제는 ReactNode 이면 안된다.

HOC은 컴포넌트를 받고 컴포넌트를 반환해야한다.
그런데 ReactNode는 React 컴포넌트의 자식 요소로 허용되는 모든 유형을 나타내는 타입이다. 따라서 여기서는 사용이 불가하다.

여기서 element의 타입을 다음과 같이 변환했다.


interface RouterBase {
  id: number;
  path: string;
  label: string;
  element: React.ComponentType;
  withAuth: boolean;
}

그에따라 router 배열을 모습은 다음과 같이 바뀌었다.


const routerData: RouterBase[] = [
  {
    id: 0,
    path: "/",
    label: "Home",
    element: Home,
    withAuth: false,
  },
  {
    id: 1,
    path: "/signin",
    label: "로그인",
    element: SignIn,
    withAuth: false,
  },
  {
    id: 2,
    path: "/afterlogin",
    label: "로그인 후 가능",
    element: Afterlogin,
    withAuth: true,
  },
];

뭐 그냥 <> 만 벗겨낸 모습인데 마우스를 올려서 타입을 확인해보면 알 것이다.

//예시 
(alias) function Home(props: IHomeProps): JSX.Element
import Home

우리가 import 한 Home 컴포넌트를 가져온 것이다.

이렇게 바꾸면 일단 우리가 만들 HOC형태의 AuthGuardLayout.tsx로 옮길 준비가 되었다.

AuthGuardLayout 수정


여기도 당연히 타입 수정이 필요하다.
ReactNode를 props로 받던 interface를 수정해주자

여기서 생각할 점이 있다.
Props로 이제 컴포넌트를 받으니 그냥 React.ComponentType 로 바꿔주면 될 것 같지만 이 컴포넌트에 들어올 컴포넌트가 Props를 받을 경우 해당 Props가 어떻게 정의 되어 있는지 모른다 따라서 제네릭으로 만들어야한다.


interface AuthGuardLayoutProps {  // 기존
  children: React.ReactNode;
}

const AuthGuardLayout: React.FC<AuthGuardLayoutProps> = ({ children }) => {
  
interface AuthGuardLayoutProps<p extends object> { // 변경
  WrappedComponent: React.ComponentType<p>;
}
const AdminGuardLayout = <P extends object>({WrappedComponent}: AuthGuardLayoutProps) =>  {
  

이제 내부 코드를 수정하자.

위 예시인 AuthGuard는 로그인이 되어있는지에 따라서 컴포넌트를 분기해야하기 때문에
해당 작업을 수행하는 함수가 필요하다.


const AuthGuardLayout = <P extends object>({WrappedComponent}: AuthGuardLayoutProps) =>  {
  
  const AdminCheck: React.FC<P> = (props) => {
    const [userProfile, setUserProfile] = useState<string | null>(null);
    const { routeTo } = useRouter();

    const fetchUserProfile = useCallback(() => {
      const userProfileResponse = // 유저 상태 체크

      if (userProfileResponse === null) {
        routeTo("/signin");
        return;
      }
      setUserProfile(userProfileResponse);
    }, [routeTo, setUserToken]);

    useEffect(() => {
      fetchUserProfile();
    }, [fetchUserProfile]);

    if (!userProfile) return <Spinner />;

    return <WrappedComponent {...props} />;
  };

return AdminCheck;
}
  

AuthGuardLayout은 함수형 컴포넌트를 반환하는 함수를 반환한다.

이 내부 함수 AdminCheck 함수에서 사용자를 확인하고 확인되면 해당 컴포넌트를 같이 들어온 props와 함께 반환한다.

마무리


이제 router.tsx에서 createBrowserRouter 부분을 수정하면 된다.

export const routers = createBrowserRouter(
  routerData.map((router) => {
    if (router.withAuth) {
      const AdminGuradElement = AdminGuardLayout(router.element);
      return {
        path: router.path,
        element: <AdminGuradElement />,
      };
    } else {
      return {
        path: router.path,
        element: <router.element/>,
      };
    }
  })
);

이와 같이 특정 작업을 진행하는 HOC을 만들어 씌우면 전체 컴포넌트에 일괄적인 작업을 진행할 수 있다.

HOC에 대해서는 아직 공부를 해야한다.

코드에 부족한 점이 있으니 만약 이 글을 보는 사람이 계시면 참고만 해주길 바란다.

profile
프론트엔드 개발자입니다.

0개의 댓글