중복 코드를 피해야 한다는 것은 중요한 말이다. 반복되는 코드의 존재만으로도 비효율이며 유지보수도 어려워진다. React에서 재사용할 수 있는 로직을 관리할 수 있는 방법은 두 가지가 있다.
- 사용자 정의 훅(Custom Hook)
- 고차 컴포넌트(High Order Component)
사용자 정의 훅(Custom Hook)
서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용되는 것이 사용자 정의 훅이다. 이는 Hook을 기반으로 필요한 훅을 만드는 기법이며, 오직 React에서만 사용할 수 있는 방식이다.
React 훅이 use로 시작하는 네이밍 규칙이 있는 것처럼 사용자 정의 훅 또한 use로 시작하는 이름을 갖는 것이 규칙이다.
아래는 HTTP 요청을 하는 fetch를 기반으로 한 사용자 정의 훅의 예시이다.
import React, { useEffect, useState } from "react";
function useFetch<T>(
url: string,
{ method, body }: { method: string; body?: XMLHttpRequestBodyInit }
) {
const [result, setResult] = useState<T | undefined>();
const [isLoading, setIsLoading] = useState<boolean>(false);
const [ok, setOk] = useState<boolean | undefined>();
const [status, setStatus] = useState<number | undefined>();
useEffect(() => {
const abortCotroller = new AbortController();
(async () => {
setIsLoading(true);
const response = await fetch(url, {
method,
body,
signal: abortCotroller.signal,
});
setOk(response.ok);
setStatus(response.status);
if (response.ok) {
const apiResult = await response.json();
setResult(apiResult);
}
setIsLoading(false);
})();
}, [url, method, body]);
return { ok, result, isLoading, status };
}
interface Todo {
userId: number;
id: number;
title: string;
completed: boolean;
}
const MyComponent = () => {
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>
);
};
/* STYLE */
export default MyComponent;
이처럼 사용자 정의 훅으로 분리하지 않았다면, 각각의 컴포넌트에서 최소 4개의 state를 선언해 구현했어야 할 것이다. 이는 useReducer를 사용해도 마찬가지일 것이다. useEffect도 필요하기 때문에 이 두가지 훅을 fetch가 필요한 곳마다 중복해서 사용해야 할 것이다.
이렇게 복잡하고 반복되는 로직은 사용자 정의 훅으로 간단하게 만들어 손쉽게 중복되는 로직을 관리할 수 있다.
사용시 주의 사항
네이밍 규칙
사용자 정의 훅은 React 훅을 이용해 만들기 때문에 사용자 정의 훅의 이름 앞에 use를 붙여야 한다. React 훅은 컴포넌트 내부나 사용자 정의 훅 내부에서만 사용할 수 있다.
만일 use를 사용하지 않으면 ESLint / react-hooks/rules-of-hooks가 에러를 지적한다.
물론 이름의 시작을 대문자로 하면 컴포넌트로 사용할 수 있기에 이 방법 또한 에러를 발생하지 않는다.
고차 함수(High Order Component, HOC)
고차 컴포넌트는 컴포넌트 자체의 로직을 재사용하기 위한 방법이다. JS의 함수의 특징을 이용하므로 React 말고도 JS 환경에서도 사용할 수 있다. React에서 가장 유명한 고차 컴포넌트는 React.memo이다.
React.memo란?
리액트 컴포넌트는 여러 렌더링 조건이 있지만, 그 중 부모 컴포넌트가 새롭게 렌더링될 때 자식 컴포넌트의 props 변경 여부와 관계없이 발생한다.
import { useEffect, useState } from "react";
const ChildComponent = ({ value }: { value: string }) => {
useEffect(() => {
console.log("render !!!!");
});
return <>{value}</>;
};
const MyComponent = () => {
const [state, setState] = useState(1);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setState(+e.target.value);
}
return (
<>
<input type="number" value={state} onChange={handleChange} />
<ChildComponent value="hello" />
</>
);
};
export default MyComponent;
위의 예제에서는 ChildComponent는 props인 value="hello"가 변경되지 않아도 handleChange로 인해 setState를 실행해 state를 변경하므로 리렌더링이 발생한다.
이처럼 props의 변화가 없음에도 컴포넌트의 렌더링을 방지하기 위해 만들어진 것이 React.memo이다.
React.memo는 렌더링 전에 props를 비교해 이전 props와 같다면 렌더링을 생략하고 기억해둔 컴포넌트를 반환한다.
import { memo, useEffect, useState } from "react";
const ChildComponent = memo(({ value }: { value: string }) => {
useEffect(() => {
console.log("render !!!!");
});
return <>{value}</>;
});
const MyComponent = () => {
const [state, setState] = useState(1);
function handleChange(e: React.ChangeEvent<HTMLInputElement>) {
setState(+e.target.value);
}
return (
<>
<input type="number" value={state} onChange={handleChange} />
<ChildComponent value="hello" />
</>
);
};
export default MyComponent;
useMemo를 사용해도 같은 동작을 구현할 수 있다.
하지만 이 경우에는 값을 반환 받기 때문에 JSX 함수 방식이 아닌 {}를 사용한 할당식을 사용해야 하므로 목적과 용도가 뚜렷한 memo를 사용하는 것이 일반적이다.
고차 함수
고차 함수란 함수를 인자로 받거나 결과로 반환하는 함수이다. 대표적으로 Array.prototype.map 함수가 있다.
고차 함수를 활용해 고차 컴포넌트를 만들어 보자.
사용자 인증 정보에 따라 인증된 사용자에게는 개인화된 컴포넌트를, 그렇지 않은 사용자에게는 별도 정의된 컴포넌트를 보여주는 예제이다.
import React, { ComponentType } from "react";
interface LoginProps {
loginRequired?: boolean;
}
function withLoginComponent<T>(Component: ComponentType<T>) {
return function (props: T & LoginProps) {
const { loginRequired, ...restProps } = props;
if (loginRequired) {
return <div>로그인이 필요합니다.</div>;
}
return <Component {...(restProps as T)}></Component>;
};
}
const Component = withLoginComponent((props: { value: string }) => {
return <h3>{props.value}</h3>;
});
const MyComponent = () => {
const isLogin = true;
return <Component value="text" loginRequired={isLogin} />;
};
/* STYLE */
export default MyComponent;
이러한 인증 처리는 서버나 NGINX와 같이 JS 이전 단계에서 처리하는 편이 효율적이지만, 이 예제에서는 고차 함수에 대한 이해를 위해 만들어졌다.
이처럼 고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 더 큰 영향력을 미치는 것이 가능하다.
단순히 값을 반환하거나 부수 효과를 실행하는 사용자 정의 훅과는 달리, 고차 컴포넌트는 컴포넌트의 결과물에 영향을 미칠 수 있는 다른 공통된 작업을 처리한다.
고차 컴포넌트는 with로 시작하는 이름을 사용해야 한다. 이것은 이미 널리 퍼진 일종의 관습이다.
또한 고차 컴포넌트를 사용하는 경우 부수효과를 최소화 해야함을 염두해야 한다. 고차 컴포넌트는 컴포넌트를 인자로 받게 되는데, 컴포넌트의 props를 임의로 수정, 추가, 삭제하는 일이 없어야 한다.
만약 임의로 고차 컴포넌트에서 컴포넌트의 props를 임의로 변경하면 예측하지 못한 상황에 props가 변경될 수 있으며, 이는 버그로 이어질 수 있다.
또한 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성이 커질 수 있다.
사용자 정의 훅 vs 고차 컴포넌트
사용자 정의 훅이 필요한 경우
단순히 useEffect, useState와 같은 훅으로 공통 로직을 격리할 수 있다면 사용자 정의 훅을 사용하는 것이 좋다. 그 이유는 사용자 정의 훅은 렌더링에 영향을 미치지 못하기 때문이다.
정리하자면, 단순히 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅을 사용하는 것이 좋다.
컴포넌트 내부에 미치는 영향을 최소화해 개발자가 훅을 원하는 방향으로만 사용할 수 있다는 장점이 있다.
고차 컴포넌트를 사용해야 하는 경우
앞선 예제처럼 애플리케이션 관점에서 어떤 사용자가 컴포넌트에 접근할 때 무언가 해야하는 경우에는 고차 컴포넌트가 조금 더 유리할 수 있다. 혹은 에러 바운더리와 비슷한 경우에도 이에 대응하는 컴포넌트를 보여주는 것으로도 사용할 수 있다.
'React > 모던 리액트 Deep Dive' 카테고리의 다른 글
리액트와 서버사이드 (0) | 2025.06.03 |
---|---|
서버 사이드 렌더링 (1) | 2025.04.30 |
useContext와 useReducer, useImperativeHandler, useLayoutEffect, useDebugValue (0) | 2025.04.22 |
useMemo, useCallback와 useRef (0) | 2025.04.09 |
useState와 useEffect (0) | 2025.04.03 |