리액트 Hooks + typescript

쏘쏘임·2022년 1월 16일
1

프론트엔드

목록 보기
2/6

🚩 글의 주제 : 타입스크립트를 이용하여 기본적인 리액트 훅을 사용하는 방법 알아보기

❓ 리액트도 아직 제대로 다뤄본 적이 없는데 생전 초면인 TS 까지 하려니 프로젝트 진행시 막히는 부분이 많은 것 같았다. 잠시 프로젝트를 내려놓고 TS를 이용해 Hooks를 하나하나 사용해보려고 한다.

1. 들어가기 전에

CRA로 타입스크립트용 리액트 프로젝트 만들기

npx create-react-app my-app --template typescript

함수의 definition 보는 방법

Hooks를 import한 후 사용하면 라이브러리에서 정의한 해당 Hooks(함수)의 정의를 볼 수 있다. (f12 혹은 우클→ Go to definition)

주의 사항

훅스의 사용법과 타입스크립트의 정의에 집중하기 위해 타입과 컴퍼넌트를 구분하지 않았습니다.

2. Hooks

2-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-2. useEffect

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

useEffect-1

useEffect-2

useEffect-3

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

2-3. useContext

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

컨텍스트를 만들어주는 createContext는 어떻게 정의되어 있을까?

createContext

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

이제 정의를 살펴봤으니 컨텍스트를 만들어보자

  1. createContext를 이용해 초기 값을 설정해준다.

    이 때 컨텍스트 타입을 명시해주는 방법은 다양하다.

    • 초기값을 가진 객체를 만들고, 이 초기값에 대한 type을 typeof로 뽑기
    import { createContext } from 'react';
    
    // 초기값 설정
    const initialState = {
      first: 'Jim',
      last: 'Julian',
    };
    
    // 초기값에 대한 타입 뽑기
    export type UserState = typeof initialState;
    
    // 타입을 사용해 컨텍스트 만들기
    const context = createContext<UserState>(initialState);
    
    export default context;
    • interface로 타입 설정 후 사용하기
    import { createContext } from 'react';
    
    export interface UserStateInterface {
      first: string;
      last: string;
    }
    
    const initialState: UserStateInterface = {
      first: 'Jim',
      last: 'Julian',
    };
    
    const context = createContext<UserStateInterface>(initialState);
    
    export default context;
  1. Provider 혹은 Consumer로 사용하기

    • 이제 생성한 컨텍스트의 Provider를 사용할 수 있다.
      • <UserContext.Provider value={user}>
    • useContext 훅을 이용해 Consumer를 만들어 컨텍스트의 값을 받아올 수 있다.
      • Consumer는 Provider의 자식 요소로 존재해야 한다.
    • 타입 지정은 위에서 export한 타입(UserState 혹은 UserStateInterface)으로 지정해주면 된다.
    import React, { useState, useContext } from 'react';
    import UserContext, { UserStateInterface, initialState } from './store';
    // 위에서 생성한 context는 default 방식으로 내보냈기 때문에 
    // UserContext와 같이 이름을 유동적으로 지을 수 있다.
    
    function ConsumerComponent() {
      const user = useContext<UserStateInterface>(UserContext);
      return (
        <div>
          <div>First: {user.first}</div>
          <div>Last: {user.last}</div>
        </div>
      );
    }
    
    function UseContextComponent() {
      const [user, setUser] = useState<UserStateInterface>(initialState);
      return (
        <UserContext.Provider value={user}>
          <ConsumerComponent />
          <button onClick={() => setUser({ first: 'new Name', last: 'new Last Name' })}>
            Change context
          </button>
        </UserContext.Provider>
      );
    }
    
    export default UseContextComponent;

2-4. useReducer

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

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;

2-5. useRef

💡 실제 DOM 요소를 추적하고 싶을 때 사용한다. 타입은 `<해당 돔요소 | null>` 로 해주면 끝!

DOM 요소들의 타입에 대해 알아보려면 공식문서를 참고하자.

Documentation - DOM Manipulation

import { useRef } from 'react';

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

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

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

export default UseRefComponent;

2-6. CustomHooks

  • fetch로 json을 받아와 값(data)와 성공 여부(done)을 반환하는 커스텀 훅을 만들어 보자.
  • (이 예제는 로컬에 임의로 json 파일을 하나 저장해놓고 불러왔다.)
  • interface로 받아올 data의 타입을 정의했다.
    export interface CardProps {
      url: string;
      user?: {
        image: string;
        link: string;
      };
    }
  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;
  2. 커스텀 훅 useFetchData 만들기 : 제네릭을 사용해 재사용성이 높은 커스텀 훅으로 만들기

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

useMemo

  • 나중에 추가적으로...

💡 알게된 점

  • 제네릭을 이용하는 경우 !
  • 훅들을 타입스크립트로 변환하는 법을 알아보려다 훅의 정의를 더 잘 알아볼 수 있었다.

📌 참고 링크

profile
무럭무럭 자라는 주니어 프론트엔드 개발자입니다.

0개의 댓글