HOC Parttern
2024.06.11
1. HOC Parttern 이란?
HOC(Higher Order Component=고차 구성 요소)는 기존 구성 요소에 추가 기능을 추가하는 사용되기에 유용합니다. 컴포넌트를 입력으로 받아, 새로운 컴포넌트를 반환하여 로직을 모듈화 할 수 있습니다.
복잡한 로직을 모듈화하여 중복코드를 줄이고 재사용성을 높일 수 있어서 DRY(Don't-Repeat-Yourself)원칙에 부합한 패턴입니다.
예를 들어 Error텍스트에 항상 동일한 스타일을 적용할때 응용할 수 있습니다.
function errorStyles(Component) {
return (props) => {
const style = { color: "#FF2400", fontSize: "1rem" };
return <Component style={style} {...props} />;
};
}
const Text = () => <p>Error !</p>;
const ErrorText = errorStyles(Text);
위와 같이 컴포넌트에 적용하면 재사용가능한 추가 구성 요소들을 만들 수 있습니다.
에러 스타일의 공통 부분은 한번에 관리하고, text부분의 컴포넌트를 error페이지 별로 동적으로 구성할 수 있게됩니다.
2. HOC Parttern 예시
- case1
조금더 복잡한 로직를 사용할때 유용합니다.
어떠한 제품 데이터를 가져오고, 로딩하고, 에러를 처리하는 컴포넌트가 있습니다.
const Products = ({ hasError, isLoading, data }) => {
const { products } = data;
if (isLoading) return <p>Loading…</p>;
if (hasError) return <p>Sorry, data could not be fetched.</p>;
if (!data) return <p>No products found.</p>;
return (
<ul>
{products.map((product) => (
<li key={product.id}>{product.name}</li>
))}
</ul>
);
};
const { data, loading, error } = fetchData();
<Products {...{ data, error }} isLoading={loading} />;
이때 제품의 여러가지이고 각 제품들은 화면을 구성하는 UI가 다르지만, 동일하게 데이터를 가져오고 로딩하고 에러를 처리를 해야한다면 다음과 같이 HOC 패턴을 사용할 수 있습니다.
-
HOC 컴포넌트
const withFetchData = (WrappedComponent, url) => { return (props) => { const [data, setData] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); useEffect(() => { setIsLoading(true); fetch(url) .then((response) => { if (response.ok) { return response.json(); } else { throw new Error("Error fetching data"); } }) .then((data) => { setData(data); setIsLoading(false); }) .catch((error) => { setError(error); setIsLoading(false); }); }, [url]); if (isLoading) { return <div>Loading...</div>; } if (error) { return <div>Error: {error.message}</div>; } return <WrappedComponent data={data} {...props} />; }; };
-
HOC 컴포넌트 사용
const ProductList = ({ data }) => ( <ul> {data.map((product) => ( <li key={product.id}>{product.name}</li> ))} </ul> ); const ProductListWithFetchData = withFetchData(ProductList, "/api/products");
이렇게 하면 UI와 API 주소만 있다면, API에서 자동으로 데이터를 가져오고 제품 목록을 렌더링하며, 가져오는 동안 로딩 메시지를 표시하고 오류가 있으면 오류 메시지를 표시하는 컴포넌트를 만들 수 있습니다.
- case 2
그리고 사용자의 개인 정보와 같이 민감한 데이터를 표시하는 경우 사용할 수 있습니다.
특정 페이지에 접근할때, 인증이 되어 있어야한다 했을때 각 페이지 별로 인증 확인 기능을 만드는 것보다 HOC패턴으로 래핑하여 간단하게 처리할 수 있습니다.
// 인증 HOC 컴포넌트
const withAuthentication = (WrappedComponent, authenticationMethod) => {
return (props) => {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isAuthenticating, setIsAuthenticating] = useState(true);
const [authError, setAuthError] = useState(null);
useEffect(() => {
setTimeout(() => {
if (
authenticationMethod === "OTP" ||
authenticationMethod === "PIN" ||
authenticationMethod === "PASSWORD"
) {
setIsAuthenticated(true);
setIsAuthenticating(false);
} else {
setAuthError("Invalid authentication method");
setIsAuthenticating(false);
}
}, 1000);
}, [authenticationMethod]);
if (isAuthenticating) {
return <div>Authenticating...</div>;
}
if (authError) {
return <div>Error: {authError}</div>;
}
if (!isAuthenticated) {
return <div>Please authenticate to view this content</div>;
}
return <WrappedComponent {...props} />;
};
};
// 인증이 필요한 컴포넌트
const UserProfile = () => (
<div>
<h2>User Profile</h2>
</div>
);
const UserProfileWithAuthentication = withAuthentication(UserProfile, "OTP");
// 다음과 같이 인증 확인 적용된 UserProfile 사용할수 있습니다.
<UserProfileWithAuthentication />;
이렇게 하면 독립적으로 인증 시스템을 유지 보수 할수 있게 되니다.
3. 결론
컴포넌트간의 결합도를 줄이며, 독립적으로 작동하게 설계할 수 있습니다.
다만 중첩해서 사용해야하는경우 (withAuth(withLogger(withData(Component))
) 코드가 너무 복잡해지는 단점도 있기는 합니다.
컴포넌트 자체에서 모듈을 분리하는 방식은 커스텀훅이 좀더 가독성과 유지보수성에서 좋은것 같긴하지만, 기존의 컴포넌트에 추가 기능을 부모 컴포넌트에서 확장하고자 할때 유용하게 사용하고 있습니다.
reference
[1] hoc-pattern” https://www.patterns.dev/react/hoc-pattern
[2] “React Design Patterns: Higher Order Components Pattern” 2023.11.26 https://medium.com/@vitorbritto/react-design-patterns-higher-order-components-pattern-421e7f1edcfa