Tech

Higher-Order Components는 여전히 유용하다

2023. 02. 09

 

 

 

 

안녕하세요. 화해에서 프론트엔드 개발을 하고 있는 박찬민입니다. Higher-Order Components는 여전히 유용하다

 

이번 글에서는 최근에 Higher-Order Components(이하 HOC)를 이용해서 코드의 유지보수성을 높이고 컴포넌트 개발을 용이하게 만든 경험을 공유해보려 합니다.

 

HOC가 무엇인지는 공식 문서나 다른 블로그에서도 많이 알 수 있어서 이 글에서는 더 실질적인 이야기를 해보겠습니다.

 

 


 

HOC는 공식 문서에서도 알 수 있듯이 ‘컴포넌트 로직 재사용’을 위한 패턴입니다. Hooks가 나온 뒤로는 컴포넌트 로직 재사용을 위해서 Hooks를 주로 사용하기 때문에 HOC를 사용하는 경우가 많이 줄었습니다. 저는 React를 늦게 시작한 편이라서 React를 본격적으로 사용할 때는 이미 함수형 컴포넌트와 Hooks가 대중화된 뒤였습니다. 그래서 개발할 때 HOC를 이용할 생각은 잘 못했습니다.

 

그런데, Hooks 만으로는 코드의 재사용성을 높이기 어려운 경우가 있었습니다. 아래에서 어떤 문제 상황을 만났는지 그리고 HOC를 이용해서 어떻게 문제를 해소했는지 소개하겠습니다.

 

 

 

 

배경 상황

어떤 ID값(이하 somethingId)을 URL의 path에 담아서 사용하고 있습니다. 그리고 많은 컴포넌트들이 somethingId를 사용하고 있으면서 somethingId가 없으면 컴포넌트는 랜더링 되지 않아야 합니다.

 

 

 

 

문제 상황

URL에서 somethingId를 가져와서 조건부 랜더링을 하려면 기본적으로 아래의 코드를 작성해야 합니다.

 


import { useParams } from 'react-router-dom';

const SomethingComponent = (props: SomethingComponentProps) => {
	// (1) somethingId를 가져온다.
	const { somethingId } = useParams<somethingidparam>();

	// (2) somethingId가 값이 있는지 확인한다.
	if (!somethingId) {
		// (3) somethingId가 없으면 랜더링 하지 않는다.
		return null;
	}
	return <>
		// ...원래 그리려는 컴포넌트 UI
	</>
}</somethingidparam>

 

위의 코드처럼 다른 컴포넌트 로직이 아무것도 없다면 깔끔하게 somethingId가 없으면 컴포넌트를 랜더링 하지 않아야 한다.라는 요건을 만족하기 위한 코드((1)번부터 (3)번 주석이 표시된 코드)를 작성할 수 있습니다. 서로 근처에 붙어 있음으로써 시간이 흐르거나 작업자가 바뀌어도 요건을 지키기 쉬워집니다. 그러나 현실적으로 어떤 컴포넌트가 추가적인 로직을 갖지 않기는 어렵습니다.   결국 코드는 아래와 같이 거대해집니다.

 

const SomethingComponent = (props: SomethingComponentProps) => {
	// (1) somethingId를 가져온다.
	const { somethingId } = useParams<SomethingIdParam>();

	const history = useHistory();
	const someState = useState();

	const { prop1, prop2 } = props;

	const handleSomething = () => {
		// blahblah...
		// blahblah...
		// blahblah...
		// blahblah...
	};

	useEffect(() => {
		// somethingId를 사용하는 무언가 로직
		if (somethingId) {
			// somethingId가 있을 때의 무언가 로직
		}
		// blahblah...
		// blahblah...
		// blahblah...
	}, [somethingId]);

	// (2) somethingId가 값이 있는지 확인한다.
	if (!somethingId) {
		// (3) somethingId가 없으면 랜더링 하지 않는다.
		return null;
	}
	return <>
		// ...원래 그리려는 컴포넌트 UI
	</>
}

 

Hooks를 사용하게 되면서 early return은 반드시 아래쪽으로 내려가야 하고 컴포넌트 로직에서 somethingId를 사용하기에 (1)번 주석이 달린 코드와 (2), (3)번 주석이 달린 코드가 서로 많이 멀어지게 됩니다. 이렇게 되면 관련된 코드의 문맥 파악이 어려워집니다. 게다가 컴포넌트 로직 내에서 somethingId를 사용하려면 값이 있는지 확인하는 조건문을 꼭 넣어줘야 합니다.

 

위와 같은 상황이 많은 컴포넌트에서 발생하고 있어 그냥 두었다가는 반복적인 코드를 너무 많이 작성하여 유지보수성이 떨어져서 훗날 큰 피를 볼 것 같았습니다.

 

 

 

 

문제 상황 해소 = HOC 도입

제가 달성하고 싶은 목표는 반복 코드를 줄이고 관련 코드를 한 군데 뭉치는 것이었습니다. Hooks는 로직의 재사용성은 높여주지만 조건부 랜더링은 결국 컴포넌트 안에서 작성되어야 해서 목표를 달성할 수 없었습니다. HOC는 코드의 재사용성을 높이면서 컴포넌트를 만들어내는 방법이기에 저에게 딱 적절한 방법이라고 생각했습니다.

 

import type { WithConditionalCSSProp } from '@emotion/react/types/jsx-namespace';

export interface SomethingIdProps {
  somethingId: string;
}

export const withSomethingId = <P extends WithConditionalCSSProp<SomethingIdProps>>(
  WrappedComponent: React.ComponentType<P>,
) => {
  const Component = (props: Omit<P, keyof SomethingIdProps>) => {
    // (1) somethingId를 가져온다.
    const { somethingId } = useParams<SomethingIdProps>();

    // (2) somethingId가 값이 있는지 확인한다.
    if (!somethingId) {
        // (3) somethingId가 없으면 랜더링 하지 않는다.
        return null;
    }

    return <WrappedComponent {...(props as P)} somethingId={somethingId} />;
  };
  return Component;
};

 

 

위의 HOC 코드는 제가 작성한 코드의 거의 전부입니다. 다른 목적의 코드와 섞이지 않아야 하기에 코드는 자연스럽게 작아집니다. HOC를 어떤 컴포넌트에 적용할 때는 아래 코드처럼 간단하게 적용할 수 있습니다.

 

// (1) 컴포넌트의 props type에 somethingId prop을 추가합니다.
interface SomethingComponentProps extends SomethingIdProps { /* ... */ }

const SomethingComponent = (props: SomethingComponentProps) => {
	/* ...components logic */
};

// (2) 컴포넌트를 HOC로 감싸줍니다.
export default withSomethingId(RequiredSomethingIdComponent);

 

 

이렇게 HOC를 만들어서 사용함으로써 아래와 같은 장점을 취했습니다.

  1. 다른 로직은 전혀 개입하지 않고 오로지 somethingId와 관련된 코드만 존재하여 코드의 문맥을 이해하기에 용이합니다.
  2. somethingId가 필요한 컴포넌트에서는 props의 type에 somethingId를 정의하고 컴포넌트를 export 할 때 HOC를 감싸주기만 하면 바로 적용할 수 있습니다.
  3. 컴포넌트 내에서 somethingId는 더 이상 undefined가 아니기 때문에 불필요한 조건문을 사용할 필요가 없습니다.

 

 

 

 

마무리

Hooks와 HOC, Render Props는 코드의 재사용이라는 공통점이 있습니다. 어느 것 하나 필요 없는 방법이 아니고 상황에 따라서 어떤 방법이 적절한지가 달라집니다. 저는 이전까지 HOC가 무엇인지는 알았지만 필요한 상황이 없었기에 경험이 없었습니다. 이번 경험을 통해서 제대로 HOC가 어떨 때 적절한지 잘 배울 수 있었습니다. 이 글을 통해서 다른 분들도 좋은 간접 경험을 하시면 좋겠습니다.

 

 


 

이 글이 마음에 드셨다면 프론트엔드 플랫폼의 다른 콘텐츠도 확인해 보세요!

Mocking으로 프론트엔드 DX를 높여보자

React Hook Form의 isDirty와 dirtyFields를 알아보자

Higher-Order Components는 여전히 유용하다

  • Hooks
  • HOC
  • 프론트엔드
avatar image

박찬민 | Front-end Developer

화해팀에서 Web Application의 Frond-end 개발을 담당하고 있습니다.

연관 아티클