HOC 패턴
HOC 패턴은
High Order Component
로, 즉 고차컴포넌트라고 합니다. ‣ 리액트 공식문서를 보면 고차 컴포넌트는 컴포넌트를 가져와 새 컴포넌트를 반환하는 함수
라고 기술되어있습니다. 좀 더 쉽게 말하면 다른 컴포넌트를 받는 컴포넌트라고 할 수 있습니다. HOC는 인자로 넘긴 컴포넌트에게 추가되길 원하는 로직을 가지고 있고, 로직이 적용된 엘리먼트를 반환하게 되는 것입니다.
예)
const EnhancedComponent = higherOrderComponent(WrappedComponent);
컴포넌트는 Props를 UI로 변환하는 반면에, 고차 컴포넌트는 컴포넌트를 새로운 컴포넌트로 변환합니다.
1. 사용이유
- 종종 여러 컴포넌트에서 같은 로직을 사용해야하는 경우에 많이 쓰입니다. 그렇기 때문에 여러 컴포넌트에서 재사용하는 방법 중 하나입니다.
- 예를 들면, 컴포넌트의 스타일 시트를 설정하는 경우, 권환을 요청하거나 전역 상태를 추가하는 경우가 될 수도 있습니다.
- 예1) 여러 컴포넌트에게 동일한 스타일을 적용하고 싶을 때
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)
=> StyledButton와 StyledText, 이 두 컴포넌트는 모두withStyles HOC로부터 스탕일링 로직이 적용되었다고 할 수 있습니다.
2. 예시로 보는 HOC
[1. 원래 코드] 강아지 사진 목록을 API로부터 받아와 렌더링 하기
//index.js import React from "react"; import { render } from "react-dom"; import DogImages from "./DogImages"; import "./styles.css"; function App() { return ( <div className="App"> <DogImages /> </div> ); } render(<App />, document.getElementById("root"));
//DogImage.js import React from "react"; import useDogImages from "./useDogImages"; export default function DogImages() { const dogs = useDogImages(); return dogs.map((dog, i) => <img src={dog} key={i} alt="Dog" />); }
//useDogImage.js import { useState, useEffect } from "react"; export default function useDogImages() { const [dogs, setDogs] = useState([]); useEffect(() => { async function fetchDogs() { const res = await fetch( "https://dog.ceo/api/breed/labrador/images/random/6" ); const { message } = await res.json(); setDogs(message); } fetchDogs(); }, []); return dogs; }
[2. 개선 1 ] 데이터를 받아오는 중에는 “로딩 중…”이라는 메시지 띄우기
- withLodaer HOC만들기
//DogImage.js import React from "react"; import withLoader from "./withLoader"; function DogImages(props) { return props.data.message.map((dog, index) => ( <img src={dog} alt="Dog" key={index} /> )); } export default withLoader( DogImages, "https://dog.ceo/api/breed/labrador/images/random/6" );
//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(); }, []); if (!data) { return <div>Loading...</div>; } return <Element {...props} data={data} />; }; }
⇒ withLoader HOC는 컴포넌트와 url에서 받아오는 데이터에는 관여하지 않는다.
⇒ 컴포넌트가 유효하고 API엔드포인트도 정상인 경우 단순히 API호출을 통해 받아온 데이터를 넘길 뿐이다.
[3. Composing] 이미지(DogImages) 컴포넌트에 마우스를 올리면 ‘호버링!’이라는 텍스트 박스 띄우기
- ‘hovering’이라는 prop를 제공하는 HOC를 만들어야 한다.
//DogImages.js import React from "react"; import withLoader from "./withLoader"; import withHover from "./withHover"; 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") );
//withHover.js import React, { useState } from "react"; export default function withHover(Element) { return props => { const [hovering, setHover] = useState(false); return ( <Element {...props} hovering={hovering} onMouseEnter={() => setHover(true)} onMouseLeave={() => setHover(false)} /> ); }; }
HOC를 사용하는 유명 오픈소스 라이브러리에는 recompose 가 있다. 나중에 혹시 HOC가 훅으로 완전 대체가 가능해 진다면 이 라이브러리는 더 이상 사용되지 않을것이다. 이 글도 마찬가지이다. by) ‣
[4. Hooks] HOC 패턴을 React Hook으로 대체하기
- 위의 withHover HOC를 useHover hook으로 리펙토링 하기
- 고차 컴포넌트를 사용하는 대신 엘리먼트에 mouseOver, mouseLeave 이벤트 핸들러를 추가할 것입니다.
- HOC처럼 엘리먼트를 반환할 수 없으니 ref를 반환하여 이벤트 핸들러를 추가할 엘리먼트를 지정할 수 있습니다
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" );
- DogImages 컴포넌트를 감싸는 대신 useHover hook을 직접 사용하여 기능을 구현할 수 있습니다.
//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]; }
3. Hook과 HOC
- 일반적으로 React Hook은 HOC 패턴을 완전 대체할 수 없지만, 대부분의 경우에서 React Hook이 tree가 깊어지는 상황을 줄일 수 있습니다. HOC 패턴을 사용하면 컴포넌트의 tree가 깊어지는 경우가 있기 때문입니다.
<withAuth> <withLayout> <withLogging> <Component /> </withLogging> </withLayout> </withAuth>
- HOC를 활용하면 동일한 로직을 한 군데 구현하여 여러 컴포넌트를 제공할 수 있습니다.
- 활용사례
- 앱 전반적으로 동일하며 커스터마이징 불가한 동작이 여러 컴포넌트에 필요한 경우
- 컴포넌트가 커스텀 로직 추가 없이 단독으로 동작할 수 있어야 하는 경우
- Hook은 내부에서 특정한 동작을 추가할 수 있게 해줍니다
- 하지만 HOC에 비해 버그를 발생시킬 확률이 높습니다
- 활용사례
- 공통 기능이 각 컴포넌트에서 쓰이기 전에 커스터마이징 되어야 하는 경우
- 공통 기능이 앱 전반적으로 쓰이는 것이 아닌 하나나 혹은 몇개의 컴포넌트에서 요구되는 경우
- 해당 기능이 기능을 쓰는 컴포넌트에게 여러 프로퍼티를 전달해야 하는 경우
4. HOC의 장점과 단점
- 장점
- 한 곳에 구현한 로직들을 여러 컴포넌트에서 재사용할 수 있습니다. 따라서 버그를 만들어 낼 확률도 줄일 수 있습니다.
- 로직을 한 곳에서 관리하여 코드를 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)
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)