React · 2026-01-14

React와 TypeScript: 사용자 정의 훅 타입화 패턴

React와 TypeScript 환경에서 사용자 정의 훅을 안전하고 일관되게 타입화하는 핵심 패턴과 실용 예제를 초보자도 이해하기 쉽게 단계별로 정리한 모음

작성일 : 2026-01-14 ㆍ 작성자 : 관리자
post
목차

개요

React 훅을 TypeScript로 작성할 때 가장 중요한 목표는 안전성과 재사용성이다. 훅의 입력과 출력에 명확한 타입을 선언하면 컴포넌트에서 발생하는 런타임 오류를 줄일 수 있다. 이 글에서는 useState의 타입 추론부터 제네릭 훅, 반환 타입 설계, 그리고 흔히 마주치는 패턴을 예제 중심으로 다룬다. 초보자도 이해하기 쉽도록 단계별로 설명한다.

useState 타입 추론과 명시적 선언

useState는 초기값을 통해 타입을 추론한다. 하지만 초기값이 null 또는 빈 배열인 경우 타입을 명시해 주어야 한다. 다음은 기본적인 예제다.

const [count, setCount] = useState<number>(0)
const [name, setName] = useState<string | null>(null)
const [items, setItems] = useState<string[]>([])

이처럼 제네릭을 사용하면 이후 set 함수에 잘못된 타입을 전달하는 실수를 방지할 수 있다. 키워드 "useState 타입 추론 react"를 염두에 두면 자동 완성과 타입 검사가 얼마나 도움이 되는지 체감할 수 있다.

간단한 사용자 정의 훅 타입 선언

가장 흔한 형태는 toggle 훅이다. boolean 상태를 토글하는 훅을 만들면 다음과 같다.

function useToggle(initial: boolean = false) {
  const [value, setValue] = useState<boolean>(initial)
  const toggle = () => setValue(v => !v)
  return { value, toggle, setValue }
}

위 훅은 반환을 객체로 했기 때문에 순서를 신경 쓸 필요가 없다. 호출부에서는 구조 분해로 쉽게 사용 가능하다.

사용 예

function Example() {
  const { value, toggle } = useToggle()
  return <button onClick={toggle}>{value ? 'On' : 'Off'}</button>
}

제네릭 훅: 데이터 패칭 패턴

많은 프로젝트에서 fetch나 API 요청을 추상화한 훅이 필요하다. 이때 제네릭을 쓰면 응답 타입을 호출부에서 지정할 수 있다. 다음은 기본적인 useAsync 패턴이다.

import { useState, useEffect } from 'react'

function useAsync<T, E = Error>(asyncFn: () => Promise<T>, deps: any[] = []) {
  const [data, setData] = useState<T | null>(null)
  const [error, setError] = useState<E | null>(null)
  const [loading, setLoading] = useState(false)

  useEffect(() => {
    let mounted = true
    setLoading(true)
    asyncFn()
      .then(result => {
        if (mounted) setData(result)
      })
      .catch(err => {
        if (mounted) setError(err as E)
      })
      .finally(() => {
        if (mounted) setLoading(false)
      })
    return () => { mounted = false }
  }, deps)

  return { data, error, loading }
}

호출부에서 타입을 지정하면 자동 완성과 타입 검사가 함께 동작한다.

type User = { id: number; name: string }

function UsersList() {
  const fetchUsers = () => fetch('/api/users').then(r => r.json() as Promise<User[]>)
  const { data, loading, error } = useAsync<User[]>(fetchUsers, [])
  if (loading) return <div>Loading...</div>
  if (error) return <div>Error</div>
  return <ul>{data?.map(u => <li key={u.id}>{u.name}</li>) }</ul>
}

반환 타입 설계: 튜플 vs 객체

커스텀 훅을 설계할 때 반환을 튜플로 할지 객체로 할지 결정해야 한다. 튜플은 이름이 고정된 소수의 값에 적합하다. 객체는 확장성과 명시성이 필요할 때 유리하다. 예를 들어 상태와 수정 함수를 반환하는 간단한 훅은 튜플로 쓸 수 있다.

function useCounter(initial = 0): [number, { inc: () => void; dec: () => void }] {
  const [count, setCount] = useState<number>(initial)
  return [count, { inc: () => setCount(c => c + 1), dec: () => setCount(c => c - 1) }]
}

그러나 확장 가능한 훅이라면 객체 반환을 권장한다. 객체는 필드명이 있어 가독성이 좋고, 추후 필드 추가 시 파괴적 변경이 적다.

실무에서 자주 마주치는 실수와 해결책

  • 초기값 누락으로 인한 any 타입: 제네릭을 명시해 해결
  • 부적절한 의존성 배열: deps에 함수나 객체를 넣을 땐 useCallback 또는 useMemo 사용
  • 불명확한 반환 타입: 인터페이스나 타입 별칭으로 명시

예시: 반환 타입 명시

type UseResult<T> = { data: T | null; loading: boolean; error: Error | null }

function useData<T>(req: () => Promise<T>): UseResult<T> { /* 구현 */ }

테스트와 문서화

타입은 코드의 문서다. 훅을 테스트할 때는 기대 타입을 기반으로 테스트 케이스를 작성하면 의도치 않은 변경을 빠르게 잡을 수 있다. Storybook이나 간단한 사용 예를 작성해 두면 재사용할 때 도움이 된다.

결론

React와 TypeScript를 함께 쓰면 훅의 입력과 출력을 명확히 하여 안정적인 코드를 만들 수 있다. useState의 타입 추론을 이해하고, 제네릭 훅과 반환 타입 설계를 적절히 선택하면 재사용성과 안전성이 크게 향상된다. 본문에서 다룬 예제는 실무에서 바로 응용할 수 있는 패턴이다. 검색 키워드로 "react hook typescript 예제", "custom hook 타입 선언", "useState 타입 추론 react"를 참고하면 추가 자료를 찾는 데 도움이 된다.

react typescript react hook typescript 예제 custom hook 타입 선언 useState 타입 추론 react 타입스크립트 훅 제네릭 훅 React Hooks 타입