공부해봅시당

[Typescript] Hook 본문

STUDY/Typescript

[Typescript] Hook

tngus 2024. 9. 3. 14:03

기본 훅

 

리액트 훅이 추가되면서 함수형 컴포넌트가 주가 되기전에는, 클래스 컴포넌트를 사용하였다. 하지만 이럴 경우 생명주기 함수에서만 상태 업데이트에 따른 로직을 실행시킬 수 있었고, 프로젝트 규모가 커질수록 스토어에 상태를 연결하거나 비슷한 로직을 가진 상태 업데이트 및 사이드 이펙트 처리가 불편해졌다. 또한 모든 상태를 하나의 함수 내에서 처리하다 보니 관심사가 뒤섞이게 되었고, 상태에 따른 테스트나 잘못 발생한 사이드 이펙트의 디버깅이 어려워졌다.

 

이번 장에서는 자주 사용하는 훅의 타입과 사용에 대해서 설명해준다.

리액트에서 hook을 import하고 vscode에서 ctrl+왼쪽클릭(윈도우)을 하면 타입을 확인할 수 있다.

1. useState

function useState<S>(initialState: S | (() => S)): [S, Dispatch<SetStateAction<S>>];
    
type Dispatch<A> = (value: A) => void;
type SetStateAction<S> = S | ((prevState: S) => S);

 

앞선 장에서도 언급했지만  useState는 튜플을 반환한다.

첫 번째 요소는 제네릭으로 지정한 S타입이며, 두 번째 요소는 상태를 업데이트할 수 있는 Dispatch 타입의 함수이다.

Dispatch함수의 제네릭으로 지정한 SetStateAction에는 useState로 관리할 상태 타입인 S또는 이전 상태 값을 받아 새로운 상태를 반환하는 함수가 있다.       (prevState:S)=>S 

이처럼 useState를 동기적으로 처리하기 위해 사용한다

useState가 동기적으로 처리된다는 말이 의아할 수 있는데, useState는 동기적으로 처리되나, 해당 값의 반영은 렌더링 사이클에 의해 비동기적으로 작동한다.

 

 

2. 의존성 배열을 사용하는 훅 (useEffect,useLayoutEffect)

공식문서에서는 아래와 같이 설명한다

The Effect Hook lets you perform side effects in function components:

두 함수 모두 함수형 컴퍼넌트에서 렌더링 이후에 부가 작용(side effect)를 실행하기 위해 실행되는데 차이점은

useEffect는 기존 생명주기 함수와 다르게, 레이아웃 배치와 화면 렌더링이 모두 완료된 후에 실행된다.
이와 달리 useLayoutEffect는 컴포넌트가 그려지기 전에 콜백 함수를 싱행하기 때문에 첫 번째 렌더링 때 빈 이름이 뜨는 경우를 방지할 수 있다.

 

function useEffect(effect: EffectCallback, deps?: DependencyList): void;
function useLayoutEffect(effect: EffectCallback, deps?: DependencyList): void;

type EffectCallback = () => void | Destructor;
type Destructor = () => void | { [UNDEFINED_VOID_ONLY]: never };
type DependencyList = readonly unknown[];

 

첫 번쨰 인자인 effect의 타입인 EffectCallback은 Destructorㄹ르 반환하거나 아무것도 반환하지 않는 함수이다.

Promise 타입은 반환하지 않으므로 useEffect의 콜백함수에는 비동기 함수가 들어갈 수 없다. useEffect내에서 비동기 함수를 호출시 경쟁 상태를 불러 일으킬 수 있기 때문이다.

 

 

두 번째 인자인 deps는 옵셔널하게 제공되며 effect가 수행되기 위한 조건을 나열한다. 이때 deps의 원소로 기본 자료형이 아닌 객체나 배열을 넣을 때는 얕은 비교를 하기 때문에 주의해야한다(참조값이 변경되면 콜백함수가 실행됨)

 

useEffect의 콜백함수는 컴포넌트가 마운트 및 리랜더링 될 때 실행되고, 클린업함수(Destructor)의 경우에는 리랜더링 될 때(콜백함수 이전에) 및 언마운트 될 때 실행된다.

 

3. useMemo와 useCallback

useMemo와 useCallback 모두 이전에 생성된 값 또는 함수를 기억하며, 동일한 값과 함수를 반복해서 생성하지 않도록 해주는 훅이다. 어떤 값을 계산하는 데 오랜 시간이 걸릴 때나 렌더링이 자주 발생하는 form에서 useMemo나 useCallback을 유용하게 사용할 수 있다. 다만 과도하게 메모이제이션할 경우 컴포넌트 성능 향상이 보장되지 않을 수 있다.

 

function useMemo<T>(factory: () => T, deps: DependencyList): T;
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;

type DependencyList = readonly unknown[];

두 훅 모두 제네릭을 지원하기 위해 T타입을 선언해준다.

 

4.  useRef

react에서도 특정 컴포넌트의 위치로 스크롤 하는 등 DOM을 직접 선택해야 하는 경우가 발생할 수 있다. 이때 리액트의 useRef를 사용한다.

ref의 특징으로는

  • 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;
}

 

MutableRefObject의 current는 값을 변경할 수 있다. 만약 null을 허용하기 위해 useRef의 제네릭에 HTMLInputElement |null 타입을 넣어주었다면, 해당 useRef는 첫 번째 타입 정의를 따른다. 이때 MutableObject의 current는 변경할 수 있는 값이 되어 ref.current의 값이 바뀌는 사이드 이펙트가 발생할 수 있다.

 

반면 RefObject의 current는 readonly로 값을 변경할 수 없다.

즉, 초기값이 null이더라도 mutable로 사용하려면 제네릭에 null을 넣으면 된다.

  const ref = useRef<HTMLInputElement>(null);
  const mutableRef = useRef<HTMLInputElement | null>(null);

 

자식 컴포넌트에 ref를 전달할 떄는  forwardRef를 사용해야 한다.

ref라는 속성의 이름은 리액트에서 'DOM 요소 접근' 이라는 특수한 목적으로 사용되기 때문에 props를 넘겨주는 방식으로 전달할 수 없다.(ref가 아닌 inputRef등 다른 이름 사용할 경우 forwardref 사용하지 않아도 된다.) 

interface Props {
  name: string;
}
const TestInput = forwardRef<HTMLInputElement, Props>((props, ref) => {
  return (
    <div>
      <label>{props.name}</label>
      <input ref={ref} />
    </div>
  );
});

//Type
function forwardRef<T, P = {}>(
    render: ForwardRefRenderFunction<T, P>,
): ForwardRefExoticComponent<PropsWithoutRef<P> & RefAttributes<T>>;

interface ForwardRefRenderFunction<T, P = {}> {
    (props: P, ref: ForwardedRef<T>): ReactNode;
 	displayName?: string | undefined;
    defaultProps?: never | undefined;
    propTypes?: never | undefined;
}

type ForwardedRef<T> = ((instance: T | null) => void) | MutableRefObject<T | null> | null;

 

forwardRef에 인자로 넘겨주는 콜백함수의 타입인 ForwardRefRendFunction은 2개의 타입 매개변수 T와 P를 받는다. P는 일반적인 리액트 컴퍼넌트의 props의 타입, T는 ref로 전달하려는 요소의 타입을 나타낸다.

ref의 타입이 T를 래핑한 형태인 ForwardedRef<T>인데 여기서는 MutableRefObject<T|null>을 통해서 앞선 ref와 다르게 RefObject가 올 수 없다. 따라서 부모 컴포넌트에서 ref를 어떻게 선언했는지와 관계없이 자식 컴포넌트가 해당 ref를 수용할 수 있다.

 

또한 useImperativeHandle 훅은 ForwardRefRendFunction과 함꼐 쓸 경우, 부모 컴포넌트에서 ref를 통해 자식 컴포넌트에서 정의한 커스터마이징된 메서드를 호출할 수 있게 된다. 즉 자식 컴포넌터는 내부 상태나 로직을 관리하면서 부모컴포넌트와의 결합도를 낮출 수 있다.

const JobCreateForm : React.ForwardRefRenderFunction<CreateFormHandle, CreateFormProps>= (props,ref)=>{
  useImperativeHandle(ref,()=>({
    submit:()=>{
      //
    }
  }))
}

 

커스텀 훅

리액트에서 기본적으로 제공하는 훅들에 더해, 사용자 정의 훅을 생성하여 컴포넌트 로직을 함수로 뽑아내 재사용할 수 있다.

가장 기본적인 훅으로는 onChange함수를 input값과 함께 반환하는 훅이다. 

 

import { useState } from 'react';

const useInput = (initialValue) => {
  const [value, setValue] = useState(initialValue);
  const onChange = (e) => {
    setValue(e.target.value);
  };

  return { value, onChange };
};

이를 타입스크립트에 적용하고, useCallback으로 최적화하면 아래와 같다.

import { ChangeEvent, useCallback, useState } from 'react';

export const useInput = (initialValue: string) => {
  const [value, setValue] = useState(initialValue);
  const onChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
    setValue(e.target.value);
  }, []);

  return { value, onChange };
};

 


출처

https://ungumungum.tistory.com/93

 

우아한 타입스크립트 with React - 9장 훅

기본 훅 리액트 훅이 추가되면서 함수형 컴포넌트가 주가 되기전에는, 클래스 컴포넌트를 사용하였다. 하지만 이럴 경우 생명주기 함수에서만 상태 업데이트에 따른 로직을 실행시킬 수 있었고

ungumungum.tistory.com

 

'STUDY > Typescript' 카테고리의 다른 글

[Typescript] 상태관리  (1) 2024.09.03
[Typescript] JSX에서 TSX로  (1) 2024.09.03
[Javascript] JSX(JavaScript XML)  (0) 2024.09.03
[Typescript] 비동기 호출  (0) 2024.08.20
[Typescript] 타입스크립트 컴파일  (0) 2024.08.20