Skip to content
This page has been auto-translated and may contain errors.View in English

실전 커스텀 훅

챕터에서 소개했듯이, 커스텀 훅은 use로 시작하는 함수로 여러 훅을 조합해서 재사용 가능한 로직을 만드는 것입니다. 이번 챕터는 실제로 어떻게 쓰는지 보여주는데, 놀라운 결과를 얻게 됩니다. 한 섹션 전체를 들여서 만든 헤드리스 Toggle 컴포넌트를 커스텀 훅으로 대체하면 코드가 훨씬 짧아지고, 그 컴포넌트는 완전히 삭제됩니다.

명확히 구분해야 할 점은 일반 **유틸리티 함수**와의 차이입니다. 데이터를 가져오거나 날짜를 포맷하는 함수는 유틸리티입니다. 커스텀 훅은 다른 훅을 호출하므로, 훅의 규칙이 그 안에서도 적용됩니다.

effect 추출하기: useEffectOnUpdate

Toggle 컴포넌트에는 "상태가 변할 때는 이 콜백을 실행하되, 첫 렌더링에서는 실행하지 않기"라는 역할을 하는 로직이 있었습니다. firstRender ref로 보호된 useEffect 말이죠. 이건 toggle 로직이 아니라 effect 로직이고, 어디서나 유용합니다. 이를 추출하면 useEffect 자체와 같은 시그니처를 가진 훅이 나옵니다:

jsx
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

useEffectOnUpdateuseEffect처럼 아무것도 반환하지 않습니다. 대부분의 커스텀 훅은 useState처럼 뭔가를 반환하고, 반환하는 값의 형태는 설계 결정입니다. useToggle은 boolean, flip 함수, 변경 콜백을 한 곳에 모읍니다:

jsx
export default function useToggle({ initialValue = false, onToggle = () => {} } = {}) {
  const [on, setOn] = useState(initialValue)
  const toggle = () => setOn(prevOn => !prevOn)

  useEffectOnUpdate(onToggle, [on])

  return [on, toggle]
}
jsx
const [on, toggle] = useToggle({ initialValue: true })

useToggle을 호출할 때마다 자기만의 useState를 얻으므로, 두 호출자는 서로 독립된 boolean을 유지합니다. 두 개의 다른 컴포넌트이든 한 컴포넌트가 훅을 두 번 호출하든 말입니다.

여기 담긴 세 가지 결정이 재사용할 패턴입니다:

  • 배열을 반환합니다 값들이 호출자가 이름을 바꿀 자연스러운 쌍일 때, 정확히 useState처럼요: 한 호출자는 [on, toggle]로 구조분해하고, 다른 호출자는 [open, toggleOpen]으로 구조분해합니다. 여러 값이 있고 이름이 순서보다 중요할 때 객체를 반환합니다.
  • 위치 인자 대신 설정 객체를 받습니다. useToggle(false, callback)이면 콜백만 원하는 호출자도 boolean을 먼저 제공해야 합니다. 하지만 { initialValue, onToggle }이면 필요한 것만 전달하고 나머지는 건너뛸 수 있으며, 모든 옵션은 기본값을 가질 수 있습니다.
  • 콜백을 noop으로 기본값 설정합니다 (() => {}). 이렇게 하면 훅이 콜백을 전달하지 않은 호출자를 깨뜨리지 않으면서 onToggle()을 조건 없이 호출할 수 있습니다.

결과: 컴포넌트 삭제하기

useToggle이 있으면, 별 위젯은 더 이상 Toggle 로직이 필요 없습니다:

jsx
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는 훅에서 값을 얻습니다. MenuuseToggle을 호출하고, { open, toggleOpen }을 자신의 context로 전달하며, 헤드리스 Toggle 컴포넌트의 파일들은 삭제됩니다.

이 삭제가 이 섹션의 진짜 교훈입니다. 헤드리스 컴포넌트는 많은 합성 컴포넌트가 공유할 수 있을 때까지 가치를 유지했고, 일단 로직이 훅에 맞으면 훅이 더 간단한 도구가 됩니다. 자랑스러워했던 작동하는 코드를 뜯어내는 일은 일의 정상적인 부분입니다.

두 패턴은 서로 보완합니다: 훅은 로직을 공유하고, 헤드리스 컴포넌트는 context와 합성을 통해 트리 전체에 로직을 공유합니다. 먼저 훅을 쓰고, 컴포넌트가 추가 번거로움을 정당화할 때까지 기다리세요. 이 섹션은 Component Library++라는 솔로 프로젝트로 마무리되는데, 이 다섯 개 챕터의 모든 패턴을 한 라이브러리에 넣습니다.

일찍부터 들여놓을 습관: 커스텀 훅을 쓰기 전에 커뮤니티가 이미 만들었는지 확인하세요. 디바운스된 값, 미디어 쿼리, 로컬 스토리지 상태, 이벤트 리스너, 이전 값 추적 같은 것들은 작은 라이브러리의 검증된 훅으로 존재하며, 그 소스를 읽는 것은 관용구를 빨리 배우는 방법입니다. 로직이 앱에만 해당될 때 직접 만들 가치가 있고, useCurrentUseruseCartTotal 같은 앱 전용 훅은 보통 앱의 데이터 로직이 살 수 있는 가장 깔끔한 층입니다: 컴포넌트는 선언적으로 유지되고, 복잡한 부분들은 한 곳에 이름이 있고 테스트 가능한 함수로 머뭅니다.

커스텀 훅은 로직을 공유하지만 상태는 공유하지 않습니다. 두 호출자가 같은 값을 봐야 할 때, 훅 혼자서는 못합니다: 상태는 한 번만 존재해야 하고, provider에 있어야 하며, 훅은 읽는 쪽이 됩니다. context 챕터useTheme 패턴처럼요. 머리에서 "재사용 가능한 로직"과 "공유된 상태"를 구분하면 훅이 스토어처럼 동작하기를 기대하는 고전적인 실수를 피합니다.

이런 훅들을 쾌적하게 배포할지 결정하는 세부사항이 두 가지 있습니다. 첫째는 정체성입니다: useToggle은 렌더링할 때마다 새로운 toggle 함수를 만들므로, 호출자가 memoized 자식에 전달하거나 다른 훅의 의존성 배열에 넣으면 매번 새로운 값을 넘깁니다. 호출자가 안정적인 참조에 의존할 때는 훅 안에서 useCallback으로 감싸세요.

둘째는 도구입니다. 래퍼 훅을 통해 deps 배열을 전달하는 것은 배열 길이가 상수면 런타임에 작동하지만, exhaustive-deps는 자신이 쓴 배열이 아니면 검증할 수 없고, 호출 지점에서 명시적으로 쓰인 게 아니면 useEffectOnUpdate(onToggle, [on])을 완전히 무시합니다. 래퍼 훅을 많이 쓰는 코드베이스는 그 설정을 추가해야 하므로, effects 챕터가 말한 버그로 취급해야 하는 경고들이 계속 당신에게 도달합니다.

Juno로직을 병에 담아 어디서나 재사용하기 커스텀 훅은 이름이 use로 시작하고 useState 같은 훅을 기반으로 로직 조각을 묶어내는 함수입니다. useToggle이 있으면 어떤 컴포넌트든 한 줄로 on-off 값과 flip 함수를 얻을 수 있습니다. 마치 useState가 값과 setter를 주는 것처럼요.

그 훅을 호출하는 각 컴포넌트는 자기만의 상태 복사본을 얻으므로, 훅은 모두가 공유하는 값이 아니라 재사용하는 조리법입니다.

Juno로직을 병에 담아 어디서나 재사용하기 반복되는 상태 관리 로직을 훅으로 추출하세요: 첫 렌더링을 건너뛰는 effect를 위한 useEffectOnUpdate, onToggle 콜백이 있는 boolean 상태를 위한 useToggle.

호출자가 자연스러운 쌍을 이름 바꿀 때 배열을 반환하고, 위치 인자 대신 기본값이 있는 설정 객체를 받으며, 콜백은 noop으로 기본값 설정하세요.

로직이 트리를 통해 이동해야 할 때까지 헤드리스 컴포넌트보다 훅을 선호하세요.

Juno로직을 병에 담아 어디서나 재사용하기 커스텀 훅은 훅을 합성하고, 그들의 규칙을 이행하며, 로직을 공유하면서 모든 호출자는 독립된 상태를 유지합니다. 상태가 복제되지 않고 공유되어야 할 때는 provider와 짝을 이루세요.

하나를 쾌적하게 배포할지 결정하는 세부사항이 두 가지 있습니다: 반환하는 함수들은 매 렌더링마다 새로 만들어지므로 호출자가 그것을 memoized 자식에 전달하거나 의존성 배열에 넣을 때는 useCallback으로 감싸세요. 그리고 deps 배열을 전달하는 래퍼 훅은 프로젝트가 규칙의 additionalHooks 옵션에 그것을 명시하지 않으면 exhaustive-deps에 보이지 않습니다.

다음: 라우팅. URL이 어떤 컴포넌트를 렌더링할지 결정하기 시작합니다.