HOC 패턴 이제 안써도 된답니다.👏🏻

이제 레거시 패턴이 되긴 했지만 그래도 어떻게 사용하고 언제 사용해야할지는 알아야하지 않겠습니까? 레거시 코드를 많이 마주치는 상황이 오기 때문이죠.


HOC(Higher-Order Components)

앱 전반적으로 재사용한 로직을 prop으로 컴포넌트에게 제공

🚀 컴포넌트는 props를 UI로 변환하는 반면에, 고차 컴포넌트는 컴포넌트를 새로운 컴포넌트로 변환합니다.

우리는 여러 컴포넌트에서 같은 로직을 사용해야 하는 경우가 있습니다. 예를 들어, 컴포넌트의 스타일 시트를 설정할 수도 있고, 상태를 추가해야하는 경우가 있죠.

같은 로직을 여러 컴포넌트에서 재사용하는 방법 중 하나로 고차 컴포넌트 패턴을 활용하는 방법이 있습니다.

고차 컴포넌트

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

네..? 무슨 말인지 잘 모르겟는데요
.


예를 들어, 여러 컴포넌트에게 동일한 스타일을 적용하고 싶다고 가정해봅시다.

로컬 스코프에 style 객체를 직접 만드는 대신, HOC가 style 객체를 만들어 컴포넌트에 전달하도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function withStyles(WrappedComponent) {
// 로직이 적용된 엘리먼트를 반환한다.
return () => {
const style = { padding: '100px', margin: '1rem', fontSize: '30px' };
return (
<div style={style}>
<WrappedComponent />
</div>
);
};
}

const Button = () => <button>Click me!</button>;
const Text = () => <p>Hello word!</p>;

const StyledButton = withStyles(Button);
const StyledText = withStyles(Text);

function App() {
return (
<div>
<StyledButton />
<StyledText />
</div>
);
}

.
Button컴포넌트와 Text컴포넌트를 수정한 StyledButton과 StyledText컴포넌트를 만들었습니다. 두 컴포넌트 모두 withStyles HOC로부터 스타일링 로직이 적용된거죠!


강아지 사진 목록을 API로부터 받아와서 렌더링을 하는 또 하나의 예제를 봐봅시다.

UX를 개선하기 위해서 데이터를 받아오는 중에는 “로딩 중”이라는 메세지를 화면에 보여주고 싶다면 어떻게 해야할까요?

지금 드는 아이디어로는 useState를 사용해서 데이터를 받아오기 전엔 Loader 컴포넌트를 렌더링하고 데이터가 다 받아와지면 이미지 컴포넌트를 렌더링시키면 될 것 같습니다.


고차 컴포넌트를 활용하면?

먼저 withLoader라는 HOC를 만듭니다.

HOC는 컴포넌트를 인자로 받아 컴포넌트를 반환해야하는 것 잊지 마세요!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// withLoader.js
import React, { useEffect, useState } from 'react';

export default function withLoader(Element, url) {
return (props) => {
const [data, setData] = useState(null);

useEffect(() => {
async function getData() {
const res = await fetch(url);
const data = await res.json();
setData(data);
}

getData();
}, []);

// API응답을 받기 전까진 `Loading...`메시지를 출력
if (!data) {
return <div>Loading...</div>;
}

return <Element {...props} data={data} />;
};
}

위 예제에서 HOC는 컴포넌트와 URL을 받습니다.

  1. withLoader의 useEffect 훅에서 url로 API를 호출하여 데이터를 받아오고 있습니다. 응답이 오기 전까지는 Loading… 텍스트를 렌더링합니다.

  2. 데이터를 받아오고 나면 data 상태를 초기화하게 되므로 인자로 전달되었던 컴포넌트가 화면에 렌더링됩니다.


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

1
2
3
4
export default withLoader(
DogImages,
'https://dog.ceo/api/breed/labrador/images/random/6'
);

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

고차 컴포넌트 패터은 동일 로직을 여러 컴포넌트들에 제공할 수 있게 해줍니다. withLoader HOC는 컴포넌트와 url에서 받아오는 데이터에 대해서는 관여하지 않고 받아온 데이터를 넘길 뿐이죠.

고차 컴포넌트를 조합해보자!

여러 고차 컴포넌트를 조합해서 사용할 수도 있습니다. 위 예제에서 DogImages 컴포넌트에 hover 효과를 적용시켜보자구요.

hovering이라는 prop을 제공하는 HOC를 만들어야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function DogImages(props) {
return (
<div {...props}>
{props.hovering && <div id="hover">Hovering!</div>}
<div id="list">
{props.data.message.map((dog, index) => (
<img src={dog} alt="Dog" key={index} />
))}
</div>
</div>
);
}

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

DogImages에서 이 prop을 기준으로 hover 효과를 결정하면 됩니다.

DogImages 엘리먼트는 이제 withHover와 withLoader에서 제공하는 prop을 사용할 수 있습니다.


쉽게 이해가 가진 않는군요…
.

Hooks

몇몇 상황은 React의 훅으로 대체할 수 있습니다.

위에서 구현했던 withHover HOC를 useHover 훅으로 대체해보면,

  • 고차 컴포넌트를 사용하는 대신 엘리먼트에 mouseOver, mouseLeave이벤트 핸들러 추가

  • HOC처럼 엘리먼트를 반환할 수 없으니, ref를 반환하여 이벤트 핸들러를 추가할 엘리먼트를 지정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// useHover.js
import { useState, useRef, useEffect } from 'react';

export default function useHover() {
const [hovering, setHover] = useState(false);
const ref = useRef(null);

const handleMouseOver = () => setHover(true);
const handleMouseOut = () => setHover(false);

useEffect(() => {
const node = ref.current;
if (node) {
node.addEventListener('mouseover', handleMouseOver);
node.addEventListener('mouseout', handleMouseOut);

return () => {
node.removeEventListener('mouseover', handleMouseOver);
node.removeEventListener('mouseout', handleMouseOut);
};
}
}, [ref.current]);

return [ref, hovering];
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// withLoader.js
import React, { useEffect, useState } from 'react';

export default function withLoader(Element, url) {
return (props) => {
const [data, setData] = useState(null);

useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((data) => setData(data));
}, []);

if (!data) {
return <div>Loading...</div>;
}

return <Element {...props} data={data} />;
};
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import withLoader from './withLoader';
import useHover from './useHover';

function DogImages(props) {
const [hoverRef, hovering] = useHover();

return (
<div ref={hoverRef} {...props}>
{hovering && <div id="hover">Hovering!</div>}
<div id="list">
{props.data.message.map((dog, index) => (
<img src={dog} alt="Dog" key={index} />
))}
</div>
</div>
);
}

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

+) usehooks 라이브러리를 설치해서 useHover hook을 사용할 수도 있습니다.


HOC 패턴 장점은?

고차 컴포넌트를 사용하면 한 곳에 구현한 로직들을 여러 컴포넌트에서 재사용할 수 있다.

단점은요?

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

👉🏻 HOC패턴의 단점

👉🏻 리액트의 Hooks과 HOC, HOC의 사용이 복잡해지는 경우


그래서 HOC 패턴 언제 사용하는거죠?

  • 앱 전반적으로 동일하며 커스터마이징 불가한 동작이 여러 컴포넌트에 필요한 경우

  • 컴포넌트가 커스텀 로직 추가 없이 단독으로 동작할 수 있어야 하는 경우


Referrence