-
[React] HOC (고차 컴포넌트, Higher-Order Component)front-end/React 2023. 8. 18. 12:49

HOC 란?
리액트의 HOC(고차 컴포넌트, Higher-Order Component)는 컴포넌트 간의 로직을 재사용하기 위한 패턴입니다.
HOC는 함수이며, 인자로 컴포넌트를 받아들이고, 새로운 컴포넌트를 반환합니다. 이렇게 반환된 컴포넌트는 원본 컴포넌트의 기능을 감싸거나 수정하는 데 사용됩니다.
우선 리액트 공식문서에서 설명이 잘 나와있어서 아래의 이미지를 스크랩 해왔습니다.

HOC 활용 및 활용 이유
함수 반환: HOC는 함수입니다. 이 함수는 인자로 컴포넌트를 받아들이고, 새로운 컴포넌트를 반환합니다.
로직 분리: HOC를 사용하여 여러 컴포넌트에서 중복되는 로직을 분리하여 재사용할 수 있습니다. 예를 들어, 인증, 권한 관리, 로깅 등과 같은 기능을 여러 컴포넌트에서 공통으로 사용하려고 할 때 HOC를 사용할 수 있습니다.
컴포넌트 감싸기: HOC는 원본 컴포넌트를 감싸는 형태로 동작합니다. 이로 인해 기존 컴포넌트의 기능을 확장하거나 수정할 수 있습니다. 인자 전달: HOC는 인자를 전달하여 컴포넌트에 추가 데이터나 기능을 제공할 수 있습니다. 이를 통해 컴포넌트 간의 상호작용을 구현할 수 있습니다. 명명 규칙: 보통 HOC는 이름 앞에 "with"를 붙여 명명됩니다. 예를 들어, withDoSomething 와 같은 이름을 가질 수 있습니다.
컴포지션: 여러 HOC를 함께 사용하여 여러 개의 기능을 한 컴포넌트에 적용할 수 있습니다. 이러한 컴포지션은 컴포넌트 간의 강력한 로직 재사용을 가능케 합니다.
HOC를 사용하면 여러 컴포넌트 간에 공통으로 사용되는 코드를 중앙에서 관리할 수 있어 유지보수성이 향상되고, 코드 중복을 줄일 수 있습니다.
또한 HOC는 상속을 사용하지 않고도 컴포넌트의 기능을 확장하거나 수정할 수 있어 유연한 코드를 작성하는 데 도움이 됩니다. 리액트 훅의 도입으로 함수형 컴포넌트에서도 상태 관리와 사이드 이펙트 처리가 용이해져서 클래스 컴포넌트를 대체하는 경우가 많아졌지만, HOC는 여전히 유용한 상황이 있을 수 있습니다.
규모가 큰 애플리케이션에서 state와 setState 를 호출하는 동일한 패턴이 반복적으로 발생한다고 가정해봅시다. 그렇게 된다면 이 로직을 한 곳에서 정의하고 많은 컴포넌트에서 로직을 공유할 수 있게 하는 추상화가 필요하게 됩니다.
이러한 경우에 고차 컴포넌트를 사용하면 좋습니다. from React 공식문서
예시
HOC 패턴의 이해를 돕기 위해 직접 코드로 간단한 예시를 만들어봤습니다.
HOC 패턴은 클래스형 컴포넌트와 함수형 컴포넌트 둘다 가능하지만 , hooks의 사용을 편하게 하기위해 함수형으로 예제로 준비해봤습니다.
아래의 두가지 코드는 간단히 num state를 increseNum 이란 함수를 통해서 +1 씩 증가시키는 간단한 예제입니다.
제목이 First, Second 라는 것만 다르고 , 로직과 UI는 똑같습니다.
Before applying HOC
//Frist.tsx import { useState } from "react" export default function Frist() { const [num, setNum] = useState<number>(0); const increaseNum = () => { setNum(prev => prev + 1) } return ( <div> <h1>First / {num}</h1> <button onClick={increaseNum}>Increase First Number</button> </div> ) }// Second.tsx import { useState } from "react"; export default function Second() { const [num, setNum] = useState<number>(0); const increaseNum = () => { setNum(prev => prev + 1) } return ( <div> <h1>SECOND / {num}</h1> <button onClick={increaseNum}>Increase Second Number</button> </div> ) }이렇게 비슷한 로직을 추가로 컴포넌트를 통해 구현을 해야할때, 지금은 두개라서 useState와 setState의 로직을 하나씩 작성해줬지만 만약에 10개,100개가 넘어가면 중복성이 너무 심할것같다는 생각이 들건데요.
여기서 인자로 컴포넌트를 받아들이고, 새로운 컴포넌트를 반환해서, 컴포넌트 재사용성이 높은 HOC 패턴을 이용해 중복로직을
줄여보겠습니다.
아래와 같이 HOC.tsx 파일을 하나 만들고 위의 num 을 증가시키는 로직 (num state 와 increase 함수)를 작성해줍니다.
여기서 중요한것은 HigherOrderComponent의 인자로는 나중에 Wrapping할 컴포넌트를 전달 해준다는 것 입니다.
// HOC.tsx import React, { useState } from 'react' interface CounterProps { num: number; increaseNum: () => void; } const WithHihgerOrderComponent = (Counter: React.ComponentType<CounterProps>) => { const HigherOrderComInner = () => { const [num, setNum] = useState(0); const increaseNum = () => { setNum(prev => prev + 1) } return ( <Counter num={num} increaseNum={increaseNum}/> ) } return HigherOrderComInner; } export default WithHihgerOrderComponent;이제 Frist , Second 컴포넌트에서 HigherOrderComponent로 감싸주기만 한다면 기능은 똑같이 작동합니다.
그전에 , 기존에 First, Second 컴포넌트에서 중복되었는 useState 와 increaseNum의 함수를 HOC에서 컨트롤 하고 있으니
먼저 지워준뒤 , HOC에서 전달받은 props로 num의 state 와 increaseNum 으로 기능 구현을 해줍니다.
After applying HOC
// After wrapping HOC First.tsx import WithHihgerOrderComponent from "./HOC"; interface CounterProps { num: number; increaseNum: () => void; } function First({num,increaseNum}:CounterProps) { return ( <div> <h1>SECOND / {num}</h1> <button onClick={increaseNum}>Increase Second Number</button> </div> ) } export default WithHihgerOrderComponent(First);// After wrapping HOC Second.tsx import WithHihgerOrderComponent from "./HOC"; interface CounterProps { num: number; increaseNum: () => void; } function Second({num,increaseNum}:CounterProps) { return ( <div> <h1>SECOND / {num}</h1> <button onClick={increaseNum}>Increase Second Number</button> </div> ) } export default WithHihgerOrderComponent(Second);예시2
이번 예시는 jsonplaceholder dummy api 를 이용해 user 리스트와 todo 리스트를 갖고오는 컴포넌트를 하나씩 생성해서 데이터를 불러와보겠습니다.
이번 예제 역시 불러오는 api 엔드포인트만 다르고 모든 로직 및 UI 는 같습니다.
Before applying HOC
// User.tsx import { useEffect, useState } from "react" interface UserItem { id:number; name:string; username:string; email:string; } export default function User() { const [user, setUser] = useState<UserItem[]>([]); useEffect(() => { const getData = async () => { const data = await fetch('https://jsonplaceholder.typicode.com/users'); const json = await data.json(); setUser(json); }; getData(); }, []); console.log(user); return ( <div> <h1>Users</h1> <ul> {user?.slice(0, 10).map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ) }// Todo.tsx import { useEffect, useState } from "react" interface TodoItem { userId: number; id: number; title: string; completed: boolean; } export default function Todo() { const [todo, setTodo] = useState<TodoItem[]>([]); useEffect(() => { const getData = async () => { const data = await fetch('https://jsonplaceholder.typicode.com/todos'); const json = await data.json(); setTodo(json); }; getData(); }, []) return ( <div> <h1>To do</h1> <ul> {todo?.slice(0, 10).map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </div> ) }위와 코드는 api 엔드포인트만 다르죠? User 컴포넌트는 users , Todo 컴포넌트는 todos 를 통해 각 데이터를 호출 하고있습니다.
useEffect를 통해 데이터를 빈 state에 setState를 통해 데이터를 채워주는 로직이 똑같습니다.
위의 코드를 HOC 패턴을 이용해 코드의 중복성을 줄이고 재사용성을 높여 보겠습니다.

위의 예제와 완전 똑같지만 , API 의 endpoint를 동적으로 받아와야하기 때문에 인자에 endpoint를 하나 더 생성 해줬습니다.
HOC 컴포넌트의 list의 api 호출한 결과값을 데이터에 담고 props로 전달해줍니다.
// HOC.tsx import React, { useEffect, useState } from 'react'; export interface UserItem { id: number; name: string; } export interface TodoItem { id:number; title:string; } export interface ListProps<T> { list: T[]; } const WithHihgerOrderComponent = <T extends UserItem | TodoItem>( DataList: React.ComponentType<ListProps<T>>, endpoint: string ) => { const HigherOrderComponentInner = () => { const [list, setList] = useState<T[]>([]); useEffect(() => { const getData = async () => { const data = await fetch(`https://jsonplaceholder.typicode.com/${endpoint}`); const json = await data.json(); setList(json); }; getData(); }, []); return <DataList list={list} />; }; return HigherOrderComponentInner; }; export default WithHihgerOrderComponent;After applying HOC
// Todo.tsx import WithHihgerOrderComponent, { ListProps, TodoItem } from "./HOC" function Todo({list}:ListProps<TodoItem>) { return ( <div> <h1>To do</h1> <ul> {list.slice(0, 10).map((todo) => ( <li key={todo.id}>{todo.title}</li> ))} </ul> </div> ) } export default WithHihgerOrderComponent(Todo,'todos')// User.tsx import WithHihgerOrderComponent, { ListProps, UserItem } from "./HOC" function User({ list }:ListProps<UserItem>) { return ( <div> <h1>User</h1> <ul> {list.slice(0, 10).map((user) => ( <li key={user.id}>{user.name}</li> ))} </ul> </div> ) } export default WithHihgerOrderComponent(User, 'users')HOC를 적용하고나서의 화면

같은 로직과 같은 UI가 중복된점이 있으면 HOC를통해 보다 나은 (유지보수성) 을 고려하면서 코드를 짤 수 있습니다.
HigherOrderComponent의 두번째 인자로 API endpoint를 넣어서 하나의 컴포넌트에서 동적으로 데이터를 받아옴으로서 재사용성이 높은, 중복을 줄일 수 있는 코드를 작성할 수 있습니다.
HOC 패턴을 사용하면 코드의 재사용성, 모듈화, 분리된 관심사, 컴포지션 등 여러 가지 장점을 얻을 수 있습니다. 이로 인해 코드의 유지보수성과 가독성이 향상되며, 확장 가능한 애플리케이션을 개발할 수 있습니다.
잘못된 부분이 있으면 언제든지 알려주시길 바랍니다. :)
'front-end > React' 카테고리의 다른 글
[React] useDebounce custom hook 으로 성능향상 해보기 (feat: Debounce) (0) 2023.09.13 [React] 왜 Create React App 대신에 Vite를 사용할까? (0) 2023.08.23 [React]When to use UseMemo and useCallback (feat:memo) / useCallback 편 (0) 2023.08.09 [React validation] Zod vs Yup 비교 / react-hook-form 같이 쓰기 (0) 2023.06.05 [React] React에서 흔히 발생하는 실수 4가지 (1) 2023.05.17