리액트 마스터하기: 커스텀 훅(Custom Hooks)으로 코드 중복 완벽하게 제거하기

리액트로 프로젝트를 진행하다 보면 여러 컴포넌트에서 비슷한 로직이 반복되는 상황을 자주 마주하게 됩니다. 예를 들어 API 데이터를 불러오거나, 입력 폼의 상태를 관리하거나, 윈도우 크기를 감지하는 로직 등이 대표적입니다.
이러한 로직을 복사하여 붙여넣는 대신, 하나로 묶어 재사용할 수 있게 만드는 것이 바로 **커스텀 훅(Custom Hooks)**입니다. 이번 포스팅에서는 커스텀 훅의 개념부터 실무 예제까지 상세히 살펴보겠습니다.
1. 커스텀 훅이란 무엇인가?
커스텀 훅은 리액트의 기본 훅(useState, useEffect 등)을 조합하여 만든 사용자 정의 함수입니다. 커스텀 훅을 사용하면 다음과 같은 장점이 있습니다.
- 코드 재사용성 향상: 반복되는 로직을 한곳에서 관리합니다.
- 컴포넌트 가독성 증대: 복잡한 비즈니스 로직을 분리하여 UI 컴포넌트 본연의 역할에 집중하게 합니다.
- 유지보수 용이성: 로직 수정 시 훅 내부 코드만 수정하면 모든 컴포넌트에 반영됩니다.
규칙: 커스텀 훅의 이름은 반드시 use로 시작해야 합니다. 이는 리액트가 해당 함수가 훅 규칙을 따르는지 식별하기 위한 약속입니다.
2. 예제 1: API 데이터 패칭 자동화 (useFetch)
가장 흔하게 중복되는 로직은 API 호출입니다. 로딩 상태와 에러 처리를 포함한 useFetch 훅을 만들어 보겠습니다.
커스텀 훅 정의
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
if (!response.ok) throw new Error('Network response was not ok');
const result = await response.json();
setData(result);
} catch (err) {
setError(err.message);
} finally {
setLoading(false);
}
};
fetchData();
}, [url]);
return { data, loading, error };
}
export default useFetch;
실제 사용 예시
function UserProfile() {
const { data: user, loading, error } = useFetch('[https://api.example.com/user/1](https://api.example.com/user/1)');
if (loading) return
Loading...
;
if (error) return
Error: {error}
;
return
;
}
3. 예제 2: 폼 상태 관리의 효율화 (useForm)
여러 개의 입력창(Input)을 관리할 때 매번 onChange 함수를 만드는 것은 번거로운 일입니다. 이를 간소화해 보겠습니다.
커스텀 훅 정의
import { useState } from 'react';
function useForm(initialValues) {
const [values, setValues] = useState(initialValues);
const handleChange = (e) => {
const { name, value } = e.target;
setValues({
...values,
[name]: value,
});
};
const reset = () => setValues(initialValues);
return [values, handleChange, reset];
}
export default useForm;
실제 사용 예시
function LoginForm() {
const [values, handleChange, reset] = useForm({ email: '', password: '' });
const handleSubmit = (e) => {
e.preventDefault();
console.log('Login data:', values);
reset();
};
return (
<form onSubmit={handleSubmit}>
<input name="email" value={values.email} onChange={handleChange} placeholder="Email" />
<input name="password" type="password" value={values.password} onChange={handleChange} placeholder="Password" />
<button type="submit">Login</button>
</form>
);
}
4. 예제 3: 브라우저 이벤트 감지 (useWindowSize)
반응형 웹을 만들거나 특정 크기에서 다른 로직을 수행해야 할 때 유용한 윈도우 크기 감지 훅입니다.
커스텀 훅 정의
import { useState, useEffect } from 'react';
function useWindowSize() {
const [windowSize, setWindowSize] = useState({
width: typeof window !== 'undefined' ? window.innerWidth : 0,
height: typeof window !== 'undefined' ? window.innerHeight : 0,
});
useEffect(() => {
const handleResize = () => {
setWindowSize({
width: window.innerWidth,
height: window.innerHeight,
});
};
window.addEventListener('resize', handleResize);
// 메모리 누수 방지를 위한 클린업 함수
return () => window.removeEventListener('resize', handleResize);
}, []);
return windowSize;
}
export default useWindowSize;
실제 사용 예시
function ResponsiveComponent() {
const { width } = useWindowSize();
return (
<div>
<p>현재 창 너비: {width}px</p>
{width < 768 ? (
<p>모바일 뷰 활성화</p>
) : (
<p>데스크톱 뷰 활성화</p>
)}
</div>
);
}
5. 커스텀 훅 작성 시 주의할 점
- 순수성 유지: 커스텀 훅 내부에서는 가능한 UI에 직접적인 영향을 주는 코드를 배제하고 데이터 처리와 상태 관리 로직만 담는 것이 좋습니다.
- 의존성 배열 관리: useEffect나 useCallback 등을 훅 내부에서 사용할 때 의존성 배열(deps)을 정확히 작성해야 버그를 예방할 수 있습니다.
- 반환 타입 결정: 값 하나만 반환할 수도 있고, 배열([value, setter])이나 객체({ data, error }) 형태로 반환할 수도 있습니다. 사용하는 쪽의 편의성을 고려하세요.
마치며
커스텀 훅은 리액트의 꽃이라고 불릴 만큼 강력한 도구입니다. 처음에는 모든 로직을 컴포넌트 내부에 적는 것이 편할 수 있지만, 프로젝트 규모가 커질수록 커스텀 훅을 통한 모듈화는 필수적입니다. 오늘 살펴본 예제들을 바탕으로 여러분만의 유용한 훅을 만들어 보세요!