HOC 패턴

camel·2023년 6월 13일
0
post-thumbnail

오늘 HOC 문법에 대하여 공부하였다.
HOC 문법은 클래스에서 사용하는 것으로 알고 있었는데
알아보니 함수문법에서도 이미 사용하고 있었다.
그래서 오늘 HOC 문법에 대해서 공부하고
좋았던 글을 정리하여서 가져왔다.




HOC(Higher-Order Components

종종 여러 컴포넌트에서 같은 로직을 사용해야 하는 경우가 있다. 이런 로직은 컴포넌트의 스타일시트를 설정하는 것일 수 있고. 권한을 요청하거나. 전역 상태를 추가하는 것일 수 있다.

같은 로직을 여러 컴포넌트에서 재사용하는 방법 중 하나로 고차 컴포넌트 패턴을 활용하는 방법이 있다. 이 패턴은 앱 전반적으로 재사용 가능한 로직을 여러 컴포넌트들이 쓸 수 있게 해 준다.

고차 컴포넌트란 다른 컴포넌트를 받는 컴포넌트를 뜻한다. HOC는 인자로 넘긴 컴포넌트에게 추가되길 원하는 로직을 가지고 있다. HOC는 로직이 적용된 엘리먼트를 반환하게 된다.

여러 컴포넌트에게 동일한 스타일을 적용하고 싶다고 가정하자. 로컬 스코프에 style 객체를 직접 만드는 대신, HOC가 style 객체를 만들어 컴포넌트에게 전달하도록 한다.

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />}
}

const Button = () = <button>Click me!</button>const Text = () => <p>Hello World!</p>const StyledButton = withStyles(Button)
const StyledText = withStyles(Text)

위의 예제에서 Button컴포넌트와 Text컴포넌트를 수정한 StyledButton과 StyledText컴포넌트를 만들었다. 두 컴포넌트 모두 withStyles HOC로 부터 스타일링 로직이 적용되었다.

이전 Container/Presentational Pattern 에서 강아지 이미지 예제를 살펴보자. 이 예제는 강아지 사진 목록을 API로부터 받아와 렌더링하고 있다.

위의 예제에서 사용자 경험을 조금 개선해 보자. 데이터를 받아오는 중에는 "로딩중..." 이라는 메시지를 화면에 보여주고 싶다. DogImages에 직접 기능을 구현하는 대신 고차 컴포넌트 패턴을 활용할 것이다.

withLoader 라는 HOC를 만들어 보자. HOC는 컴포넌트를 인자로 받아 컴포넌트를 반환해야 한다. 아래 예제에서는 데이터 로딩이 끝나고 나서 보여져야 할 엘리먼트를 받는다.

일단 최소로 동작하는 버전의 withLoader를 구현해 보자.

function withLoader(Element) {
  return props => <Element />}

위의 예시에서는 단순히 인자로 전달된 엘리먼트를 그대로 반환하고 있는데. 이것보다 데이터가 불려지는 중인지 여부를 엘리먼트에 넘겨야 한다.

withLoader를 재사용 가능하게 하기 위해 강아지 사진 API를 하드코딩 하지 않고 withLoader의 인자로 전하도록 한다. 이 로더 함수에서는 API응답을 받기 전까진 Loading...메시지를 출력하게 될 것이다.

위의 예시에서 HOC는 컴포넌트와 URL을 받는다.

  1. withLoader의 useEffect훅에서 url 로 API를 호출하여 데이터를 받아오고 있다. 응답이 오기 전 까지 반환되는 엘리먼트는 Loading... 텍스트를 출력하고 있다.
  2. 데이터를 받아오고 나면 data상태를 초기화하게 되므로 인자로 전달되었던 컴포넌트가 화면에 노출된다.

이 기능을 앱에 어떻게 추가할 수 있으며, DogIamges의 목록에는 어떻게 "로딩 중..." 인디케이터를 표시할 수 있을까?

DogImages.js에서 더 이상 DogImages 컴포넌트를 직접 export할 필요가 없다. 그 대신 withLoading HOC로 감싸진 DogImages 컴포넌트를 export하면 된다.

export default withLoading(DogImages)

withLoader HOC는 인자로 데이터를 요청할 URL도 받고 있으므로 위의 코드에서 두 번째 인자에 추가해준다.

`export default withLoader(
  DogImages,
  'https://dog.ceo/api/breed/labrador/images/random/6'
)`

withLoader HOC는 데이터를 prop으로 전달하고 있기 때문에 그것을 통해 강아지 사진 목록을 사용할 수 있다.

이를 이용해서 강아지 사진을 받기 전에 “로딩중…” 이라는 메시지를 보여줄 수 있게 되었다.

고차 컴포넌트 패턴은 동일 로직을 여러 컴포넌트들에 제공할 수 있게 해 준다. withLoader HOC는 컴포넌트와 url 에서 받아오는 데이터에 대해서는 관여하지 않는다. 컴포넌트가 유효하고 API엔드포인트도 정상인 경우 단순히 API호출을 통해 받아온 데이터를 넘길 뿐이다.


Composing

여러 고차 컴포넌트를 조합할 수도 있다. 위의 예제에서 DogImages컴포넌트에 마우스를 올리면 “호버링!” 이라는 텍스트 박스가 나타나도록 하고 싶다고 해 보자.

hovering이라는 prop을 제공하는 HOC를 만들어야 한다. DogImages에서 이 prop을 기준으로 텍스트 노출 여부를 결정하면 된다.

아래 예제에서는 withLoaderHOC에 withHover HOC를 사용하고 있다.

DogImages 엘리먼트는 이제 withHover와 withLoader에서 제공하는 prop을 사용할 수 있다. 따라서 “호버링!” 이라는 텍스트는 이 값을 기준으로 노출하면 된다.

HOC를 사용하는 유명 오픈소스 라이브러리에는 recompose 가 있다. 나중에 혹시 HOC가 훅으로 완전 대체가 가능해 진다면 이 라이브러리는 더 이상 사용되지 않을것이다. 이 글도 마찬가지이다.

Hooks

몇몇 상황에서는 HOC패턴은 React의 훅으로 대체할 수 있다.

위에서 구현했던 withHover HOC를 useHover 훅으로 리펙토링해 보자. 고차 컴포넌트를 사용하는 대신 엘리먼트에 mouseOvermouseLeave 이벤트 핸들러를 추가할 것이다. 또 HOC처럼 엘리먼트를 반환할 수 없으니 ref를 반환하여 이벤트 핸들러를 추가할 엘리먼트를 지정할 수 있도록 한다.

useEffect훅에서 컴포넌트에 이벤트 핸들러를 추가하고 hovering 상태의 값을 상황에 맞게 초기화한다. ref와 hovering 모두 훅에서 반환되어야 한다. 그래야 마우스가 올라갔는지 여부를 알고 싶은 엘리먼트에 이벤트를 추가할 수 있고, hovering 값을 참고하여 텍스트를 노출할 수 있기 때문이다.

DogImages컴포넌트를 감싸는 대신 useHover훅을 직접 사용하여 기능을 구현할 수 있다.

일반적으로는 React의 훅은 HOC패턴을 완전 대체할 수 없다.

대부분의 경우에서 React의 훅은 트리가 깊어지는 상황을 줄일 수 있다. - React 문서

React 문서에서 이야기하는 것 처럼 트리가 깊어지는 상황을 줄일 수 있다. HOC 패턴을 사용하면 컴포넌트의 트리가 깊어지는 경향이 있다.

<withAuth>
  <withLayout>
    <withLogging>
      <Component />
    </withLogging>
  </withLayout>
</withAuth>

컴포넌트 내에서 훅을 직접 사용하여 더 이상 컴포넌트를 래핑하지 않아도 된다.

고차 컴포넌트를 활용하면 동일한 로직을 한 군데 구현하여 여러 컴포넌트에 제공할 수 있다. 훅은 컴포넌트의 내부에서 특정한 동작을 추가할 수 있게 해 주지만. HOC에 비해 버그를 발생시킬 확률을 증가시킨다.

HOC의 사용 사례

  • 앱 전반적으로 동일하며 커스터마이징 불가한 동작이 여러 컴포넌트에 필요한 경우
  • 컴포넌트가 커스텀 로직 추가 없이 단독으로 동작할 수 있어야 하는 경우

Hooks의 사용 사례

  • 공통 기능이 각 컴포넌트에서 쓰이기 전에 커스터마이징 되어야 하는 경우
  • 공통 기능이 앱 전반적으로 쓰이는 것이 아닌 하나나 혹은 몇개의 컴포넌트에서 요구되는 경우
  • 해당 기능이 기능을 쓰는 컴포넌트에게 여러 프로퍼티를 전달해야 하는 경우

장단점

장점

고차 컴포넌트를 사용하면 한 곳에 구현한 로직들을 여러 컴포넌트에서 재사용할 수 있다. 동일 구현을 여러군데에 직접 구현하며 버그를 만들어 낼 확률을 줄일 수 있다. 로직을 한 곳에서 관 리하여 코드를 DRY 하면서 관심사의 분리도 적용할 수 있게 되었다.


단점

HOC가 반환하는 컴포넌트에 전달하는 props 의 이름이 겹칠 수 있다.

function withStyles(Component) {
  return props => {
    const style = { padding: '0.2rem', margin: '1rem' }
    return <Component style={style} {...props} />}
}

const Button = () = <button style={{ color: 'red' }}>Click me!</button>const StyledButton = withStyles(Button)

위의 예제에서. withStyles HOC는 style 이라는 prop을 엘리먼트에 전달하고 있다. 하지만 Button컴포넌트는 이미 style 이라는 prop을 가지고 있다. 이 경우 덮어쓰게 될 것이다. HOC를 만들 땐 이런 상황을 고려해야 하며 prop 병합을 통해 해결한다.

function withStyles(Component) {
  return props => {
    const style = {
      padding: '0.2rem',
      margin: '1rem',
      ...props.style
    }

    return <Component style={style} {...props} />}
}

const Button = () = <button style={{ color: 'red' }}>Click me!</button>const StyledButton = withStyles(Button)

HOC를 여러번 조합하여 사용하는 경우 모든 prop이 안에서 병합되므로 어떤 HOC가 어떤 props에 관련이 있는지 파악하기가 어렵다. 앱의 디버깅이나 규모를 키울 때 방해가 될 수 있다.




끝으로

요새 회사에서 복습 겸 내용 정리를 하고 있는데,
다른 사람한테는 정보 전파용으로 작성하신다고 한다.
그래서 좋은 글을 골라서 편집을 하고, 가져왔다.

끝으로 출처를 남기는데 patterns 내용관련해서 정리한 사이트 중에
이만한 사이트는 못본 것 같다.
추천한다.

출처:
https://patterns-dev-kr.github.io/design-patterns/hoc-pattern/

profile
잘부탁드립니다.

4개의 댓글

comment-user-thumbnail
2023년 6월 18일

잘 보고 갑니다 !

답글 달기
comment-user-thumbnail
2023년 6월 18일

예시가 계속 나와서 보는 재미가 있네욤 ㅋㅋㅋ 고생하셨습니당 !!

답글 달기
comment-user-thumbnail
2023년 6월 18일

어렵네요..! 마지막 링크도 참고해서 더 공부해봐야겠습니다. 잘 읽었습니다~!

답글 달기
comment-user-thumbnail
2023년 6월 18일

좋은 설명과 추천 감사합니당!

답글 달기

관련 채용 정보