Typescript with React

Seunghwa's Devlog·2021년 3월 3일
0

Typescript에서 useState hook을 사용할 때 해당 상태가 어떤 타입을 가지는지 Generics를 사용하여 설정해준다.

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState<number>(0); // Generics 사용
  const onIncrease = () => setCount(count + 1);
  const onDecrease = () => setCount(count - 1);
  return (
    <div>
      <h1>{count}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
}

export default Counter;

하지만, Generics를 생략해도 상관없다. 알아서 타입을 잘 유추하기 때문이다

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);
  const onIncrease = () => setCount(count + 1);
  const onDecrease = () => setCount(count - 1);
  return (
    <div>
      <h1>{count}</h1>
      <div>
        <button onClick={onIncrease}>+1</button>
        <button onClick={onDecrease}>-1</button>
      </div>
    </div>
  );
}

export default Counter;

하지만 꼭 써야할 경우가 존재한다. 바로 상태가 null일 수도 있고 아닐 수도 있을 때 사용한다.

type Information = { name: string; description: string };
const [info, setInformation] = useState<Information | null>(null);

추가적으로 상태의 타입이 까다로운 구조를 가진 객체이거나 배열일 때는 Generics를 명시하는 것이 좋다.

type Todo = { id: number; text: string; done: boolean };
const [todos, setTodos] = useState<Todo[]>([]);

배열인 경우에는 빈 배열만 넣었을 때 해당 값이 어떤 Type인지 유추할 수 없기 때문에 Generics를 명시해야 한다

  • useState hook 사용
import React, {useState} from 'react';

function Counter(){
    const [count, setCount] = useState(0);
    const onIncrease = () => setCount(count + 1);
    const onDecrease = () => setCount(count - 1);
    return(
        <div>
            <h1>{count}</h1>
            <div>
                <button onClick={onIncrease}>+1</button>
                <button onClick={onDecrease}>-1</button>
            </div>
        </div>
    );
}
  • useReducer hook 사용
import React, {useReducer} from 'react';
type Action = {type : 'INCREASE'} | {type : 'DECREASE'};

function reducer(state : number, action : Action) : number {
//  리듀서를 만들 땐 이렇게 파라미터로 받아오는 상태의 타입과 함수가 리턴하는 타입을 동일하게 하는 것이 매우 중요합니다.
    switch(action.type){
        case 'INCREASE' :
            return state + 1;
        case 'DECREASE' :
            return state - 1;
        default :
        throw new Error('Unhandled action');
    }
}

function Counter(){
    const [count, dispatch] = useReducer(reducer, 0);
    const onIncrease = () => dispatch({type : 'INCREASE'});
    const onDecrease = () => dispatch({type : 'DECREASE'});

    return(
        <div>
            <h1>{count}</h1>
            <div>
                <button onClick={onIncrease}>+1</button>
                <button onClick={onDecrease}>-1</button>
            </div>
        </div>
    );
}

export default Counter;

Typescript 환경에서 Redux 사용하기

  • 라이브러리 설치
yarn add redux react-redux @types/react-redux

redux의 경우에는 자체적으로 Typescript 지원이 되지만 react-redux의 경우 그렇지 않으므로 @types를 붙여서 패키지를 설치해야한다.

  • EX) 카운터 구현을 위한 Redux 사용

src/modules/counter.ts
1) 액션 type 선언

const INCREASE = 'counter/INCREASE' as const;
const DECREASE = 'counter/DECREASE' as const;
const INCREASE_BY = 'counter/INCREASE_BY' as const;

as const는 const assertions 문법 → 추후 액션 생성함수를 통해 액션 객체를 만들게 됐을 때 type의 Typescript타입이 string이 되지 않고 실제 값을 가리키게 됨

2) 액션 생성 함수 선언

export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
  type: INCREASE_BY,
  payload: diff
})

추후 액션 생성 함수들은 컨테이너 컴포넌트에서 불러와서 사용을 해야하므로

3) 액션 객체들에 대한 type 설정

type CounterAction =
  | ReturnType<typeof increase>
  | ReturnType<typeof decrease>
  | ReturnType<typeof increaseBy>;

type : Typescript의 타입

나중에 리듀서를 작성 할 때 action 파라미터의 타입을 설정하기 위해서 모든 액션들의 TypeScript 타입을 준비해주어야 한다.

ReturnType : 함수에서 반환하는 타입을 가져올 수 있게 해주는 유틸 타입

이전의 액션에 type 값들을 선언 할 때 as const를 사용하지 않으면 ReturnType을 사용하게 됐을때 type 의 타입이 무조건 string으로 처리됨

4) 상태의 타입과 초기값 선언

type CounterState = {
  count: number;
}

const initialState: CounterState = {
  count: 0
};

5) 리듀서 작성

function counter(state: CounterState = initialState, action: CounterAction) {
  switch (action.type) {
    case INCREASE:
      return { count: state.count + 1 };
    case DECREASE:
      return { count: state.count - 1 };
    case INCREASE_BY:
      return { count: state.count + action.payload };
    default:
      return state;
  }
}

export default counter;

함수의 반환 타입에 상태의 타입을 넣는 것 잊지말기!

  • Redux 적용
    src/modules/index.ts

1) 루트 리듀서 생성

import { combineReducers } from 'redux';
import counter from './counter';

const rootReducer = combineReducers({
  counter
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

Javascript와 다른 점은 RootState라는 타입을 만들어서 내보내 줘야 함

→ 추후 컨테이너 컴포넌트를 만들 때 스토어에서 관리하고 있는 상태를 조회하기 위해서 useSelector를 사용할 때 필요함

index.tsx

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { Provider } from 'react-redux';
import { createStore } from 'redux';
import rootReducer from './modules';

const store = createStore(rootReducer);

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
);

프리젠테이셔널 컴포넌트와 컨테이너 컴포넌트는 분리해도 되고 안 해도 상관 없다.

  • 카운터 프리젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리 O

src/components/Counter.tsx

프리젠테이셔널 컴포넌트 생성

// 프리젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리
import React from 'react';

type CounterProps = {
  count: number;
  onIncrease: () => void;
  onDecrease: () => void;
  onIncreaseBy: (diff: number) => void;
};

function Counter({
  count,
  onIncrease,
  onDecrease,
  onIncreaseBy
}: CounterProps) {
  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
      <button onClick={() => onIncreaseBy(5)}>+5</button>
    </div>
  );
}

export default Counter;

src/containers/CounterContainter.tsx
컨테이너 컴포넌트 생성

import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../modules';
import { increase, decrease, increaseBy } from '../modules/counter';
import Counter from '../components/Counter';

function CounterContainer() {
  const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();

  const onIncrease = () => {
    dispatch(increase());
  };

  const onDecrease = () => {
    dispatch(decrease());
  };

  const onIncreaseBy = (diff: number) => {
    dispatch(increaseBy(diff));
  };

  return (
    <Counter
      count={count}
      onIncrease={onIncrease}
      onDecrease={onDecrease}
      onIncreaseBy={onIncreaseBy}
    />
  );
}

export default CounterContainer;

src/App.tsx

import React from 'react';
import CounterContainer from './containers/CounterContainer';

function App() {
  return <CounterContainer />;
}

export default App;
  • 카운터 프리젠테이셔널 컴포넌트 / 컨테이너 컴포넌트 분리 X
    useSelector와 useDispatch로 이루어진 커스텀 Hook을 만들어서 사용

src/hooks/useCounter.tsx

import { useSelector, useDispatch } from 'react-redux';
import { RootState } from '../modules';
import { increase, decrease, increaseBy } from '../modules/counter';
import { useCallback } from 'react';

export default function useCounter() {
  const count = useSelector((state: RootState) => state.counter.count);
  const dispatch = useDispatch();

  const onIncrease = useCallback(() => dispatch(increase()), [dispatch]);
  const onDecrease = useCallback(() => dispatch(decrease()), [dispatch]);
  const onIncreaseBy = useCallback(
    (diff: number) => dispatch(increaseBy(diff)),
    [dispatch]
  );

  return {
    count,
    onIncrease,
    onDecrease,
    onIncreaseBy
  };
}

src/components/Counter.tsx

import React from 'react';
import useCounter from '../hooks/useCounter';

function Counter() {
  const { count, onIncrease, onDecrease, onIncreaseBy } = useCounter();

  return (
    <div>
      <h1>{count}</h1>
      <button onClick={onIncrease}>+1</button>
      <button onClick={onDecrease}>-1</button>
      <button onClick={() => onIncreaseBy(5)}>+5</button>
    </div>
  );
}

export default Counter;

필요한 함수와 값을 props로 받아오는 것이 아닌, useCounter hook을 통해서 받아옴

src/App.tsx

import React from 'react';
import Counter from './components/Counter';

function App() {
  return <Counter />;
}

export default App;
profile
에러와 부딪히고 새로운 것을 배우며 성장해가는 과정을 기록합니다!

0개의 댓글