타입스크립트로 하는 리덕스

sham·2021년 12월 10일
0

Betti 개발일지

목록 보기
12/14

https://react.vlpt.us/using-typescript/05-ts-redux.html
https://velog.io/@velopert/use-typescript-and-redux-like-a-pro

개요

타입스크립트에서 리덕스를 설치하는 방법에 대해 정리했다.

세팅

npx create-react-app practice-redux --template typescript
npm install redux react-redux @types/react-redux

react-redux는 자체적으로 TypeScript 지원이 되지 않기 때문에 @types 이 붙은 패키지를 설정해주어야 한다.

src/modules/counter.ts

// 액션 타입 선언

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

// 액션 생성 함수 선언
export const increase = () => ({ type: INCREASE });
export const decrease = () => ({ type: DECREASE });
export const increaseBy = (diff: number) => ({
    type: INCREASE_BY,
    payload: diff
});

// 액션 객체들에 대한 타입 설정
type CounterAction =
    | ReturnType<typeof increase>
    | ReturnType<typeof decrease>
    | ReturnType<typeof increaseBy>;

// 상태의 타입 및 초깃값 선언

type CounterState = {
    counter: number;
}

const initialState: CounterState = {
    counter: 0
};
// 리듀서 선언

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

export default counter;

as const 를 문자열 뒤에 붙여주면 추후 액션 함수를 생성할 때 문자열이 아닌 고유한 타입으로 인식된다.

ReturnType은 함수에서 반환한 리턴 타입을 가져올 수 있게 해준다.

액션 타입을 선언하고 그 타입을 참고하는 액션 함수를 만든다. payload는 FSA 규칙을 따르기 위함이라는데, 액션의 값을 payload로 통일함으로써 형식을 표준화하려는 것 같다.

모든 액션들의 타입들을 선언해주고, 리듀서에서 사용할 state의 타입과 초깃값을 선언한 후 리듀서를 선언해준다.

이렇게 하나의 리덕스 관련 타입, 액션 관련 함수, 리듀서를 한 파일에 모아놓는 것을 Ducks 패턴이라고 한단다.

src/modules/index.ts

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

const rootReducer = combineReducers({
    counter
});

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

모든 리듀서들을 하나로 묶어서 루트 리듀서를 만드는데, RootState 라는 타입을 만들어서 내보내주어야 한다. 이 타입은 useSelector를 사용 할 때 필요로 하게 된다.


프리젠테이셔널/컨테이너 구조

https://blueshw.github.io/2017/06/26/presentaional-component-container-component/
https://fe-churi.tistory.com/34
https://velog.io/@seong-dodo/React-Presentational컴포넌트와-Container컴포넌트

컴포넌트를 용도에 맞게 두 개의 컴포넌트로 구분하는 패턴이다.

프리젠테이션 컴포넌트는 props를 받아 그대로 출력하는, 눈에 보여지는 부분을 담당한다. 데이터를 변경하지 않고, 리덕스 액션을 날리지도 않는다.

컨테이너 컴포넌트는 액션을 날리고 값을 변경하는 등, 눈에 보여지지 않는 부분을 담당한다. 프리젠테이션 컴포넌트에 props를 날리는 역할을 담당한다.

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

카운터 프리젠테이셔널 컴포넌트를 만든다. 이 프리젠테이셔널 컴포넌트는 동일한 컨테이너 컴포넌트로부터 props를 전달받아 렌더링을 담당할 것이다.

src/containers/CounterContainer.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;

state를 사용할 것이기에 useSelector로 state를 받으면서 타입을 아까 지정한 RootState로 사용한다.

src/App.tsx

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

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

export default App;

커스텀 훅을 이용한다면

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;

src/App.tsx

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

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

export default App;
profile
씨앗 개발자

0개의 댓글