인생마커
article thumbnail

1. 개요

사내 프로젝트를 진행하던 와중, 한 개발자 분이 Higher Order Component(이하 HOC) 패턴으로 어떤 컴포넌트를 작성했습니다.

React Query를 기반으로 데이터 fetching 과 함께 prop drilling으로 response를 조작하여 전달하는 컴포넌트였는데, 
동작에서는 사실상 문제는 없었지만 HOC 패턴은 현대 React 코드에서는 잘 사용하지 않는 패턴입니다.

이는 React 공식 문서 에서도 명시하고 있는 부분입니다.

어떤 디자인 패턴이 왜 나왔는지, 어떤 경우에 사용하는지에 대해서 정확히 인지한다는 것은 단순히 기술을 알고 적용하는 것에 비해서 훨씬 깊은 생각과 경험을 제공하는 것 같습니다.

React 생태계에서 HOC 패턴은 컴포넌트 로직을 재사용하기 위한 강력한 디자인 패턴으로 오랫동안 사용되어 왔습니다. 특히 클래스형 컴포넌트가 주류였던 시절에는 코드 재사용과 관심사 분리를 위한 핵심 패턴이었습니다. 그러나 React 16.8에서 Hook이 도입되면서 상태 관리와 부수 효과를 다루는 방식에 혁명적인 변화가 일어났고, HOC 패턴의 사용 빈도는 크게 감소했습니다.

 

이 글에서는 클래스형 컴포넌트에서 HOC 패턴이 어떻게 유용하게 사용되었는지, Hook의 등장으로 어떻게 패러다임이 변화했는지, 그리고 현대 React 개발에서 HOC의 위치는 어디인지 소소하게 알아보고 정리하려 합니다.

2. HOC 패턴의 이해

HOC는 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수입니다.
이는 JavaScript의 고차 함수(Higher Order Function)와 유사한 개념입니다.

<javascript />
// 기본적인 HOC 패턴 function withExample(WrappedComponent) { return class extends React.Component { render() { return <WrappedComponent {...this.props} />; } }; }

 

HOC 패턴은 횡단 관심사(cross-cutting concerns) 를 처리하는 데 매우 효과적임과 동시에 로깅, 권한 검사, 데이터 fetcing 등과 같은 로직을 여러 컴포넌트에서 재사용할 수 있게 해주었습니다.

 

2.1. 클래스형 컴포넌트에서 HOC의 활용 예제

2.1.1. 예제 1: 데이터 페칭 HOC

<javascript />
function withData(WrappedComponent, dataSource) { return class extends React.Component { constructor(props) { super(props); this.state = { data: null, loading: true, error: null }; } componentDidMount() { this.fetchData(); } fetchData() { this.setState({ loading: true }); fetch(dataSource) .then(response => response.json()) .then(data => { this.setState({ data: data, loading: false }); }) .catch(error => { this.setState({ loading: false, error: error }); }); } render() { const { data, loading, error } = this.state; return ( <WrappedComponent data={data} loading={loading} error={error} {...this.props} /> ); } }; } // 사용 예시 class UserList extends React.Component { render() { const { data, loading, error } = this.props; if (loading) return <div>로딩 중...</div>; if (error) return <div>에러 발생: {error.message}</div>; return ( <ul> {data.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); } } const UserListWithData = withData(UserList, 'https://api.example.com/users');

 

2.1.2. 예제 2: 인증 HOC

<javascript />
function withAuth(WrappedComponent) { return class extends React.Component { constructor(props) { super(props); this.state = { isAuthenticated: false }; } componentDidMount() { // 로컬 스토리지나 쿠키에서 인증 상태 확인 const token = localStorage.getItem('token'); this.setState({ isAuthenticated: !!token }); } render() { const { isAuthenticated } = this.state; if (!isAuthenticated) { return <Redirect to="/login" />; } return <WrappedComponent {...this.props} />; } }; } // 사용 예시 class AdminDashboard extends React.Component { render() { return <div>관리자 대시보드 내용</div>; } } const ProtectedAdminDashboard = withAuth(AdminDashboard);

 

3. HOC 패턴의 한계와 문제점

HOC 패턴은 강력했지만 몇 가지 중요한 한계와 문제점이 있었습니다.

1. Wrapper Hell (래퍼 지옥): 여러 HOC를 중첩해서 사용하면 컴포넌트 계층 구조가 복잡해지고 디버깅이 어려워집니다.

<javascript />
// 여러 HOC를 중첩 사용한 예 const EnhancedComponent = withAuth( withData( withTheme( withTranslation(BaseComponent) ) ) );

 

2. Props 충돌: 여러 HOC가 동일한 이름의 prop을 주입하면 충돌이 발생하여 문제를 일으킬 가능성이 높아집니다.

3. Ref 전달 문제: HOC로 감싸진 컴포넌트에 ref를 사용할 때 또한 문제를 일으킬 수 있었습니다. (React.forwardRef가 나오기 전).

외에도 여러 문제 상황을 직면하고 react는 해결 방법을 제시했습니다.

 

Hook의 개요 – React

A JavaScript library for building user interfaces

ko.legacy.reactjs.org

 

4. React Hooks의 등장과 패러다임 변화

React 16.8에서 Hook이 도입되면서 함수형 컴포넌트에서도 상태 관리와 생명주기 기능을 사용할 수 있게 되었습니다.
이는 HOC 패턴을 대체할 수 있는 새로운 방식을 제공했습니다.

4.1. HOC vs. Custom Hook 비교 데이터 fetching 예제:

<javascript />
function useData(dataSource) { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); useEffect(() => { const fetchData = async () => { try { setLoading(true); const response = await fetch(dataSource); const result = await response.json(); setData(result); setLoading(false); } catch (err) { setError(err); setLoading(false); } }; fetchData(); }, [dataSource]); return { data, loading, error }; } // 사용 예시 function UserList() { const { data, loading, error } = useData('https://api.example.com/users'); if (loading) return <div>로딩 중...</div>; if (error) return <div>에러 발생: {error.message}</div>; return ( <ul> {data && data.map(user => ( <li key={user.id}>{user.name}</li> ))} </ul> ); }

 

인증 예제:

<javascript />
function useAuth() { const [isAuthenticated, setIsAuthenticated] = useState(false); useEffect(() => { const token = localStorage.getItem('token'); setIsAuthenticated(!!token); }, []); return isAuthenticated; } // 사용 예시 function AdminDashboard() { const isAuthenticated = useAuth(); if (!isAuthenticated) { return <Navigate to="/login" />; } return <div>관리자 대시보드 내용</div>; }

5. Hook이 HOC보다 선호되는 이유

Hook을 사용하면 합성과 관심사 분리, 컴포넌트와 로직의 관계가 더 명확해지고 여러 Hook을 조합해도 컴포넌트 계층이 깊어지지 않습니다.
이는 HOC처럼 컴포넌트를 여러 겹으로 감싸는 구조가 아니라서 코드의 가독성도 향상됩니다.

예를 들어, 인증과 데이터 로딩 기능이 모두 필요한 컴포넌트의 경우:

<javascript />
// HOC 방식 - 중첩된 래핑이 필요함 const UserProfileWithAuthAndData = withAuth(withData(UserProfile, '/api/user')); // Hook 방식 - 단일 컴포넌트 내에서 직접 조합 가능 function UserProfile() { const isAuthenticated = useAuth(); const { data, loading } = useData('/api/user'); // 나머지 로직... }


Props 관리 측면에서도 Hook이 더 간단합니다.
HOC는 자동으로 props를 전달하는 과정에서 이름 충돌이 발생할 수 있지만, Hook은 필요한 값만 명시적으로 반환하고 사용하기 때문에 이런 문제가 훨씬 적습니다.

또한 여러 HOC를 사용할 때 어떤 prop이 어디서 왔는지 추적하기 어려워지는 문제도 Hook에서는 발생하지 않습니다.
이로 인해 어떤 데이터가 어디서 오는지 더 명확하게 파악할 수 있습니다.

TypeScript와 같은 정적 타입 언어와 함께 사용할 때도 Hook이 더 자연스럽습니다.

HOC는 타입 정의가 복잡한 반면, Hook은 일반 함수처럼 작동하므로 타입 추론이 더 간단하고 직관적입니다.

<javascript />
// HOC의 타입 정의 - 복잡한 제네릭 타입이 필요함 interface WithDataProps<T> { data: T; loading: boolean; error: Error | null; } function withData<T, P extends object>( WrappedComponent: React.ComponentType<P & WithDataProps<T>>, fetchFn: () => Promise<T> ): React.ComponentType<Omit<P, keyof WithDataProps<T>>> { // 구현... } // Hook의 타입 정의 - 직관적이고 간단함 function useData<T>(fetchFn: () => Promise<T>) { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState<boolean>(true); const [error, setError] = useState<Error | null>(null); // 구현... return { data, loading, error }; }

 

디버깅 측면에서도 Hook이 우위를 가집니다.
HOC를 여러 개 중첩해서 사용하면 React DevTools에서 컴포넌트 구조가 복잡해지지만, Hook은 그런 문제가 없습니다. 문제가 발생했을 때 어떤 Hook이 원인인지 추적하기도 더 쉽습니다.

예를 들어, React DevTools에서 HOC로 감싸진 컴포넌트는 다음과 같이 여러 층의 래퍼 컴포넌트가 표시되어 디버깅이 복잡해질 수 있습니다.

<javascript />
ConnectFunction > WithTheme > WithTranslation > WithAuth > WithData > ActualComponent


반면, Hook을 사용하면 컴포넌트 계층이 평평하게 유지되어 디버깅이 더 직관적입니다.

마지막으로, 테스트 작성도 Hook이 더 용이합니다.

일반 함수처럼 독립적으로 테스트할 수 있어 복잡한 설정 없이도 단위 테스트를 효과적으로 작성할 수 있습니다.

<javascript />
import { renderHook, act } from '@testing-library/react-hooks'; import useCounter from './useCounter'; test('useCounter increments count', () => { const { result } = renderHook(() => useCounter()); act(() => { result.current.increment(); }); expect(result.current.count).toBe(1); });

 

6. 현대 React에서 HOC의 위치

Hook이 많은 경우에 HOC를 대체했지만, HOC가 완전히 사라진 것은 아닙니다.
특히 다음과 같은 경우에는 여전히 HOC가 유용할 수 있습니다.

  1. 클래스 컴포넌트와의 호환성: 레거시 코드베이스에서는 HOC가 여전히 중요한 역할을 합니다.
  2. 라이브러리 통합: 일부 라이브러리(예: Redux의 connect)는 HOC 패턴을 사용합니다.
  3. Rendor Props 패턴과의 조합: 특정 패턴에서는 HOC와 Render Props와 함께 사용하는 것이 효과적일 수 있습니다.
<javascript />
// Redux connect HOC 예제 import { connect } from 'react-redux'; class TodoList extends React.Component { render() { const { todos, toggleTodo } = this.props; return ( <ul> {todos.map(todo => ( <li key={todo.id} onClick={() => toggleTodo(todo.id)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text} </li> ))} </ul> ); } } const mapStateToProps = state => ({ todos: state.todos }); const mapDispatchToProps = dispatch => ({ toggleTodo: id => dispatch({ type: 'TOGGLE_TODO', id }) }); export default connect(mapStateToProps, mapDispatchToProps)(TodoList);

 

React Hooks가 나온 후에는 useSelector와 useDispatch Hook을 사용하는 방식으로 변경되었습니다:

<javascript />
import { useSelector, useDispatch } from 'react-redux'; function TodoList() { const todos = useSelector(state => state.todos); const dispatch = useDispatch(); const toggleTodo = id => { dispatch({ type: 'TOGGLE_TODO', id }); }; return ( <ul> {todos.map(todo => ( <li key={todo.id} onClick={() => toggleTodo(todo.id)} style={{ textDecoration: todo.completed ? 'line-through' : 'none' }} > {todo.text} </li> ))} </ul> ); }

7. 결론

HOC 패턴은 React Hook이 등장하기 전 코드 재사용과 관심사 분리를 위한 유용한 디자인 패턴이었으나,

React Hook의 등장 이후 상태 관리와 부수 효과를 보다 더 쉽게 공유할 수 있게 되면서 현대에는 잘 사용하지 않는 패턴이 되었습니다.

다만, 개발자들이 언제나 자신이 원하는 개발 환경에 놓이는 것 역시 아닙니다.

HOC 패턴이 분명 필요한 상황 혹은 레거시 코드를 만져야 하는 상황에서 이러한 디자인 패턴을 인지하고 있는지는 확실한 무기가 될거라고 생각합니다.

'React' 카테고리의 다른 글

Airbnb (react/function-component-definition)  (0) 2023.11.09
[Next.js] 동적 라우팅을 사용하는 정적 사이트의 site map  (0) 2022.05.11
[Next.js] SSG(ISR), SSR 비용 효율  (0) 2022.05.08
SWR  (0) 2022.02.18
profile

인생마커

@Cottonwood__

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!