Part9_Hooks
장경은
책 읽기

[9장] 훅

9.1 리액트 훅

useEffect와 useLayoutEffect

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
type DependencyList = ReadonlyArray<any>;
type EffectCallback = () => void | Destructor;
  • useEffect의 첫 번째 인자이자 effect의 타입인 EffectCallback은 Destructor를 반환하거나 아무것도 반환하지 않는 함수이다.

    • Promise 타입은 반환하지 않으므로 useEffect의 콜백 함수에는 비동기 함수가 들어갈 수 없다.
    • useEffect에서 비동기 함수를 호출할 수 있다면 경쟁 상태(Race Condition)를 불러일으킬 수 있기 때문이다.
  • useEffect는 deps가 변경되었는지를 얕은 비교로만 판단하기 때문에, 실제 객체 값이 바뀌지 않았더라도 객체의 참조 값이 변경되면 콜백 함수가 실행된다.

    • 이런 특징은 이후에 살펴볼 useMemo나 useCallback과 같은 다른 훅에서도 동일하게 적용 된다.
  • useEffect는 살펴본 것처럼 Destructor를 반환하는데 이것은 컴포넌트가 마운트 해제될 때 실행하는 함수이다.

    • 하지만 이 말은 어느 정도만 맞다. deps가 빈 배열이라면 useEffect의 콜백 함수는 컴포넌트가 처음 렌더링될 때만 실행되며, 이때의 Destructor(클린업 함수라고도 한다)는 컴포넌트가 마운트 해제될 때 실행된다.
    • 그러나 deps 배열이 존재한다면, 배열의 값이 변경될 때마다 Destructor가 실행된다.
  • useLayoutEffect를 사용하면 화면에 해당 컴포넌트가 그려지기 전에 콜백 함수를 실행하기 때문에 첫 번째 렌더링 때 빈 이름이 뜨는 경우를 방지할 수 있다.

useRef

  • useRef는 세 종류의 타입 정의를 가지고 있다.
    • useRef에 넣어주는 인자 타입에 따라 반환되는 타입이 달라진다.
function useRef<T>(initialValue: T): MutableRefObject<T>;
function useRef<T>(initialValue: T | null): RefObject<T>;
function useRef<T = undefined>(): MutableRefObject<T | undefined>;
 
interface MutableRefObject<T> {
    current: T;
}
 
interface RefObject<T> {
    readonly current: T | null;
}
  • 위처럼 useRef는 MutableRefObject 또는 RefObject를 반환한다.

    • MutableRefObject의 current는 값을 변경할 수 있다.
    • 만약 null을 허용하기 위해 useRef의 제네릭에 HTMLInputElement | null 타입을 넣어주었다면, 해당 useRef는 첫 번째 타입 정의(function useRef<T>(initialValue: T): MutableRefObject<T>;)를 따를 것이다.
    • 이때 MutableObject의 current는 변경할 수 있는 값이 되어 ref.current의 값이 바뀌는 사이드 이펙트가 발생할 수 있다.
    • 반면 RefObject의 current는 readonly로 값을 변경할 수 없다.
  • ref라는 속성의 이름은 리액트에서 'DOM 요소 접근'이라는 특수한 목적으로 사용되기 때문에 props를 넘겨주는 방식으로 전달할 수 없다.

    • 이때 forwardRef가 사용된다.
    • ref가 아닌 inputRef 등의 다른 이름을 사용한다면 forwardRef를 사용하지 않아도 된다.
  • useImperativeHandle은 ForwardRefRenderFunction과 함께 쓸 수 있는 훅이다.

    • 이 훅을 활용하면 부모 컴포넌트에서 ref를 통해 자식 컴포넌트에서 정의한 커스터마이징된 메서드를 호출할 수 있게 된다.
    • 이에 따라 자식 컴포넌트는 내부 상태나 로직을 관리하면서 부모 컴포넌트와의 결합도도 낮출 수 있다.
// <form> 태그의 submit 함수만 따로 뽑아와서 정의한다
type CreateFormHandle = Pick<HTMLFormElement, 'submit'>;
 
type CreateFormProps = {
    defaultValues?: CreateFormValue;
};
 
const JobCreateForm: React.ForwardRefRenderFunction<CreateFormHandle, CreateFormProps> = (props, ref) => {
    // useImperativeHandle Hook을 사용해서 submit 함수를 커스터마이징한다
    useImperativeHandle(ref, () => ({
        submit: () => {
            /* submit 작업을 진행 */
        }
    }));
 
    // ...
};
  • 이렇게 하면 부모 컴포넌트에서는 current.submit()을 사용하여 자식 컴포넌트의 특정 메서드를 실행할 수 있다.