안녕하세요! AT SOPT YB 36기 엄지우입니다.
지난주 과제에서 사용자 정의 훅(Custom Hook)을 만들어보라는 코드 리뷰를 받았는데요.
사실 그동안 사용자 정의 훅을 제대로 만들어 사용해 본 경험이 없었고, 개념도 잘 정리되어 있지 않아서 이번 기회에 관련 내용을 정리해 보았습니다!
로직 재사용 방법 중 하나인 고차 컴포넌트(HOC)와 비교하면서, 각각의 개념과 어떤 상황에서 더 적합한지 등을 함께 공부해 보았어요 🤓

🤔 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까❓
재사용할 수 있는 로직을 관리하는 방법
1. 사용자 정의 훅 (Custom Hook)
2. 고차 컴포넌트 (Higher Order Component)
1. 사용자 정의 훅
- 서로 다른 컴포넌트 내부에서 같은 로직을 공유하고자 할 때 주로 사용
- 리액트에서만 사용할 수 있음
- use로 시작 → 리액트 훅이라는 것을 바로 인식할 수 있음
🔍 HTTP 요청을 처리하는 fetch 기반의 사용자 정의 훅
import { useEffect, useState } from 'react';
interface UseFetchOptions {
method: string;
body?: BodyInit;
}
function useFetch<T>(url: string, { method, body }: UseFetchOptions) {
// 실제 응답 데이터
const [result, setResult] = useState<T | undefined>();
// 로딩 상태 (요청 중 여부)
const [isLoading, setIsLoading] = useState<boolean>(false);
// 응답 성공 여부 (response.ok)
const [ok, setOk] = useState<boolean | undefined>();
// HTTP 상태 코드 저장 (200, 404 등)
const [status, setStatus] = useState<number | undefined>();
useEffect(() => {
const abortController = new AbortController();
(async () => {
setIsLoading(true);
try {
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);
}
} catch (error) {
if (error instanceof DOMException && error.name === 'AbortError') {
console.log('요청이 중단되었습니다.');
} else {
console.error('Fetch 오류:', error);
}
} finally {
setIsLoading(false);
}
})();
return () => {
abortController.abort();
};
}, [url, method, body]);
// 훅에서 사용할 응답 정보 반환
return { ok, result, isLoading, status };
}
export default useFetch;
- 훅으로 분리하지 않았다면 fetch로 API 호출을 해야 하는 모든 컴포넌트 내에서 공통적으로 최소 4개의 state를 선언해서 구현해야 함
2. 고차 컴포넌트 (HOC)
- 컴포넌트 자체의 로직을 재사용하기 위한 방법
- 고차 함수(Higher Order Function)의 일종으로, 자바스크립트 함수의 특징을 이용하므로 자바스크립트 환경에서 쓰일 수 있음
- HOF: 함수를 인자로 받거나, 함수를 반환하는 함수
- 리액트에서 제공하는 API 중 하나인 React.memo도 고차 컴포넌트의 한 종류!
✅ React.memo란?
🔍 React.memo 사용 X
import { useEffect, useState, ChangeEvent } from 'react';
const ChildComponent = ({ value }: { value: string }) => {
useEffect(() => {
console.log('렌더링!');
});
return <>안녕하세요! {value}</>;
};
function ParentComponent() {
const [state, setState] = useState(1);
function handleChange(e: ChangeEvent<HTMLInputElement>) {
setState(Number(e.target.value));
}
return (
<>
<input type="number" value={state} onChange={handleChange} />
<ChildComponent value="hello" />
</>
);
}
- 부모 컴포넌트가 렌더링될 때마다 자식 컴포넌트도 렌더링됨
- 자식 컴포넌트는 의존성 배열이 없는 useEffect를 사용하고 있어, 렌더링될 때마다 useEffect가 매번 실행됨
- input 값 수정 → setState() 실행 → ParentComponent 리렌더링 → ChildComponent 리렌더링
🔍 React.memo 사용 O
const ChildComponent = memo(({ value }: { value: string }) => {
useEffect(() => {
console.log('렌더링!');
});
return <>안녕하세요! {value}</>;
});
- ChildComponent에 React.memo 사용
- props가 변경되지 않았다는 것을 memo가 확인하고, 이전에 기억한 컴포넌트를 그대로 반환!
- 리렌더링 X
- 리렌더링 X
function add(a) {
return function (b) {
return a + b;
};
}
const result = add(1); // result는 이제 function (b) { return a + b }를 가리킴
const result2 = result(2); // result(2) → a=1, b=2 → 3
- add 함수는 함수를 반환하므로 고차 함수
- result는 함수를 가리키는 변수
- result1 = add(2);
- result3 = result1(5); // 2+5=7
💫 고차 함수를 활용한 리액트 고차 컴포넌트 만들어보기
import React from 'react';
// 1. 로그인 여부를 나타내는 props 타입 정의
interface LoginProps {
loginRequired?: boolean;
}
// 2. 고차 컴포넌트 정의
function withLoginComponent<T>(Component: React.ComponentType<T>) {
return function (props: T & LoginProps) {
const { loginRequired, ...restProps } = props;
if (loginRequired) {
return <>로그인이 필요합니다.</>;
}
return <Component {...(restProps as T)} />;
};
}
// 3. 원래 컴포넌트 정의 후 고차 컴포넌트로 감싸기
const Component = withLoginComponent((props: { value: string }) => {
return <h3>{props.value}</h3>;
});
// 4. 실제 사용 예시
export default function App() {
const isLogin = true;
return (
<div>
<Component value="text" loginRequired={isLogin} />
{/* 또는 loginRequired prop을 아예 생략해도 됨 */}
{/* <Component value="text" /> */}
</div>
);
}
- Component를 withLoginComponent(고차 컴포넌트)로 감싸면서, 기존 props인 value 외에도 loginRequired 같은 로그인 관련 props를 전달할 수 있게 됨
- loginRequired 값에 따라 '로그인이 필요합니다' 또는 value 값을 출력
- loginRequired를 안 넘기면 (undefined), 로그인 제한 없이 항상 value 값 출력
❗고차 컴포넌트는 컴포넌트 전체를 감쌀 수 있다는 점에서 사용자 정의 훅보다 컴포넌트에 더욱 큰 영향력을 미침
⚠️ 주의할 점
- with로 시작하는 이름 사용
- use의 경우와 같이 ESLint 규칙 등으로 강제되는 사항 X / 일종의 관습!
- 부수 효과 최소화
- 반드시 컴포넌트를 인수로 받게 되는데, 컴포넌트의 props를 임의로 수정, 추가, 삭제 X
- 여러 개의 고차 컴포넌트로 컴포넌트를 감쌀 경우 복잡성 ⬆️
- 고차 컴포넌트는 최소한으로 사용
3. 사용자 정의 훅과 고차 컴포넌트 중 무엇을 써야 할까?
⭐ 사용자 정의 훅이 필요한 경우
- useEffect, useState와 같이 리액트에서 제공하는 훅으로만 공통 로직을 격리할 수 있을 경우
- 사용자 정의 훅은 그 자체로는 렌더링에 영향을 미치지 못함
- 컴포넌트 내부에 미치는 영향을 최소화해, 개발자가 훅을 원하는 방향으로만 사용할 수 있다는 장점
// 사용자 정의 훅 방식
function HookComponent() {
const { loggedIn } = useLogin();
useEffect(() => {
if (!loggedIn) {
// do something..
}
}, [loggedIn]);
}
// 고차 컴포넌트 방식
const HOCComponent = withLoginComponent(() => {
// do something...
});
- useLogin은 단순히 loggedIn에 대한 값만 제공할 뿐, 처리는 컴포넌트를 사용하는 쪽에서 원하는 대로 사용 가능
- 부수 효과가 비교적 제한적!
- withLoginComponent는 고차 컴포넌트가 어떤 일을 하는지, 어떤 결과물을 반환할지는 고차 컴포넌트를 직접 보거나 실행하기 전까지 알 수 없음
- 대부분의 고차 컴포넌트는 렌더링에 영향을 미치는 로직이 존재하므로, 비교적 예측하기가 어려움
✅ 컴포넌트 전반에 걸쳐 동일한 로직으로 값을 제공하거나 특정한 훅의 작동을 취하게 하고 싶다면 사용자 정의 훅 사용
🩷 고차 컴포넌트를 사용해야 하는 경우
// 사용자 정의 훅 방식
function HookComponent() {
const { loggedIn } = useLogin();
if (!loggedIn) {
return <LoginComponent />;
}
return <>안녕하세요</>;
}
// 고차 컴포넌트 방식
const HOCComponent = withLoginComponent(() => {
// 로그인 상태는 신경 쓰지 않음
return <>안녕하세요</>;
});
- HookComponent는 개발자가 로그인 상태를 직접 판단해서 제어함
- HOCComponent는 로그인 여부에 따른 제어는 고차 컴포넌트가 맡고, 개발자는 내용에만 집중할 수 있음
→ 더 간단하고 깔끔한 컴포넌트 작성이 가능해짐
✅ 렌더링의 결과물에도 영향을 미치는 공통 로직이라면 고차 컴포넌트 사용
💡 정리
| 구분 | 사용자 정의 훅 (Custom Hook) | 고차 컴포넌트 (Higher Order Component) |
| 목적 | 로직(데이터, 상태, 부수 효과)을 추상화 | UI 렌더링 흐름을 제어하거나 조건부로 감싸기 |
| 사용하는 쪽 | 내부에서 직접 로직 제어 가능 | 로직을 위임하고 결과만 사용 |
| 예측 가능성 | 높음 (훅 내부가 명확하게 보임) | 낮음 (렌더링 여부, 처리 방식이 감춰짐) |
| 재사용 방식 | 로직 복사 없이 함수처럼 호출 | 컴포넌트를 감싸 새로운 컴포넌트로 생성 |
| 주요 용도 | 상태 관리, API 요청, 이벤트 추적, 로딩 등 | 인증 처리, 접근 제어, 레이아웃 래핑 등 |
| 대표 예시 | `useFetch`, `useLogin`, `useDarkMode` | `withLoginComponent`, `withAuth`, `withLayout` |
| 적합한 경우 | 여러 컴포넌트에서 공통된 로직만 공유하고 싶을 때 | 여러 컴포넌트에 공통된 구조/제어 흐름을 부여하고 싶을 때 |
| 사용 위치 | 함수 내부 (컴포넌트에서 직접 호출) | 함수 외부 (컴포넌트를 감쌈) |
'4주차' 카테고리의 다른 글
| TypeScript에서 같은 이름의 타입을 여러 개 생성하면 어떻게 될까? (0) | 2025.05.13 |
|---|---|
| API 폴더? 서비스 폴더? 구조 분리가 중요한 이유 (0) | 2025.05.13 |
| 우리가 Next.js를 사용해야 하는 이유 (0) | 2025.05.13 |
| 복잡한 경로 관리는 그만! React 프로젝트에서 라우터 구조와 절대경로 설정 정리하기 (0) | 2025.05.12 |
| API 모듈화 - axios Instance 활용하기 (0) | 2025.05.12 |