2025-09-22 12:45
-
리액트 커스텀 훅은 반복되는 로직을 재사용 가능한 함수로 추출하여 컴포넌트의 가독성과 유지보수성을 향상시키기 위해 만들어졌다.
-
커스텀 훅은 이름이 ‘use’로 시작하고 내부에서 다른 훅을 호출할 수 있는 자바스크립트 함수로, 상태 관련 로직을 컴포넌트로부터 분리하는 역할을 한다.
-
커스텀 훅을 사용하면 API 데이터 호출, 폼 입력 처리, 윈도우 이벤트 감지 등 다양한 공통 기능을 여러 컴포넌트에서 손쉽게 공유하고 재활용할 수 있어 개발 효율성을 극대화한다.
리액트 커스텀 훅 완벽 정복 핸드북 개발 생산성을 높이는 마법
리액트(React) 개발의 패러다임을 바꾼 ‘훅(Hook)‘이 등장한 이후, 개발자들은 함수형 컴포넌트에서 상태(state)와 생명주기(lifecycle) 기능을 이전보다 훨씬 직관적으로 다룰 수 있게 되었다. 하지만 훅의 진정한 힘은 단순히 useState, useEffect 같은 기본 훅을 사용하는 데 그치지 않는다. 바로 여러 컴포넌트에서 반복되는 로직을 나만의 훅으로 만들어 재사용하는 **커스텀 훅(Custom Hook)**에 있다.
이 핸드북은 리액트 커스텀 훅의 탄생 배경부터 구조, 실용적인 사용법, 그리고 성능 최적화를 위한 심화 내용까지 모든 것을 담았다. 이 글을 끝까지 읽는다면, 당신의 리액트 코드는 더욱 간결해지고, 유지보수는 용이해지며, 개발 생산성은 극적으로 향상될 것이다.
커스텀 훅 왜 만들어졌을까
커스텀 훅의 탄생 배경을 이해하려면 훅이 등장하기 이전의 시대로 거슬러 올라가야 한다. 과거 클래스형 컴포넌트 시절, 개발자들은 컴포넌트 간에 공통된 로직을 재사용하기 위해 주로 두 가지 패턴을 사용했다. 바로 **고차 컴포넌트(Higher-Order Component, HOC)**와 **렌더 프롭(Render Prop)**이다.
1. 고차 컴포넌트(HOC)와 렌더 프롭의 시대
-
고차 컴포넌트 (HOC): 컴포넌트를 인자로 받아 새로운 컴포넌트를 반환하는 함수다. 마치 장식(Decorator)처럼 기존 컴포넌트에 공통 로직(예: 데이터 로딩, 인증 체크)을 주입하는 역할을 했다. 하지만 HOC를 여러 개 중첩해서 사용하면 코드의 출처를 파악하기 힘들어지는 ‘래퍼 지옥(Wrapper Hell)’ 현상이 발생하고, 타입스크립트와 함께 사용하기 까다롭다는 단점이 있었다.
-
렌더 프롭 (Render Prop): 컴포넌트의 prop으로 함수를 전달하여, 그 함수의 반환값을 렌더링하는 패턴이다. HOC보다 코드의 흐름이 명시적이라는 장점이 있지만, JSX 코드 내부에 또 다른 함수와 컴포넌트가 중첩되어 가독성이 떨어지는 단점이 있었다.
이러한 패턴들은 분명 유용했지만, 본질적으로 UI 렌더링과 관련 없는 로직을 재사용하기 위해 불필요한 컴포넌트 계층을 추가해야 한다는 공통적인 문제를 안고 있었다.
2. 로직 재사용의 새로운 해법 커스텀 훅
리액트 팀은 이러한 문제들을 해결하기 위해 훅을 도입했다. 훅의 가장 큰 철학은 **“상태 관련 로직을 컴포넌트로부터 분리하여 재사용할 수 있게 하자”**는 것이다. 커스텀 훅은 이 철학을 실현하는 핵심 도구다.
컴포넌트의 본질은 UI를 렌더링하는 것이다. 그런데 데이터 fetching, 브라우저 API 연동, 폼 상태 관리 같은 로직들이 컴포넌트 내부에 뒤섞여 있으면 코드는 복잡해지고 테스트는 어려워진다. 커스텀 훅은 이러한 비(非)UI 로직들을 컴포넌트 밖의 독립적인 함수로 깔끔하게 추출할 수 있게 해준다.
비유하자면, 커스텀 훅은 마치 잘 정리된 ‘공구함’과 같다. useFetch, useForm, useWindowSize처럼 특정 기능에 맞게 만들어진 공구(커스텀 훅)들을 필요할 때마다 가져와 컴포넌트라는 작업대 위에서 사용하는 것이다. 이를 통해 컴포넌트는 오직 UI에만 집중할 수 있게 되고, 재사용 가능한 로직들은 공구함 속에서 체계적으로 관리된다.
커스텀 훅의 구조 파헤치기
커스텀 훅은 특별한 문법이 아니다. 단지 두 가지 규칙을 따르는 자바스크립트 함수일 뿐이다.
-
이름이
use로 시작한다:useSomething,useMyHook처럼 함수의 이름은 반드시 ‘use’로 시작해야 한다. 이는 리액트가 해당 함수가 훅의 규칙(Rules of Hooks)을 따르는지 여부를 판단하는 중요한 기준이 된다. 린터(Linter)는 이 규칙을 통해 훅이 최상위 레벨에서만 호출되는지, 조건문이나 반복문 안에서 호출되지 않는지 등을 검사할 수 있다. -
내부에서 다른 훅을 호출할 수 있다: 커스텀 훅은
useState,useEffect,useContext와 같은 리액트 기본 훅이나 다른 커스텀 훅을 호출하여 상태 관리, 생명주기 제어 등의 기능을 조합할 수 있다. 이것이 커스텀 훅이 단순한 유틸리티 함수를 넘어 강력한 재사용 로직의 단위가 될 수 있는 이유다.
커스텀 훅의 기본 형태
JavaScript
import { useState, useEffect } from 'react';
// useWindowWidth 라는 커스텀 훅 정의
function useWindowWidth() {
// 1. 상태(state)를 관리하기 위해 useState 훅 사용
const [width, setWidth] = useState(window.innerWidth);
// 2. 생명주기(lifecycle) 관리를 위해 useEffect 훅 사용
useEffect(() => {
const handleResize = () => setWidth(window.innerWidth);
// 컴포넌트가 마운트될 때 이벤트 리스너 추가
window.addEventListener('resize', handleResize);
// 컴포넌트가 언마운트될 때 이벤트 리스너 제거 (clean-up)
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 빈 배열을 전달하여 최초 렌더링 시에만 실행되도록 설정
// 3. 훅이 관리하는 상태 값을 반환
return width;
}
// 컴포넌트에서 커스텀 훅 사용
function MyComponent() {
const windowWidth = useWindowWidth(); // 커스텀 훅 호출
return (
<div>
<p>현재 창 너비: {windowWidth}px</p>
</div>
);
}
위 useWindowWidth 예제는 커스텀 훅의 핵심 구조를 잘 보여준다.
-
useState를 사용해 ‘창 너비’라는 상태를 관리한다. -
useEffect를 사용해 ‘resize’ 이벤트 리스너를 추가하고 제거하는 부수 효과(side effect)를 처리한다. -
최종적으로 관리하던 상태 값(
width)을 반환하여 컴포넌트가 사용할 수 있도록 한다.
이처럼 커스텀 훅은 상태 로직(useState)과 부수 효과 로직(useEffect)을 하나로 캡슐화하여 컴포넌트에 필요한 값을 제공하는 역할을 한다.
실전 커스텀 훅 사용법
이제 이론을 넘어 실제 프로젝트에서 유용하게 사용할 수 있는 커스텀 훅 예제들을 살펴보자.
1. API 데이터 요청 useFetch
컴포넌트에서 비동기 데이터를 가져오는 로직은 매우 흔하게 반복된다. 로딩 중(loading), 데이터(data), 에러(error) 세 가지 상태를 관리하는 useFetch 훅을 만들어보자.
JavaScript
import { useState, useEffect } from 'react';
function useFetch(url, options = {}) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
// 이전 요청이 남아있는 경우를 대비한 정리(clean-up) 함수
const controller = new AbortController();
const signal = controller.signal;
const fetchData = async () => {
setLoading(true);
setError(null);
try {
const response = await fetch(url, { ...options, signal });
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
setData(result);
} catch (err) {
if (err.name !== 'AbortError') {
setError(err);
}
} finally {
setLoading(false);
}
};
fetchData();
// 컴포넌트가 언마운트되거나 url이 변경되기 전에 요청을 취소
return () => {
controller.abort();
};
}, [url]); // url이 변경될 때마다 useEffect가 다시 실행
return { data, loading, error };
}
// 사용 예시
function PostList() {
const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts');
if (loading) return <p>로딩 중...</p>;
if (error) return <p>에러가 발생했습니다: {error.message}</p>;
return (
<ul>
{posts && posts.map(post => <li key={post.id}>{post.title}</li>)}
</ul>
);
}
이제 어떤 컴포넌트에서든 API 호출이 필요할 때 useFetch 훅 한 줄이면 loading, data, error 상태를 손쉽게 관리할 수 있다. 컴포넌트는 더 이상 비동기 처리의 복잡한 로직을 신경 쓸 필요 없이, 가져온 데이터를 어떻게 보여줄지에만 집중하면 된다.
2. 폼 입력 관리 useForm
여러 개의 입력(input)을 가진 폼을 관리하는 것은 번거로운 일이다. 각 입력마다 useState를 선언하고 onChange 핸들러를 만들어야 한다. 이 반복적인 작업을 useForm 훅으로 추상화해보자.
JavaScript
import { useState } from 'react';
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues(prevValues => ({
...prevValues,
[name]: value,
}));
};
const reset = () => {
setValues(initialValues);
};
return [values, handleChange, reset];
}
// 사용 예시
function LoginForm() {
const [formValues, handleFormChange, resetForm] = useForm({
email: '',
password: '',
});
const handleSubmit = (e) => {
e.preventDefault();
console.log('로그인 시도:', formValues);
resetForm();
};
return (
<form onSubmit={handleSubmit}>
<input
type="email"
name="email"
value={formValues.email}
onChange={handleFormChange}
placeholder="이메일"
/>
<input
type="password"
name="password"
value={formValues.password}
onChange={handleFormChange}
placeholder="비밀번호"
/>
<button type="submit">로그인</button>
</form>
);
}
useForm 훅은 폼 상태(values), 상태 변경 함수(handleChange), 초기화 함수(reset)를 배열로 반환한다. 이를 통해 여러 개의 입력 필드를 가진 복잡한 폼도 단 몇 줄의 코드로 간결하게 관리할 수 있다.
심화 내용 더 나은 커스텀 훅 만들기
커스텀 훅을 만드는 것에 익숙해졌다면, 이제는 더 효율적이고 안정적인 훅을 만드는 방법을 고민해야 한다.
1. 유연한 인터페이스 설계
커스텀 훅은 다양한 상황에서 재사용될 것을 염두에 두고 만들어야 한다. 따라서 입력(인자)과 출력(반환값)을 유연하게 설계하는 것이 중요하다.
| 구분 | 설명 | 예시 |
|---|---|---|
| 인자 (Arguments) | 훅의 동작을 외부에서 제어할 수 있도록 옵션 객체나 콜백 함수를 인자로 받는다. | useFetch(url, { method: 'POST' }) |
| 반환값 (Return Value) | 여러 값을 반환할 때는 배열(Array)이나 객체(Object)를 사용한다. 배열은 순서가 중요하고 이름을 자유롭게 지을 때 ([value, setValue]), 객체는 순서가 중요하지 않고 반환값의 의미를 명확히 하고 싶을 때 ({ data, loading, error }) 사용한다. | const { data, loading } = useFetch(...) |
2. 성능 최적화 useCallback과 useMemo
커스텀 훅 내부에서 함수를 정의하거나 복잡한 연산을 수행할 경우, 컴포넌트가 리렌더링될 때마다 불필요한 작업이 반복될 수 있다. 이때 useCallback과 useMemo를 사용하여 성능을 최적화할 수 있다.
-
useCallback: 커스텀 훅이 함수를 반환할 때, 해당 함수가 의존하는 값이 변경되지 않는 한 함수를 재생성하지 않도록 메모이제이션(memoization)한다. 이는 자식 컴포넌트에 prop으로 함수를 전달할 때 유용하다. -
useMemo: 커스텀 훅 내부에서 비용이 큰 연산의 결과값을 메모이제이션하여, 의존성 배열의 값이 변경될 때만 연산을 다시 수행하도록 한다.
JavaScript
import { useState, useMemo, useCallback } from 'react';
function useComplexCalculation(list) {
// list가 변경되지 않으면 expensiveCalculation 함수는 다시 실행되지 않는다.
const processedData = useMemo(() => {
console.log('복잡한 연산 수행...');
return list.map(item => item * 2); // 예시: 비용이 큰 연산
}, [list]);
// 반환되는 함수를 useCallback으로 감싸 불필요한 재생성을 방지한다.
const logData = useCallback(() => {
console.log(processedData);
}, [processedData]);
return { processedData, logData };
}
3. 디버깅과 네이밍
커스텀 훅의 이름은 그 기능을 명확하게 설명해야 한다. useData보다는 useFetchPosts가, useHandler보다는 useFormInput이 훨씬 좋은 이름이다. 명확한 이름은 디버깅을 쉽게 만들고, 리액트 개발자 도구에서도 훅의 상태를 추적하는 데 도움을 준다.
결론 커스텀 훅은 당신의 개발 무기다
커스텀 훅은 단순히 코드를 재사용하는 기술을 넘어, 리액트 애플리케이션의 아키텍처를 설계하는 강력한 도구다. 잘 만들어진 커스텀 훅은 다음과 같은 이점을 제공한다.
-
관심사의 분리 (Separation of Concerns): UI 로직과 비즈니스 로직을 분리하여 코드를 더 깔끔하고 이해하기 쉽게 만든다.
-
재사용성 (Reusability): 반복되는 로직을 여러 컴포넌트에서 쉽게 공유하여 개발 시간을 단축한다.
-
가독성 (Readability): 컴포넌트는 자신이 무슨 일을 하는지 명확하게 드러내고, 복잡한 로직은 커스텀 훅 뒤에 숨겨진다.
-
테스트 용이성 (Testability): 순수 자바스크립트 함수인 커스텀 훅은 컴포넌트와 독립적으로 쉽게 테스트할 수 있다.
오늘 소개한 내용들을 바탕으로 당신의 프로젝트에 반복되는 로직이 보인다면, 주저하지 말고 커스텀 훅으로 추출해보자. 작게는 useToggle부터 시작해서 프로젝트의 도메인에 특화된 useShoppingCart, useAuth 같은 복잡한 훅에 이르기까지, 커스텀 훅을 적극적으로 활용하는 순간 당신의 개발 경험은 한 차원 높은 수준으로 올라서게 될 것이다.