개발자라면 누구나 중복 코드를 피야하는 것을 안다. 리액트에서는 재사용할 수 있는 로직을 관리할 수 있는 두가지 방법이 있다. 커스텀 훅과 고차 컴포넌트이다.
이 둘이 무엇인지 또 공통된 코드를 하나로 만들고자 할 때 어떤 것을 선택해야 할 지 알아보자.
사용자 정의 훅 (Custom Hook)
커스텀 훅은 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용된다. 리액트의 훅을 기반으로 개발자가 필요한 훅을 만드는 기법이다.
리액트의 훅 이름이 모두 use로 시작하는것과 같이 커스텀 훅 또한 use로 시작하는 함수를 만들어야 한다.
HTTP 요청을 하는 fetch를 기반으로 한 커스텀 훅 예제를 보며 알아보자.
import { useEffect, useState } from 'react'
// HTTP 요청을 하는 사용자 정의 훅
function useFetch<T>(
url: string,
{ method, body }: { method: string; body?: XMLHttpRequestBodyInit }
) {
// 응답 결과
const [result, setResult] = useState<T | undefined>()
// 요청 중 여부
const [isLoading, setIsLoading] = useState<boolean>(false)
// 2xx 3xx로 정상 응답인지 여부
const [ok, setOk] = useState<boolean | undefined>()
// HTTP status
const [status, setStatus] = useState<number | undefined>()
useEffect(() => {
const abortController = new AbortController()
;(async () => {
setIsLoading(true)
const response = await fetch(url, {
method,
body,
signal: abortController.signal,
})
setOk(response.ok)
setStatus(response.status)
if (response.ok) {
const apiResult = await response.json()
setResult(apiResult)
}
setIsLoading(false)
})()
return () => {
abortController.abort()
}
}, [url, method, body])
return { ok, result, isLoading, status }
}
interface Todo {
userId: number
id: number
title: string
completed: boolean
}
export default function App() {
// 사용자 지정 훅 사용
const { isLoading, result, status, ok } = useFetch<Array<Todo>>('https://jsonplaceholder.typicode.com/todos', {
method: 'GET',
})
useEffect(() => {
if (!isLoading) {
console.log('fetchResult >>', status)
}
}, [status, isLoading])
return (
<div>
{ok
? (result || []).map(({ userId, title }, index) => (
<div key={index}>
<p>{userId}</p>
<p>{title}</p>
</div>
))
: null}
</div>
)
}
위의 코드는 fetch를 이용해 API를 호출하는 로직을 커스텀 훅으로 분리한 예제이다.
만약 훅으로 분리하지 않았다면 API를 호출하는 모든 컴포넌트에 공통으로 관리되지 않는 최소 4개의 state를 선언해서 각각 구현했어야 할 것이다.
이렇게 복잡하고 반복되는 로직은 커스텀 훅으로 간단하게 만들 수 있다.
훅에서 필요한 useState, useEffect 로직을 커스텀 훅인 useFetch 내부에 둔다면 사용하는 쪽에서 useFetch훅만 사용해도 손쉽게 중복되는 로직을 관리할 수 있다.
고차 컴포넌트
(고차 컴포넌트는 컴포넌트를 매개변수로 받아 새로운 컴포넌트를 반환하는 컴포넌트)
컴포넌트 자체의 로직을 재사용하기 위한 방법이다.
커스텀 훅과의 차이
커스텀 훅은 리액트 훅을 기반으로 하므로 리액트에서만 사용할 수 있지만 고차 컴포넌트는 고차 함수의 일종으로 JS의 일급객체, 함수의 특징을 이용하므로 자바스크립트 환경에서 널리 쓸 수 있다.
리액트에서는 이러한 고차 컴포넌트 기법으로 중복 로직을 관리할 수 있다.
고차함수
리액트의 함수 컴포넌트도 결국 함수이기 때문에 함수를 기반으로 고차함수를 만드는 것을 먼저 이해해야 한다.
고차 함수의 정의는 ‘함수를 인수로 받거나 결과로 반환하는 함수’ 이다.
대표적 고차 함수로는 map이 이있다.
const list = [1, 2, 3];
const doubledList = list.map((item) => item * 2);
위의 코드에서 고차함수의 정의와 같이 (item) ⇒ item * 2
,즉 함수를 인수로 받는것을 알 수 있다.
고차 함수를 활용하면 함수를 인수로 받거나 새로운 함수를 반환해 완전 새로운 결과를 만들어 낼 수 있다. 자연스럽게, 리액트의 함수 컴포넌트도 “함수”이므로 고차 함수를 사용해 다양한 작업을 할 수 있다. (중복 로직 관리, 최적화 등등)
고차 컴포넌트 만들어보기
다음은 사용자 인증 정보에 따라 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도로 정의된 공통 컴포넌트를 보여주는 예제이다.
고차함수의 특징대로 개발자가 만든 또 다른 함수(컴포넌트)를 반환할 수 있다는 점에서 고차 컴포넌트를 사용하면 유용한 상황이다.
interface LoginProps {
loginRequired?: boolean
}
function withLoginComponent<T>(Component: ComponentType<>) {
return function (props: T & LoginProps) {
const { loginRequired, restProps } = props
if (loginRequired) {
return <>로그인이 필요합니다.</>
}
return <Component {...(restProps as T)} />
}
}
// 원래 구현하고자 하는 컴포넌트를 만들고, withLoginComponent로 감싸기만 하면 끝이다.
// 로그인 여부, 로그인이 안 되면 다른 컴포넌트를 렌더링하는 책임은 모두
// 고차 컴포넌트인 withLoginComponent에 맡길 수 있어 매우 편리하다.
const Component = withLoginComponent((props: { value: string }) => {
return <h3>{props. value}</h3>
})
export default function App() {
// 로그인 관련 정보를 가져온다.
const isLogin = true
return <Component value="text" loginRequired={isLogin} />
// return <Component value="text" />;
}
Component
는 일반적인 컴포넌트지만, 이 컴포넌트를 withLoginComponent
라는 컴포넌트로 감싸뒀다. withLoginComponent
는 함수 컴포넌트를 인수로 받으며, 컴포넌트를 반환하는 고차 컴포넌트이다.
이 컴포넌트는 props에 loginRequired가 있다면 넘겨받은 함수를 반환하는 것이 아니라 <>로그인이 필요합니다.</>
를 반환하고 loginRequired가 없거나 false라면 원래 반환해야 할 결과를 그대로 반환한다.
단순 값을 반환하거나 부수 효과를 실행하는 커스텀 훅과 다르게, 고차 컴포넌트는 컴포넌트의 결과물에 영향을 미치는 공통된 작업을 처리할 수 있다.
고차 컴포넌트 사용 시 주의사항
- 고차 컴포넌트를 만들 때 with로 시작하는 이름을 사용해야 한다. 강제사항은 아니지만 고차 컴포넌트임을 쉽게 알 수 있도록 하는 일종의 관습이다.
- 부수효과를 최소화 해야 한다. 고차 컴포넌트는 반드시 컴포넌트를 인수로 받는데, 컴포넌트의 기존의 props를 임의로 수정해선 안된다. 만약 수정하게 된다면 고차 컴포넌트를 사용하는 쪽에서 예측하지 못한 상황에서 props가 수정될지도 모른다는 부담감을 주게 되기 때문이다. 만약 컴포넌트에 추가 정보를 제공해주고 싶다면 별도 props를 추가해 내려주는 것이 좋다.
- 여러 개의 고차 컴포넌트로 컴포넌트를 감싸지 않는다. 여러개의 고차 컴포넌트가 반복적으로 컴포넌트를 감싸면 어떤 결과를 만들질 지 예측하기 어려워 진다. 그러므로 최소한으로 사용해야 한다.
커스텀 훅과 고차 컴포넌트 중 무엇을 써야 할까?
커스텀 훅
컴포넌트 랜더링에 직접적으로 영향을 미치지 않으며 그저 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 함 →
언제?
- 단순히 useEffect, usesState와 같이 훅으로만 공통 로직을 격리할 수 있는 경우
- 단순히 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하고 싶은 경우
장점
- 개발자가 원하는 방향으로 사용 가능
커스텀 훅은 그저 로직에 따른 값을 제공할 뿐 이에 대한 반환값을 바탕으로 무엇을 할지는 개발자에 달렸다.
고차 컴포넌트
- 함수 컴포넌트의 반환값, 즉 랜더링 결과물에도 영향을 미치는 공통 로직일 경우