Higher-order component (HOC)는 React에서 컴포넌트 로직을 재사용하기 위한 것으로 컴포넌트를 인자로 받아서 해당 컴포넌트를 감싸고 새로운 기능을 추가하거나 공통되는 props를 넘겨주거나 기존 기능을 수정하는 컴포넌트를 반환하는 함수이다. 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를 적용한다면 중복되는 코드를 줄일 수 있다.
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;
WrappedComponent
를 매개변수로 한다.handleDownload
라는 함수를 만들어서 각 페이지에서 사용할 다운로드 액션 함수를 인자로 받아서 적용시킬 예정이다.PrivateDownloadModal
은 WrappedComponent
와 함께 렌더링한다.page.tsx
interface Props {
handleDownload: (downloadFunc: () => void) => void;
}
const SomePage = ({ handleDownload }: Props) => {
// ...
};
export default withPrivateDownloadModal(SomePage); // 여기서 HOC로 컴포넌트를 감싸준다.
withPrivateDownloadModal
에선 props로 handleDownload
를 넘겨주었으므로 Props 타입에 추가해주고 사용하면 된다.이전 회사에서 동일한 경험이 있었다. 그 때도 사용 설명서, 사용 동의 모달을 띄우는 데 API 호출까지 완전히 동일한 모달을 사용했는데 스타일이나 동작이 조금 달라서 컴포넌트를 2개 만들었다가 HOC 형태로 묶은 기억이 있다.
그 때의 기억을 살려 현재 회사에서도 비슷한 로직이 발생하는 경우 HOC로 묶으면 좋을 것 같아서 시도했다.
다만, 공통된 로직을 모두 HOC로 뺀다면 with***(with***(with***(Component)))
이렇게 작성해야 될 수도 있고, 여러 개의 HOC가 겹치게 되면서 Props 충돌이 있을 수도 있다는 점은 주의해야될 것이라 생각한다.
그리고 컴포넌트가 Nested 되므로 디버깅할 때 불리하다. 그래서 정말 불가피한 경우가 아니라면 Hooks로 로직을 따로 빼서 사용하는 것이 더 좋아보인다.
글 잘 봤습니다.