개발새발
React Hooks 분석하기 본문
React에서 제공하는 Hook들은 모두 타입스크립트와 함께 사용할 수 있도록 정교하게 정의되어 있다. 본 글에서는 useState, useEffect, useRef, useMemo, useCallback 등 대표적인 Hook의 타입 정의를 공식 타입 정의 파일을 기준으로 하나씩 분석하고, 이를 실제 코드와 연결해 살펴보려고 한다.
1. useState 타입 분석
useState는 상태와 해당 상태를 업데이트하는 함수를 반환한다. 다음과 같이 정의되어 있다:
function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
function useState<S = undefined>(): [S | undefined, Dispatch<SetStateAction<S | undefined>>];
핵심 개념
- Dispatch<A>는 (value: A) => void 함수이다.
- SetStateAction<S>는 S 또는 (prev: S) => S 타입이다.
즉, 상태 업데이트 함수는 아래 두 가지 방식 모두 지원한다:
setState("value");
setState(prev => prev + "value");
제네릭 없이 사용하는 경우
const [value, setValue] = useState();
타입은 undefined로 추론되므로, setValue(1) 같은 코드는 에러가 나지 않지만 의도치 않은 사용이 가능해진다.
명시적 제네릭 사용
const [value, setValue] = useState<string>();
이렇게 하면 value의 타입은 string | undefined가 되어, 안전하게 사용할 수 있다.
2. useRef 타입 분석
useRef는 다음 세 가지 오버로딩이 있다:
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
MutableRefObject<T> vs RefObject<T>
항 | MutableRefObject | RefObject |
current | T | `T |
수정 가능 여부 | ✅ 가능 | ❌ readonly |
interface MutableRefObject<T> { current: T; }
interface RefObject<T> { readonly current: T | null; }
왜 useRef(null)이 에러가 없는가?
const inputEl = useRef(null);
이는 두 번째 오버로딩에 해당되어 RefObject<T>가 된다. ref={inputEl}과 같이 JSX에서 사용할 수 있다.
에러가 나는 경우
const inputEl = useRef(); // MutableRefObject<undefined>
이 경우 ref={inputEl}에서 타입 에러가 발생한다. JSX의 ref는 RefObject<T> 또는 RefCallback<T>를 기대하기 때문이다.
3. useEffect 타입 분석
function useEffect(effect: EffectCallback, deps?: DependencyList): void;
- EffectCallback: () => void | Destructor
- DependencyList: readonly unknown[]
Destructor 타입은 아래와 같다:
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };
이러한 형태는 TypeScript의 void의 너그러움을 제어하기 위한 타입 해킹이다. void는 return 1 같은 것도 허용하므로, 명시적으로 다른 값을 반환하지 못하게 하기 위해 고안된 방식이다.
4. useCallback, useMemo 타입 비교
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
function useMemo<T>(factory: () => T, deps: DependencyList | undefined): T;
- useCallback은 함수를 반환할 때 쓰며 deps를 필수로 받는다.
- useMemo는 값을 반환할 때 쓰며 deps를 생략 가능하지만, 실수를 방지하기 위해 undefined를 직접 넣도록 유도한다.
// OK
const value = useMemo(() => computeHeavy(), undefined);
// Error 없이 동작하지만 추천되지 않음
const value = useMemo(() => computeHeavy());
5. useCallback의 타입 제약
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
T extends Function이기 때문에, any 매개변수를 명시적으로 타이핑하지 않으면 에러가 발생한다.
const onChange = useCallback((e) => {
setValue(e.target.value);
}, []); // ❌ 매개변수 e의 타입이 any로 추론됨 → noImplicitAny 에러
해결 방법
const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
}, []);
또는 아래처럼 변수에 직접 타입을 지정해도 된다:
const onChange: ChangeEventHandler<HTMLInputElement> = useCallback((e) => {
setValue(e.currentTarget.value);
}, []);
6. DOM 이벤트 타입 찾기
JSX에서 form의 onSubmit과 input의 onChange는 아래와 같은 타입을 사용한다:
type FormEventHandler<T = Element> = (event: FormEvent<T>) => void;
type ChangeEventHandler<T = Element> = (event: ChangeEvent<T>) => void;
따라서 정확한 이벤트 타입은 아래와 같다:
const onSubmitForm: FormEventHandler<HTMLFormElement> = ...
const onChange: ChangeEventHandler<HTMLInputElement> = ...
'Typescript' 카테고리의 다른 글
React 직접 타이핑 하기 (0) | 2025.05.30 |
---|---|
JSX 타입 이해하기 (0) | 2025.05.24 |
React 타입 분석하기 (1) | 2025.05.24 |
axios의 타입을 어떻게 찾았는지 이해하기 (0) | 2025.05.17 |
다양한 모듈 형식으로 js 파일 생성하기 (0) | 2025.05.17 |