💻 Frontend

리액트 고차 컴포넌트(HOC)

category
💻 Frontend

React 고차 컴포넌트

고차 컴포넌트(HOC, Higher Order Component)는 컴포넌트 로직을 재사용하기 위한 React의 고급 기술입니다. React API가 아니며, React의 구성적 특성에서 나오는 패턴입니다.
고차 컴포넌트란, 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수입니다.
 

횡단 관심사 분리

고차 컴포넌트는 횡단 관심사를 분리하는데 사용한다.
잠깐, 횡단 관심사란?
횡단 관심사(cross-cutting concern) 여러 서비스에 걸쳐서 동작해야 하는 코드를 의미합니다. 추가로 애플리케이션 내 여러 핵심 비지니스 로직들에 걸쳐서 실행되어야 하는 동작들을 의미합니다.
notion image
 
횡단 관심사의 대표적인 예시를 보자면 아래와 같습니다.
<App> <Router> <Container> <Page>
  • 전체 어플리케이션을 담당하는 App 컴포넌트 입니다.
  • 브라우저 요청 따라 컴포넌트를 선택하는 것이 Router 컴포넌트 입니다.
  • UI 렌더에 필요한 데이터 처리는 Container 컴포넌트의 역할입니다.
  • UI를 렌더링하는 것은 Page 컴포넌트 입니다.
하지만 아무리 이렇게 계층으로 분리하여도 기능까지 완벽하게 분리할 수는 없습니다. 코드의 중복이 생길 수 밖에 없죠. 이처럼 각 계층을 넘어 공통으로 필요한 관심사가 있는데 이것을 횡단 관심사라고 합니다.
정리하면 어플리케이션 각 계층에서 공통적으로 필요한 문제를 횡단 관심사라고 부를 수 있습니다. 고차 컴포넌트는 이런 공통의 기능을 해결하는 역할을 합니다.

예제

  • 컴포넌트가 렌더링 될 때마다 로그를 기록합니다.
  • Button 컴포넌트를 클릭할 때마다 로그를 기록합니다.
import React, { useEffect } from 'react'; function Header() { useEffect(() => { // 로깅 console.log('[Header] 마운트'); }, []); return <header>Header</header>; }
import React, { useEffect } from 'react'; function Button() { useEffect(() => { // 로깅 console.log('[Button] 마운트'); }, []); const handleClick = () => { // 로깅 console.log('[Button] 클릭'); }; return <button onClick={handleClick}>클릭</button>; }

문제점

여기서 두가지 문제를 짚을 수 있습니다.
  1. 두 컴포넌트가 중복된 코드를 가지는 것입니다. 바로 컴포넌트가 렌더링 되었을 때 로그를 남기는 코드죠. 두 컴포넌트 모두 같은 생명주기 메서드를 사용하고 같은 로그 기록 함수를 호출합니다.
  1. 로깅이 각 컴포넌트 고유의 역할이 아니라는 것입니다. Header와 Button은 헤더 영역 UI를 렌더하고 클릭할 수 있는 버튼을 렌더하는 것만을 책임집니다. 결과물은 두 컴포넌트가 로깅을 해야하지만 코드를 구현할 때 로깅은 컴포넌트 안에 있어서는 안될 것 같다는 것입니다.
이러한 기능은 두 컴포넌트 어디에도 소속되어 있지 않습니다. 에플리케이션 전반에 사용되는 공통의 기능이죠.
이러한 문제를 코차 컴포넌트가 어떻게 해결할 수 있을까요?

해결 방법

  1. 컴포넌트를 래핑해서 프롭으로 전달
    1. 컴포넌트에 횡단 관심사를 추가할 수 있는 함수를 만듭니다. 고차 컴포넌트는 함수 이름이 with로 시작해야합니다.
import React, { useEffect } from 'react'; const withLogging = WrappedComponent => { function WithLogging(props) { useEffect(() => { log("마운트"); }, []); const enhancedProps = { log, }; return <WrappedComponent {...props} {...enhancedProps} />; } function log(message) { console.log(`[${getComponentName(WrappedComponent)}]`, message); } function getComponentName({ displayName, name }) { return displayName || name || "Component"; } return WithLogging; };
withLogging은 컴포넌트를 받아 컴포넌를 반환하는 함수, 즉 고차 컴포넌트입니다.
  1. 이 함수는 인자로 받은 WrappedComponent를 내부에서 만든 WithLogging 컴포넌트에서 사용합니다. 단순히 래핑한 정도로 보일지 모를테지만 WithLogging이 하는 일이 있습니다.
  1. 바로 enhancedProps를 컴포넌트에 추가하는 것이죠. enhanceProps는 컴포넌트의 역할과 무관한 범용적인 log 함수를 전달하는 역할을 합니다.
  1. 마지막으로 새로운 컴포넌트 WithLogging을 반환합니다.
정리
  1. 컴포넌트를 인자로 받기
  1. Props를 생성하기
  1. 인자로 받은 컴포넌트에 생성한 props를 넘겨 새로운 컴포넌트 반환
 
withLogging을 이용해 위의 예제를 다시 작성해 보죠.
const Header = () => <header>Header</header> const Button ({log}) => { const handleClick = () => log("클릭"); return <button onClick={handleClick}>클릭</button>; } const EnhancedHeader = withLogging(Header) const EnhancedButton = withLogging(Button)
HeaderButton 코드가 매우 단순하게 바뀌었습니다. 로깅 기능이 모두 withLogging으로 빠졌기 때문이죠. 컴포넌트 본연의 기능만 남고 어플리케이션 공통의 기능을 고차 함수로 옮긴 셈입니다.
 

마무리

면접 질문

Q: 고차 컴포넌트가 무엇인가요?
A: 컴포넌트를 받아 새로운 컴포넌트를 생성하는 패턴으로, 횡단관심사 문제를 해결하는 역할을 합니다.

References