실전 커스텀 훅
훅 챕터에서 소개했듯이, 커스텀 훅은 use로 시작하는 함수로 여러 훅을 조합해서 재사용 가능한 로직을 만드는 것입니다. 이번 챕터는 실제로 어떻게 쓰는지 보여주는데, 놀라운 결과를 얻게 됩니다. 한 섹션 전체를 들여서 만든 헤드리스 Toggle 컴포넌트를 커스텀 훅으로 대체하면 코드가 훨씬 짧아지고, 그 컴포넌트는 완전히 삭제됩니다.
명확히 구분해야 할 점은 일반 **유틸리티 함수**와의 차이입니다. 데이터를 가져오거나 날짜를 포맷하는 함수는 유틸리티입니다. 커스텀 훅은 다른 훅을 호출하므로, 훅의 규칙이 그 안에서도 적용됩니다.
effect 추출하기: useEffectOnUpdate
Toggle 컴포넌트에는 "상태가 변할 때는 이 콜백을 실행하되, 첫 렌더링에서는 실행하지 않기"라는 역할을 하는 로직이 있었습니다. firstRender ref로 보호된 useEffect 말이죠. 이건 toggle 로직이 아니라 effect 로직이고, 어디서나 유용합니다. 이를 추출하면 useEffect 자체와 같은 시그니처를 가진 훅이 나옵니다:
export default function useEffectOnUpdate(effectFunction, deps) {
const firstRender = useRef(true)
useEffect(() => {
if (firstRender.current) {
firstRender.current = false
} else {
effectFunction()
}
}, deps)
}호출자는 useEffectOnUpdate(onToggle, [on])이라고 쓰면 첫 렌더링 관련 코드가 사라집니다. 다른 훅을 감싸는 훅은 감싼 로직이 몇 줄이어도 이름이 의도를 명확하게 드러내면 만들 가치가 있습니다.
보호 로직에는 개발 전용 특성이 있습니다
렌더 props에서도 지적했던 것과 같은 문제입니다: StrictMode는 첫 effect 실행에서 ref를 사용하므로, 콜백이 두 번째에 실행됩니다. 프로덕션 빌드는 이렇게 하지 않으며, 지속적인 해결책은 첫 렌더링인지 추적하는 대신 값을 이전 값과 비교하는 것입니다.
상태 추출하기: useToggle
useEffectOnUpdate는 useEffect처럼 아무것도 반환하지 않습니다. 대부분의 커스텀 훅은 useState처럼 뭔가를 반환하고, 반환하는 값의 형태는 설계 결정입니다. useToggle은 boolean, flip 함수, 변경 콜백을 한 곳에 모읍니다:
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
const [on, setOn] = useState(initialValue)
const toggle = () => setOn(prevOn => !prevOn)
useEffectOnUpdate(onToggle, [on])
return [on, toggle]
}const [on, toggle] = useToggle({ initialValue: true })useToggle을 호출할 때마다 자기만의 useState를 얻으므로, 두 호출자는 서로 독립된 boolean을 유지합니다. 두 개의 다른 컴포넌트이든 한 컴포넌트가 훅을 두 번 호출하든 말입니다.
여기 담긴 세 가지 결정이 재사용할 패턴입니다:
- 배열을 반환합니다 값들이 호출자가 이름을 바꿀 자연스러운 쌍일 때, 정확히
useState처럼요: 한 호출자는[on, toggle]로 구조분해하고, 다른 호출자는[open, toggleOpen]으로 구조분해합니다. 여러 값이 있고 이름이 순서보다 중요할 때 객체를 반환합니다. - 위치 인자 대신 설정 객체를 받습니다.
useToggle(false, callback)이면 콜백만 원하는 호출자도 boolean을 먼저 제공해야 합니다. 하지만{ initialValue, onToggle }이면 필요한 것만 전달하고 나머지는 건너뛸 수 있으며, 모든 옵션은 기본값을 가질 수 있습니다. - 콜백을 noop으로 기본값 설정합니다 (
() => {}). 이렇게 하면 훅이 콜백을 전달하지 않은 호출자를 깨뜨리지 않으면서onToggle()을 조건 없이 호출할 수 있습니다.
결과: 컴포넌트 삭제하기
useToggle이 있으면, 별 위젯은 더 이상 Toggle 로직이 필요 없습니다:
export default function Star({ onChange }) {
const [on, toggle] = useToggle({ onToggle: onChange })
return on
? <BsStarFill className="star filled" onClick={toggle} />
: <BsStar className="star" onClick={toggle} />
}메뉴는 여전히 합성 컴포넌트 형태를 유지합니다. 그 부분들이 여전히 공유 상태가 필요하기 때문입니다. 하지만 이제 그 provider는 훅에서 값을 얻습니다. Menu는 useToggle을 호출하고, { open, toggleOpen }을 자신의 context로 전달하며, 헤드리스 Toggle 컴포넌트의 파일들은 삭제됩니다.
이 삭제가 이 섹션의 진짜 교훈입니다. 헤드리스 컴포넌트는 많은 합성 컴포넌트가 공유할 수 있을 때까지 가치를 유지했고, 일단 로직이 훅에 맞으면 훅이 더 간단한 도구가 됩니다. 자랑스러워했던 작동하는 코드를 뜯어내는 일은 일의 정상적인 부분입니다.
두 패턴은 서로 보완합니다: 훅은 로직을 공유하고, 헤드리스 컴포넌트는 context와 합성을 통해 트리 전체에 로직을 공유합니다. 먼저 훅을 쓰고, 컴포넌트가 추가 번거로움을 정당화할 때까지 기다리세요. 이 섹션은 Component Library++라는 솔로 프로젝트로 마무리되는데, 이 다섯 개 챕터의 모든 패턴을 한 라이브러리에 넣습니다.
use로 시작하고 useState 같은 훅을 기반으로 로직 조각을 묶어내는 함수입니다. useToggle이 있으면 어떤 컴포넌트든 한 줄로 on-off 값과 flip 함수를 얻을 수 있습니다. 마치 useState가 값과 setter를 주는 것처럼요. 그 훅을 호출하는 각 컴포넌트는 자기만의 상태 복사본을 얻으므로, 훅은 모두가 공유하는 값이 아니라 재사용하는 조리법입니다.
다음: 라우팅. URL이 어떤 컴포넌트를 렌더링할지 결정하기 시작합니다.

