HOC 패턴
종종 여러 컴포넌트에서 같은 로직을 사용해야 하는 경우가 있다. 이런 로직은 컴포넌트의 스타일시트를 설정하는 것일 수 있고. 권한을 요청하거나. 전역 상태를 추가하는 것일 수 있다.
같은 로직을 여러 컴포넌트에서 재사용하는 방법 중 하나로 고차 컴포넌트 패턴을 활용하는 방법이 있다. 이 패턴은 앱 전반적으로 재사용 가능한 로직을 여러 컴포넌트들이 쓸 수 있게 해 준다.
고차 컴포넌트란 다른 컴포넌트를 받는 컴포넌트를 뜻한다. 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을 받는다.
withLoader
의useEffect
훅에서url
로 API를 호출하여 데이터를 받아오고 있다. 응답이 오기 전 까지 반환되는 엘리먼트는Loading...
텍스트를 출력하고 있다.- 데이터를 받아오고 나면
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을 기준으로 텍스트 노출 여부를 결정하면 된다.
아래 예제에서는 withLoader
HOC에 withHover
HOC를 사용하고 있다.
DogImages
엘리먼트는 이제 withHover
와 withLoader
에서 제공하는 prop을 사용할 수 있다. 따라서 “호버링!” 이라는 텍스트는 이 값을 기준으로 노출하면 된다.
HOC를 사용하는 유명 오픈소스 라이브러리에는 recompose 가 있다. 나중에 혹시 HOC가 훅으로 완전 대체가 가능해 진다면 이 라이브러리는 더 이상 사용되지 않을것이다. 이 글도 마찬가지이다.
Hooks
몇몇 상황에서는 HOC패턴은 React의 훅으로 대체할 수 있다.
위에서 구현했던 withHover
HOC를 useHover
훅으로 리펙토링해 보자. 고차 컴포넌트를 사용하는 대신 엘리먼트에 mouseOver
, mouseLeave
이벤트 핸들러를 추가할 것이다. 또 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의 사용 사례
- 공통 기능이 각 컴포넌트에서 쓰이기 전에 커스터마이징 되어야 하는 경우
- 공통 기능이 앱 전반적으로 쓰이는 것이 아닌 하나나 혹은 몇개의 컴포넌트에서 요구되는 경우
- 해당 기능이 기능을 쓰는 컴포넌트에게 여러 프로퍼티를 전달해야 하는 경우
사례 분석
몇몇 라이브러리들은 기본적으로 HOC패턴에 의존하며 릴리즈 후에 훅이 지원되었다. Apollo Client가 적합한 예시이다.
아래 예시를 보기 위해 Apollo Client에 대한 지식이 필요하지는 않다
Apollo Client를 사용하기 위한 방법 중 하나는 graphql()
이라는 HOC를 사용하는 것이다.
graphql()
HOC를 사용하면 고차 컴포넌트로 감싸는 것으로 컴포넌트에서 사용 가능한 데이터를 만들어 낼 수 있다. 지금도 이 라이브러리를 사용할 수는 있지만 몇가지 단점도 존재한다.
컴포넌트가 여러 리졸버에 접근해야 하는 경우 graphql()
HOC를 여러번 중첩해 사용해야 한다. 여러 HOC를 겹쳐 사용하는 코드는 데이터가 어떻게 전달되는지 파악하기 어렵다. 또 HOC를 사용하는 순서가 중요해지면 리펙토링 과정에 버그를 만들어내기 쉽다.
React에 훅이 추가된 이후 Apollo 도 훅을 자체적으로 지원하기 시작했다. graphql()
HOC를 사용하는 것 대신 개발자는 훅을 사용하여 직접 데이터에 접근할 수 있게 되었다.
다음 예제는 graphql()
HOC를 사용하던 이전 예제와 동일하지만 useMutation
훅을 사용하여 데이터에 접근하고 있다.
useMutation
훅을 활용하여 컴포넌트가 데이터에 접근하기 위해 필요한 코드들을 일부 줄일 수 있었다.
또한 여러 리졸버를 사용할 때 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)
위의 예제에서. 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에 관련이 있는지 파악하기가 어렵다. 앱의 디버깅이나 규모를 키울 때 방해가 될 수 있다.