Higher-order Component로 중복 코드 줄이기

배준형·2023년 7월 28일
0

Higher-order Component란?

Higher-order component (HOC)는 React에서 컴포넌트 로직을 재사용하기 위한 것으로 컴포넌트를 인자로 받아서 해당 컴포넌트를 감싸고 새로운 기능을 추가하거나 공통되는 props를 넘겨주거나 기존 기능을 수정하는 컴포넌트를 반환하는 함수이다. HOC를 사용하면 컴포넌트 간 코드 재사용을 간단하게 하고, 추상화를 도와준다.

HOC를 사용해볼 수 있는 경우

회사에서 정보보호 취약점 개선안으로 사내 Admin 사이트에서 회원 정보를 다운로드 받으려고 할 때 사유를 입력해야 하는 로직이 추가되어야 했다. 각 다운로드마다 사유를 입력하는 모달을 띄우도록 기획이 완료됐는데, 다운로드를 받을 수 있는 페이지는 총 4개였고, 작업을 모두 마치니 4개의 페이지에 모두 동일한 방식으로 모달이 사용되고 있었다.

page.tsx

const [downloadModalVisible, setDownloadModalVisible] = useState(false);
const [downloadAction, setDownloadAction] = useState<(reason: string) => void>();
const onCloseModal = () => setDownloadModalVisible(false);

const handleDownload = (downloadFunc: () => void) => {
  setDownloadAction(() => downloadFunc);
  setDownloadModalVisible(true);
}

// ... 그 외 컴포넌트 코드

return (
	<>
		// ...
		<PrivateDownloadModal
		  open={downloadModalVisible}
		  onCancel={onCloseModal}
		  handleOk={(reason) => {
		    downloadAction?.(reason);
		  }}
		/>
	</>
)

해당 모달을 적용한 모든 페이지에서 위의 로직이 복사 붙여넣기 하듯 중복되었고, 이런 경우 HOC를 적용한다면 중복되는 코드를 줄일 수 있다.

HOC 컴포넌트 만들기

withPrivateDownloadModal.tsx

import React, { ComponentType, useCallback, useState } from 'react';
import PrivateDownloadModal from './PrivateDownloadModal';

interface PrivateDownloadModalProps {
  handleDownload: (downloadFunc: () => void) => void;
}

const withPrivateDownloadModal = <P,>(WrappedComponent: ComponentType<P & PrivateDownloadModalProps>) => (props: P) => {
  const [downloadModalVisible, setDownloadModalVisible] = useState<boolean>(false);
  const [downloadAction, setDownloadAction] = useState<(reason: string) => void>();
  const onCloseModal = useCallback(() => setDownloadModalVisible(false), []);

  const handleDownload = (downloadFunc: () => void) => {
    setDownloadAction(() => downloadFunc);
    setDownloadModalVisible(true);
  };

  return (
    <>
      <WrappedComponent {...props} handleDownload={handleDownload} />
      <PrivateDownloadModal
        open={downloadModalVisible}
        onCancel={onCloseModal}
        handleOk={(reason) => {
          downloadAction?.(reason);
        }}
      />
    </>
  );
};

export default withPrivateDownloadModal;
  • 해당 HOC는 WrappedComponent 를 매개변수로 한다.
  • 공통된 로직들은 그대로 사용한다.
  • 완전히 공통되지 않은 변수, 함수들은 props로 넘겨주는 형태로 사용한다.
    • 여기선 handleDownload라는 함수를 만들어서 각 페이지에서 사용할 다운로드 액션 함수를 인자로 받아서 적용시킬 예정이다.
  • PrivateDownloadModalWrappedComponent와 함께 렌더링한다.

HOC 사용하기

page.tsx

interface Props {
  handleDownload: (downloadFunc: () => void) => void;
}

const SomePage = ({ handleDownload }: Props) => {
	// ...
};

export default withPrivateDownloadModal(SomePage); // 여기서 HOC로 컴포넌트를 감싸준다.
  • 컴포넌트를 export할 때 HOC로 감싸주면 해당 컴포넌트에 HOC 기능이 활용된다.
  • withPrivateDownloadModal 에선 props로 handleDownload 를 넘겨주었으므로 Props 타입에 추가해주고 사용하면 된다.

결론

이전 회사에서 동일한 경험이 있었다. 그 때도 사용 설명서, 사용 동의 모달을 띄우는 데 API 호출까지 완전히 동일한 모달을 사용했는데 스타일이나 동작이 조금 달라서 컴포넌트를 2개 만들었다가 HOC 형태로 묶은 기억이 있다.

그 때의 기억을 살려 현재 회사에서도 비슷한 로직이 발생하는 경우 HOC로 묶으면 좋을 것 같아서 시도했다.

다만, 공통된 로직을 모두 HOC로 뺀다면 with***(with***(with***(Component))) 이렇게 작성해야 될 수도 있고, 여러 개의 HOC가 겹치게 되면서 Props 충돌이 있을 수도 있다는 점은 주의해야될 것이라 생각한다.

그리고 컴포넌트가 Nested 되므로 디버깅할 때 불리하다. 그래서 정말 불가피한 경우가 아니라면 Hooks로 로직을 따로 빼서 사용하는 것이 더 좋아보인다.

참조

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

1개의 댓글

comment-user-thumbnail
2023년 7월 28일

글 잘 봤습니다.

답글 달기