본문 바로가기
JavaScript/React

React) 리덕스를 사용하여 리액트 애플리케이션 상태 관리하기 ③ - 리덕스 더 편하게 사용하기

by 박채니 2022. 12. 16.
안녕하세요, 코린이의 코딩 학습기 채니 입니다.
[리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다.

 

리덕스를 사용하여 리액트 애플리케이션 상태 관리하기

 

리덕스 더 편하게 사용하기

 

액션 생성 함수, 리듀서를 작성할 땐 redux-actions 라이브러리 / immer 라이브러리를 사용하여 편하게 사용 가능!

 

redux-actions

 

  • 액션 생성 함수를 더 짧은 코드로 작성 가능
  • 리듀서 작성 시에도 switch/case 문이 아닌 handleActions 함수를 사용
$ yarn add redux-actions

 

counter 모듈에 적용하기

modules/counter.js

import { createAction } from 'redux-actions';

...
...

// 액션 생성 함수 만들기
// export 키워드 -> 해당 함수를 다른 파일에서 불러와 사용 가능!
// export const increase = () => ({ type: INCREASE });
// export const decrease = () => ({ type: DECREASE });

// redux-actions 사용
export const increase = createAction(INCREASE);
export const decrease = createAction(DECREASE);

...
...

액션 생성 함수createAction 함수를 사용해 만들어줍니다.

redux-actions를 사용하기 전엔 매번 객체를 만들고 타입을 지정해주어야 했지만, redux-actions를 이용해 간편하게 처리해 줄 수 있습니다.

 

리듀서 함수도 가독성 있게 바꿔보겠습니다.

modules/counter.js

import { createAction, handleActions } from 'redux-actions';

// 초기 상태
const initialState = {
    number: 0
};

// 리듀서 함수 생성
//function counter(state = initialState, action) {
//    switch(action.type) {
//        case INCREASE : return { number: state.number + 1 };
//        case DECREASE : return { number: state.number - 1 };
//        default: return state;
//    }
// }

// redux-actions 사용
const counter = handleActions(
    {
        [INCREASE]: (state, action) => ({ number: state.number + 1 }),
        [DECREASE]: (state, action) => ({ number: state.number - 1 })
    },
    initialState
)

리듀서 함수는 handleActions 함수를 사용해줍니다.

handleActions 첫 번째 파라미터에는 각 액션에 대한 업데이트 함수를 넣어주고,

두 번째 파라미터에는 초기 상태를 넣어줍니다.

 

todos 모듈에 적용하기

counter 모듈과 동일하게 todos에도 적용해보겠습니다. 하지만 각 액션 생성 함수에 파라미터가 필요합니다.

createAction으로 액션을 만들면 액션에 필요한 추가 데이터는 payload라는 이름을 사용하게 됩니다.

 

예시

const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION);
const action = myAction('hello world');

/*
	결과:
	{ type: MY_ACTION, paylaod: 'hello world' }
*/

 

만일 파라미터 그대로를 payload에 넣지 않고 변형을 주고 싶다면 아래처럼 해줍니다.

const MY_ACTION = 'sample/MY_ACTION';
const myAction = createAction(MY_ACTION, text => `${text}!`);
const action = myAction('hello world');

/*
	결과:
	{ type: MY_ACTION, paylaod: 'hello world!' }
*/

 

modules/todos.js

import { createAction, handleActions } from "redux-actions";

...
...

// 액션 생성 함수 만들기
// export const changeInput = input => ({
//     type: CHANGE_INPUT,
//     input
// });

// let id = 3; // insert가 호출될 때마다 1씩 증가
// export const insert = text => ({
//     type: INSERT,
//     todo: {
//         id: id++,
//         text,
//         done: false
//     }
// });

// export const toggle = id => ({
//     type: TOGGLE,
//     id
// });

// export const remove = id => ({
//     type: REMOVE,
//     id
// });

// redux-actions 사용
export const changeInput = createAction(CHANGE_INPUT, input => input);

let id = 3;
export const insert = createAction(INSERT, text => ({
    id: id++,
    text,
    done: false
}));

export const toggle = createAction(TOGGLE, id => id);
export const remove = createAction(REMOVE, id => id);

...
...

훨씬 가독성이 좋아진 것을 확인할 수 있습니다.

여기서 insert에는 todo 객체를 가지고 있어야 하므로, text를 넣으면 todos 객체가 반환되는 함수를 넣어주었습니다.

나머지 함수에는 input => input 혹은 id => id 처럼 파라미터 그대로를 반환하는 함수를 넣어주었는데,

이를 생략해도 되지만 추후 코드 파악이 쉽도록 작성해주었습니다.

 

이제 리듀서를 재작성해보겠습니다.

추가 데이터들은 모두 payload의 이름을 갖고 있으므로 action.id 등이 아닌 action.payload로 값을 조회해주어야 합니다.

modules/todos.js

// 리듀서 함수 생성
// function todos(state = initialState, action) {
//     switch(action.type) {
//         case CHANGE_INPUT:
//             return {
//                 ...state,
//                 input: action.input
//             };
//         case INSERT:
//             return {
//                 ...state,
//                 todos: state.todos.concat(action.todo)
//             };
//         case TOGGLE: 
//             return {
//                 ...state,
//                 todos: state.todos.map(todo => todo.id === action.id ? { ...todo, done: !todo.done } : todo)
//             };
//         case REMOVE:
//             return {
//                 ...state,
//                 todos: state.todos.filter(todo => todo.id !== action.id)
//             };
//         default: return state;
//     }
// }

// redux-actions 사용
const todos = handleActions(
    {
        [CHANGE_INPUT]: (state, action) => ({ ...state, input: action.payload }),
        [INSERT]: (state, action) => ({
            ...state, 
            todos: state.todos.concat(action.payload)
        }),
        [TOGGLE]: (state, action) => ({
            ...state,
            todos: state.todos.map(todo => 
                todo.id === action.payload ? { ...todo, done: !todo.done } : todo
            )
        }),
        [REMOVE]: (state, action) => ({
            ...state,
            todos: state.todos.filter(todo => todo.id !== action.payload)
        })
    },
    initialState
)

export default todos;

하지만 action.payload 값을 이용하기 때문에 추후 헷갈릴 수도 있습니다.

이 때 비구조화 할당 문법으로 payload의 이름을 새로 설정해줍니다.

 

// redux-actions 사용
const todos = handleActions(
    {
        [CHANGE_INPUT]: (state, { payload: input }) => ({ ...state, input }),
        [INSERT]: (state, { payload: todo }) => ({
            ...state, 
            todos: state.todos.concat(todo)
        }),
        [TOGGLE]: (state, { payload: id }) => ({
            ...state,
            todos: state.todos.map(todo => 
                todo.id === id ? { ...todo, done: !todo.done } : todo
            )
        }),
        [REMOVE]: (state, { payload: id }) => ({
            ...state,
            todos: state.todos.filter(todo => todo.id !== id)
        })
    },
    initialState
)

알아보기 더 쉽죠!

 

immer

 

※ immer 관련 포스팅

https://chanychu.tistory.com/456

 

React) immer - 사용법 및 useState 함수형 업데이트와 immer 함께 쓰기

안녕하세요, 코린이의 코딩 학습기 채니 입니다. [리액트를 다루는 기술]의 책을 참고하여 포스팅한 개인 공부 내용입니다. immer - 구조가 복잡한 객체도 매우 쉽고 짧은 코드를 사용해 불변성을

chanychu.tistory.com

 

리듀서에서 상태 업데이트 시 불변성 유지를 위해 spread 연산자를 사용하였습니다.

만약 구조가 깊어진다면 코드가 복잡해지고 불변성을 지키기 어려워집니다. 이 때 immer를 사용해 쉽게 처리해줄 수 있습니다.

 

immer 설치

$ yarn add immer

 

counter 모듈처럼 간단한 리듀서에는 immer 사용 시 오히려 코드가 길어지므로, todos 모듈에 적용하겠습니다.

 

modules/todos.js

const todos = handleActions(
    {
        [CHANGE_INPUT]: (state, { payload: input }) =>
            produce(state, draft => {
                draft.input = input;
            }),
        [INSERT]: (state, { payload: todo }) =>
            produce(state, draft => {
                draft.todos.push(todo);
            }),
        [TOGGLE]: (state, { payload: id }) =>
            produce(state, draft => {
                const todo = draft.todos.find(todo => todo.id === id);
                todo.done = !todo.done;
            }),
        [REMOVE]: (state, { payload: id }) => 
            produce(state, draft => {
                const index = draft.todos.findIndex(todo => todo.id === id);
                draft.todos.splice(index, 1);
            })
    },
    initialState
)

immer 사용 시 모든 업데이트 함수에 immer를 사용할 필요는 없고, 일반 자바스크립트로 처리하는 것이 더 간편할 때는 사용하지 않아도 됩니다.