
Custom Hooks: 잘못 만든 추상화가 빚을 만든다 (The Art of Abstraction)
Custom Hook은 React의 가장 강력한 무기지만, 잘못 사용하면 오히려 독이 됩니다. 단순히 로직을 옮겨 적는 것을 넘어, '상태(State)'와 '이펙트(Effect)'를 분리하고 재사용 가능한 'Headless UI' 패턴을 설계하는 방법을 심도 있게 다룹니다.

Custom Hook은 React의 가장 강력한 무기지만, 잘못 사용하면 오히려 독이 됩니다. 단순히 로직을 옮겨 적는 것을 넘어, '상태(State)'와 '이펙트(Effect)'를 분리하고 재사용 가능한 'Headless UI' 패턴을 설계하는 방법을 심도 있게 다룹니다.
로버트 C. 마틴(Uncle Bob)이 제안한 클린 아키텍처의 핵심은 무엇일까요? 양파 껍질 같은 계층 구조와 의존성 규칙(Dependency Rule)을 통해 프레임워크와 UI로부터 독립적인 소프트웨어를 만드는 방법을 정리합니다.

클래스 이름 짓기 지치셨나요? HTML 안에 CSS를 직접 쓰는 기괴한 방식이 왜 전 세계 프론트엔드 표준이 되었는지 파헤쳐봤습니다.

서로 다른 인터페이스를 연결해주는 변환기. 레거시 시스템과 신규 시스템을 이어주는 가장 강력한 디자인 패턴.

분명히 클래스를 적었는데 화면은 그대로다? 개발자 도구엔 클래스가 있는데 스타일이 없다? Tailwind 실종 사건 수사 일지.

React 개발자들이 흔히 하는 실수가 있습니다. 컴포넌트 코드가 좀 길어진다 싶으면, 무조건 useSomething이라는 파일을 만들고 코드를 복사해서 옮겨 넣습니다. 그리고 "리팩토링했다"고 뿌듯해하죠.
하지만 2주 뒤, 다른 동료가 그 훅을 쓰려고 열어보면 절망에 빠집니다.
// 나쁜 예: 이름만 훅이고 실상은 '잡동사니 보관함'
const useModalWithUserAndAuthAndLog = () => {
const [user, setUser] = useState(null);
const [isModalOpen, setIsModalOpen] = useState(false);
useEffect(() => {
// API 호출도 하고...
// 로컬 스토리지도 확인하고...
// 로그도 찍고...
}, []);
return { user, isModalOpen, setIsModalOpen }; // 연관 없는 상태들의 파티
};
이건 추상화가 아닙니다. 그냥 "쓰레기를 안 보이는 곳으로 치운 것"에 불과합니다. 방이 깨끗해 보일지 몰라도, 벽장 문을 열면 쓰레기가 쏟아져 나옵니다(Leaky Abstraction). 진정한 Custom Hook은 "비즈니스 로직의 캡슐화"이자 "재사용 가능한 상태 기계(State Machine)의 설계"여야 합니다.
좋은 훅은 내부 구현을 몰라도 쓸 수 있어야 합니다. 외부에서는 "내가 무엇을 원하는지(Intent)"만 전달하고, 훅은 "어떻게 처리할지(Implementation)"를 알아서 수행해야 합니다.
// 가장 기초적인 Custom Hook
function useInput(initialValue) {
const [value, setValue] = useState(initialValue);
const handleChange = (e) => {
setValue(e.target.value);
}
return { value, onChange: handleChange };
}
이 훅은 input 태그가 어떻게 동작하는지, 이벤트 객체에서 target.value를 어떻게 꺼내는지(How)를 숨깁니다.
개발자는 "나는 입력값을 관리하고 싶어"라는 의도(What)만 가지고 이 훅을 사용하면 됩니다.
const userHook = useInput("Guest");
<input {...userHook} />
최근 React 생태계에서 가장 핫한 키워드는 Headless UI입니다. (Radix UI, Headless UI, React Aria 등이 이 패턴을 씁니다.) "스타일은 네가 알아서 해, 나는 기능만 줄게"라는 철학입니다. 이를 Custom Hook으로 구현하면 재사용성이 극대화됩니다.
드롭다운을 만들 때 가장 골치 아픈 게 뭘까요?
이걸 매번 Dropdown 컴포넌트 짤 때마다 구현하면 지옥입니다. 이걸 훅으로 뽑아냅니다.
// useDropdown.js
function useDropdown() {
const [isOpen, setIsOpen] = useState(false);
const ref = useRef(null);
const toggle = () => setIsOpen(!isOpen);
const close = () => setIsOpen(false);
// Click Outside 감지 로직
useEffect(() => {
const handleClickOutside = (event) => {
if (ref.current && !ref.current.contains(event.target)) {
close();
}
};
document.addEventListener("mousedown", handleClickOutside);
return () => document.removeEventListener("mousedown", handleClickOutside);
}, []);
// ESC 키 감지 로직
useEffect(() => {
const handleEsc = (e) => {
if (e.key === 'Escape') close();
};
document.addEventListener("keydown", handleEsc);
return () => document.removeEventListener("keydown", handleEsc);
}, []);
return {
isOpen,
toggle,
close,
ref // DOM에 연결할 ref 반환
};
}
이제 디자이너가 "드롭다운 배경을 파란색으로 해주세요", "애니메이션 넣어주세요"라고 요구해도 로직은 건드릴 필요가 없습니다. UI 컴포넌트만 수정하면 됩니다. 로직은 useDropdown이 꽉 잡고 있으니까요.
React 입문자들이 가장 많이 만드는 훅이 useFetch입니다.
// 초보자의 useFetch
function useFetch(url) {
const [data, setData] = useState(null);
useEffect(() => {
fetch(url).then(res => res.json()).then(setData);
}, [url]);
return data;
}
깔끔해 보이지만, 이 코드는 실제로 쓰면 재앙을 부릅니다.
서버 상태(Server State) 관리는 너무나 복잡한 문제입니다. 그래서 2024년 현재는 TanStack Query (React Query)나 SWR 같은 라이브러리를 쓰는 것이 표준입니다. 여러분이 직접 비동기 훅을 짜기보다는, 이런 라이브러리를 래핑(Wrapping)해서 "도메인 훅"을 만드는 것이 훨씬 좋습니다.
// 좋은 예: 도메인 로직을 담은 훅
import { useQuery } from '@tanstack/react-query';
export const useTeamMembers = (teamId) => {
return useQuery({
queryKey: ['team', teamId],
queryFn: () => getTeamMembers(teamId),
staleTime: 1000 * 60, // 1분간 캐시
enabled: !!teamId, // teamId가 있을 때만 요청
});
};
이렇게 하면 컴포넌트에서는 isLoading, error 처리를 우아하게 할 수 있고, 캐싱 혜택도 공짜로 얻습니다.
훅을 설계할 때 너무 많은 옵션(파라미터)을 받으려 하지 마세요.
useChart({ color, size, data, xAxis, yAxis, tooltip, legend, ... }) -> 이렇게 만들면 쓰는 사람이 헷갈립니다.
10개의 파라미터를 받는 만능 훅 하나보다, 3개의 기능을 가진 작은 훅 3개를 조합(Composition)해서 쓰는 것이 훨씬 낫습니다.
// ❌ 만능 훅 (God Hook)
const { data } = useTable({ sort: true, filter: true, pagination: true, url: '/api' });
// ✅ 작은 훅들의 조합
const { data } = useQuery(...);
const { sortedData } = useSort(data);
const { filteredData } = useFilter(sortedData);
const { pageData } = usePagination(filteredData);
이렇게 하면 나중에 "필터 기능만 빼주세요"라는 요구사항이 왔을 때 useFilter 줄만 지우면 됩니다. 레고 블록처럼 조립하세요.
Custom Hook을 만들 때 가장 많이 겪는 버그는 Stale Closure(상한 클로저) 문제입니다.
useEffect나 useCallback의 의존성 배열(dependency array)을 대충 적으면, 훅이 "오래된 상태값"을 기억하고 있는 현상이 발생합니다.
// 버그 발생: count가 1에서 멈춤
function useInterval(callback, delay) {
useEffect(() => {
const id = setInterval(callback, delay);
return () => clearInterval(id);
}, []); // 의존성 배열을 비워두면 callback이 처음 생성된 버전만 기억함!
}
이런 문제를 해결하려면 useRef를 사용해 최신 콜백을 항상 가리키게 하거나, 의존성 배열을 정확하게 채워줘야 합니다. 이걸 손으로 다 챙기기 어렵기 때문에 ESLint 플러그인(eslint-plugin-react-hooks)을 반드시 켜두고 경고를 무시하지 말아야 합니다.
Custom Hook을 만들 때는 "함수를 짠다"고 생각하지 말고, "UI가 없는 컴포넌트를 짠다"고 생각하세요. 컴포넌트를 쪼갤 때 재사용성과 단일 책임 원칙을 고민하듯이, 훅을 만들 때도 똑같이 고민해야 합니다.
useData 같은 이름은 사형감입니다.)잘 만든 Custom Hook 하나는 열 컴포넌트 안 부럽습니다. 여러분의 코드가 "복사 붙여넣기"에서 "우아한 조립"으로 진화하길 바랍니다.