React Hooks + typescript

H_Chang·2023년 12월 18일
0

타입스크립트를 이용하여 기본적인 리액트 훅을 사용하는 방법 알아보자

1. useState

타입을 정의하지 않고 사용하면 다음과 같은 에러가 난다.

⚠️ Type '사용한 타입(number, string...)' is not assignable to type 'never'.
당황하지 말고 useState 함수를 호출할 때 어떤 타입이 state로 들어갈지 지정해주면 된다.

export default function UseStateComponent() {
  const [arr, setArr] = useState<number[]>([]);
  // 타입 없이 useState([])를 쓰면 never[] 타입이 된다.
  // Error ===> Type 'number' is not assignable to type 'never'.
  // useState<number[]> 와 같이 타입을 지정해주기

  return (
    <div>
      <div>
        <button onClick={() => setArr([...arr, arr.length + 1])}>Add to array</button>
        {JSON.stringify(arr)}
      </div>
    </div>
  );
}

이 경우도 마찬가지다. null로 값을 초기화하여 타입스크립트는 name의 타입이 null일 것이라고 추론했고, SetStateAction이라는 내부 로직에서도 추론한 반환값인 null로 동작하려고 해 오류가 난다.

string 혹은 null 값이 name으로 올 수 있다고 정의해주자.

⚠️ TS2345: Argument of type '"Jim"' is not assignable to parameter of type 'SetStateAction'.

const [name, setName] = useState<string | null>(null);
return (
    <div>
      <button onClick={() => setName(**'Jim'**)}>Add to array</button>
      {JSON.stringify(name)}
    </div>
);

2. useEffect

useEffect의 정의를 살펴보자.(VS코드에서 f12 혹은 우클→Go to definition)


useEffect는 첫 번째 인자로 EffectCallback 타입을 받고 있다. EffectCallback이란 함수이며, ‘void’ 혹은 ‘void나 undefined를 반환하는 함수’를 반환한다는 것을 알 수 있다.

3. useContext

💡 props 드릴링을 할 필요 없이 createContext로 생성한 context를 이용해 Provider를 만들 수 있다. Provider의 자식 위치에서 useContext(스토어)를 사용하면 Consumer 로서 스토어에 저장된 값들을 빼올 수 있다.
컨텍스트를 만들어주는 createContext는 어떻게 정의되어 있을까?

제네릭으로 타입을 받아 해당 타입을 다시 반환하는 것을 볼 수 있다. defaultValue로 추론하지만 개발할 때 컨텍스트의 타입을 명시해주는 것이 좋다.

4. useReducer

💡 복잡한 상태관리가 필요할 때 사용한다. 현재 진행 중인 프로젝트에서는 리덕스 툴킷(RTK)를 사용할 것이지만 기본이 되는 리액트의 useReducer를 사용해 액션부터 차근차근 만들어보자.

import { useReducer } from 'react';

// reducer는 복잡한 상태 관리가 필요할 때 사용한다.
// 따라서 이런 간단한 상태는 예제만을 위한 것이라는 것을 염두에 두고 살펴보자.
const initialState = {
  counter: 100,
};

// ACTION 타입을 설정한다. type은 반드시 존재해야 하고 payload는 선택이다. 
type ACTIONTYPES = { type: 'increment'; payload: number } | { type: 'decrement'; payload: number };

// reducer 함수는 state의 값을 action에 따라 어떻게 처리할지 가이드해주는 순수함수다.
// default 값으로는 적절한 action type이 들어오지 않은 것이니 에러 처리를 해주자.
function counterReducer(state: typeof initialState, action: ACTIONTYPES) {
  switch (action.type) {
    case 'increment':
      return {
        ...state,
        counter: state.counter + action.payload,
      };
    case 'decrement':
      return {
        ...state,
        counter: state.counter - action.payload,
      };
    default:
      throw new Error('Bad action');
  }
}

// useReducer 훅을 이용해 위에서 만든 리듀서와 초기값을 전달한다.
// useReducer는 state와 dispatch를 반환하는데, state는 초기값과 같은 타입을 유지하며 상태관리되는 값이고
// dispatch는 Action(type,payload)을 받아 리듀서로 전달해주는 역할을 한다. 
function UseReducerComponent() {
  const [state, dispatch] = useReducer(counterReducer, initialState);
  return (
    <div>
      <div>{state.counter}</div>
      <button
        onClick={() =>
          dispatch({
            type: 'increment',
            payload: 10,
          })
        }
      >
        Increment
      </button>
      <button
        onClick={() =>
          dispatch({
            type: 'decrement',
            payload: 5,
          })
        }
      >
        Decrement
      </button>
    </div>
  );
}

export default UseReducerComponent;

5. useRef

💡 실제 DOM 요소를 추적하고 싶을 때 사용한다. 타입은 <해당 돔요소 | null> 로 해주면 끝!
DOM 요소들의 타입에 대해 알아보려면 공식문서를 참고하자.

Documentation - DOM Manipulation
https://www.typescriptlang.org/docs/handbook/dom-manipulation.html#the-document-interface

import { useRef } from 'react';

// 실제 DOM요소를 추적하고 싶을 때 사용

function UseRefComponent() {
  const inputRef = useRef<HTMLInputElement | null>(null);

  return <input ref={inputRef} />;
}

export default UseRefComponent;

6. CustomHooks

  • fetch로 json을 받아와 값(data)와 성공 여부(done)을 반환하는 커스텀 훅을 만들어 보자.
  • (이 예제는 로컬에 임의로 json 파일을 하나 저장해놓고 불러왔다.)
  • interface로 받아올 data의 타입을 정의했다.
export interface CardProps {
  url: string;
  user?: {
    image: string;
    link: string;
  };
}

6-1. 커스텀 훅 useFetchData 만들기 : 제네릭을 사용하지 않은 리팩토링이 필요한 코드다.

function useFetchData(url: string): { data: CardProps[] | null; done: boolean } {
  const [data, dataSet] = useState<CardProps[] | null>(null);
  const [done, doneSet] = useState(false);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((d: CardProps[]) => {
        dataSet(d);
        doneSet(true);
      });
  }, [url]);

  return { data, done };
}

function CustomHookComponent() {
  const { data, done } = useFetchData('/card-mock.json');
  return <div>{done && <img alt='' src={data?.[0].user?.image} />}</div>;
}

export default CustomHookComponent;

6-2. 커스텀 훅 useFetchData 만들기 : 제네릭을 사용해 재사용성이 높은 커스텀 훅으로 만들기

  • 6-1번에서 만든 커스텀 훅은 무조건 CardProps[] 타입을 반환하는 경우에만 사용할 수 있다. useFetchData가 요청에 따라 다양한 형태의 data를 받아오려면 어떻게 할까? ⇒ Generic 사용 !
  • 함수 정의시 로 지정하고, 필요한 곳에 타입을 사용
  • 함수 호출시 필요한 데이터 타입을 지정해준다
// 제네릭 함수로 변경해보자. 통상적으로 <T>를 사용한다.
// 함수 정의할 때 함수 이름 옆에 <T>
// 호출할 때 지정해 줄 data 타입이 필요한 곳에 모두 T로 지정한다.
function useFetchData<T>(url: string): { data: T | null; done: boolean } {
  const [data, dataSet] = useState<T| null>(null);
  const [done, doneSet] = useState(false);

  useEffect(() => {
    fetch(url)
      .then(res => res.json())
      .then((d: Payload) => {
        dataSet(d);
        doneSet(true);
      });
  }, [url]);

  return { data, done };
}

function CustomHookComponent() {
	// 제네릭 함수를 호출할 때 타입을 지정된다.
  const { data, done } = useFetchData**<CardProps[]>**('/card-mock.json');
  return <div>{done && <img alt='' src={data?.[0].user?.image} />}</div>;
}
profile
프론트 엔드 시작하는 뉴비!

0개의 댓글