본문 바로가기
공부/프로그래밍

[react + typescript] redux toolkit 사용하기(createAction , ActionType , createReducer)

by demonic_ 2020. 12. 23.
반응형

redux toolkit은 Redux의 공식 개발도구다. 액션 생성자, 리듀서 자체는 단순 함수인데 문제는 코드가 엄청나게 늘어난다는 불편함이 있다. redux toolkit을 사용하면 코드를 줄이는데 도움을 준다(그리 크진 않은듯...)

 

여기서는 typescript를 함께 사용하고 있다. 그럼 사용방법에 대해 알아보자

(여기선 리듀서에 대한 설명을 생략한다. 개념을 설명하는 포스팅이 아니기 때문)

 

 

우선 다음을 설치한다

npm i typesafe-actions

 

 

최근버전은 5.x 버전인데 함수하나가 달라졌다

이전에는 createStandardAction 기능이 5.x 로 넘어오면서 createAction 으로 변경되었다.

 

 

관련 github 주소: https://github.com/piotrwitek/typesafe-actions#v4xx-to-v5xx

 

piotrwitek/typesafe-actions

Typesafe utilities for "action-creators" in Redux / Flux Architecture - piotrwitek/typesafe-actions

github.com

 

본문내용중에 다음이 있다.

2. createStandardAction was renamed to createAction and .map method was removed in favor of simpler redux-actions style API.

 

 

 

그럼 이제 설정해보자.

우선 Reducer를 등록하는 index 파일을 생성한다.

import { combineReducers } from "redux";
import testReducer from "./testReducer";


const rootReducer = combineReducers({
    testReducer
})

export default rootReducer;

export type RootState = ReturnType<typeof rootReducer>;

 

 

testReducer.ts 파일을 이용해 리듀서를 설정한다.

아래는 완성본이고 하나씩 해석해보기로 하자.

import {
    createAction
    , createReducer
} from 'typesafe-actions';

// 상태의 타입 선언
interface TestReducer {
    no: number;
    text: string;
}

// 상태 초기화
const initialState: TestReducer = {
    no: 0,
    text: "hello"
}

// 액션타입 선언
export const RESET_TEXT = "testReducer/RESET_TEXT";
export const ADD_TEXT = "testReducer/ADD_TEXT";

// 액션함수 선언
export const resetText = createAction(RESET_TEXT)();
export const addText = createAction(ADD_TEXT)<TestReducer>()
export const removeText = createAction(REMOVE_TEXT)()


// 액션 객체타입 선언
export const actions = {resetText, addText, removeText}
type TestReducerActions = ActionType<typeof actions>;


// 리듀서 추가
const testReducer = createReducer<TestReducer, TestReducerActions>(initialState, {
    [RESET_TEXT]: () => ({
        no: 0,
        text: ""
    }),
    [ADD_TEXT]: (state, action) => {
        console.log(state.text)
        return ({
            no: action.payload.no,
            text: action.payload.text
        })
    }, 
    [REMOVE_TEXT]: () => ({
        text: ""
    })
})


export default testReducer;

 

 

initialState 로 초기값을 설정했고, TestReducer로 값을 선언했다.

 

그리고 액션, 액션함수, 리듀서를 등록해야 하는데 액션함수와 리듀서를 등록할 때 toolkit 에서 제공하는 createAction , ActionType , createReducer 3가지를 사용한다.

 

 

1) 액션함수 선언

createAction 을 이용해 액션을 등록할 수 있는데, 중요한 것은 함수로 등록해야 한다. 예를들어 다음과 같다.

# 끝에 () 를 입력함
export const resetText = createAction(RESET_TEXT)();

끝에 ()를 붙이지 않으면 다음의 에러가 발생한다

Unhandled Runtime Error
Error: Actions must be plain objects. Use custom middleware for async actions.

 

첫번째 액션(RESET_TEXT)은 파라미터가 없다. 그러나 두번째 액션(ADD_TEXT)는 파라미터를 받아 State를 갱신해야 한다. 그래서 설정을 다음과 같이 해주었다.

# TypeScript 적용
export const addText = createAction(ADD_TEXT)<TestReducer>()

 

참고로 끝에 선언하는 부분인 <TestReducer> 를 빼도 작동은 한다. 다만 이렇게 할경우 추후 타입스크립트로 오류검토할때 에러를 리턴한다. 똑똑한 에디터라면 해당 액션을 호출하는 곳에서부터 빨간줄이 그어져 있다.

 

 

2) 액션함수 등록

이부분은 생략해도 된다. 만약 이걸 생략할거면 import 한 ActionType 도 빼도 된다.

export const actions = {resetText, addText}
type TestReducerActions = ActionType<typeof actions>;

 

번거로움을 무릎쓰고서라도 등록하면 2가지 장점이 있다.

1) 위에서 등록한 액션이 리듀서에도 등록되어있는지 검토

2) 컴포넌트에서 actions.[액션함수명]을 이용해 호출 가능

1번의 경우 리듀서에 추가로 더 등록하거나 뺐을 경우 에러를 리턴한다.

 

예를들면 다음과 같이 REMOVE_TEXT라는 액션을 추가했다 하자

# 액션 선언
export const REMOVE_TEXT = "testReducer/REMOVE_TEXT";
# 액션함수 추가
export const removeText = createAction(REMOVE_TEXT)()

 

그런데 이걸 Test actions 에 등록하지 않은 채로 리듀서에 등록하면 다음과 같은 빨간줄을 볼 수 있다.

 

2번인 actions.[액션함수명]을 이용해 호출 가능한 부분은 컴포넌트에서 다음처럼 코딩이 가능하다

import { actions } from "../reducers/testReducer";

...
    const dispatch = useDispatch();
   
    dispatch(actions.addText({no: 10, text: value}))
....

 

 

2) 리듀서 등록

마지막으로 리듀서 등록이다. createReducer를 이용해 등록가능하며, 액션함수를 등록했다면 여기 설정시 같이 등록할 수 있다.

// 타입스크립트를 적용하여 등록하는 경우
const testReducer = createReducer<TestReducer, TestReducerActions>(initialState, {
    [RESET_TEXT]: () => ({
        no: 0,
        text: ""
    }),
    [ADD_TEXT]: (state, action) => {
        return ({
            no: action.payload.no,
            text: action.payload.text
        })
    },
    [REMOVE_TEXT]: () => ({
        no: 0,
        text: ""
    })
})

 

타입스크립트를 적용하지 않은 채 등록하는 경우 아래처럼 하면 된다.

const testReducer = createReducer(initialState, {
    [RESET_TEXT]: () => ({
        no: 0,
        text: ""
    }),
    [ADD_TEXT]: (state, action) => {
        return ({
            no: action.payload.no,
            text: action.payload.text
        })
    },
    [REMOVE_TEXT]: () => ({
        no: 0,
        text: ""
    })
})

 

 

여기서 중요한 점은 최종값을 반드시 리턴해줘야 한다는 점인데 RESET_TEXT의 경우는 return을 명시하지 않아도 자동으로 리턴한다.

반대로 ADD_TEXT의 경우는 action.payload 안에 들어있는 값을 통해 state를 갱신한다.

 

 

payload라는 것이 새롭게 등장했는데, reducer를 호출하면서 value에 대한 값을 모두 여기에 넣어준다.

그래서 실제로 호출하는 것을 console.log로 찍어보면 다음과 같은 값이 넘어옴을 확인할 수 있다.

 

만약 payload 라는 변수명 말고 다른유형으로 선언하고 싶다면 액션함수가 선언되는 곳에서 수정할 수 있는데, 다음처럼 만들면 된다.

...
export const addText = createAction(ADD_TEXT, (value) => {
    console.log("value: ", value)
    return {
        no: value.no
        , text: value.text
    }
})()
...

 

이럴경우 리듀서에서도 맞춰 수정해줘야 한다.

...
    [ADD_TEXT]: (state, action) => {
        console.log("state: ", state)
        console.log("action: ", action)
        // payload 방식
        // return ({
        //     no: action.payload.no,
        //     text: action.payload.text
        // })
        return ({
            no: action.no, 
            text: action.text
        })
    },
...

 

 

마지막으로 컴포넌트에서 호출하는 방법이다.

dispatch 를 이용해 호출할 수 있으며, 방법은 다음과 같이 2가지로 할 수 있다.

 

1) dispatch 에 직접 선언하는 방법

2) 액션함수를 호출하는 방법

import { RESET_TEXT, ADD_TEXT } from "../reducers/testReducer";

...
    const addTextHandler = () => {
        const value = "world";
        
        // 1번 방법
        dispatch({
            type: ADD_TEXT, 
            payload: {
                no: 10
                , text: value
            }
        })
        
        // 2번 방법
        dispatch(actions.addText({no: 10, text: value}))
    }

    return (
        <>
            <p>no: {no}</p>
            <p>hi {text || "없음"}</p>
            <div>
                <button onClick={addTextHandler}>
                    입력추가 버튼
                </button>
            </div>
        </>
    )
...

 

1번은 여타하는 방법과 같다. 다만 payload라는 이름을 추가했다.

2번의 경우 actions에 담긴 액션함수를 이용해 호출한다. 무엇을 선택하든 본인 취향이지만 개인적으로 2번이 더 편해보이는건 사실이다.

 

테스트하면 잘 작동한다

 

끝.

 

 

참조:

redux toolkit 공식문서

https://redux-toolkit.js.org/api/createAction

 

createAction | Redux Toolkit

createAction

redux-toolkit.js.org

 

반응형

댓글