@reduxjs/toolkit

김동현·2022년 3월 14일
0

Redux

목록 보기
4/5
post-thumbnail

redux toolkit 필요성

기존 redux만 사용시 애플리케이션의 규모가 점점 커지면 아래와 같은 잠재적인 문제가 발생할 수 있습니다.

  1. 상태 변경 로직을 갖는 reducer의 함수 크기가 점점 커지게 될 것입니다.

  2. Redux store의 상태 객체가 많은 상태를 관리하게 될 것입니다.

  3. action 객체의 type 값에 오타가 생기면 reducer 함수의 실행이 우리의 예상과 달리지게 됩니다.

  4. reducer 함수가 인수로 전달받는 기존 상태 객체를 직접적으로 변경해서는 안되고, 언제나 변경된 상태값을 갖는 새로운 상태 객체를 반환해야 합니다. 실수로라도 기존 상태 객체를 직접 변경하지 않도록 해야 하며 모든 상태, 즉 프로퍼티를 매번 반환값으로 작성해주어야 합니다.

우리는 이러한 문제들을 "@reduxjs/toolkit"이라는 라이브러리를 통해서 해결할 수 있습니다.

@reduxjs/toolkit 패키지를 설치를 위해 터미널에 아래 명령을 입력해줍니다.

npm install @reduxjs/toolkit

createSlice

기존 redux만 사용시 상태가 많아질 수록 상태 객체가 커지고, reducer 함수가 커져 유지보수하기가 어려워지는 문제점이 있었습니다.

createSlice 함수로 생성한 slice 객체는 하나의 상태 객체 내 존재하는 상태값들을 서로 관련있는 상태값들로 구성하여 여러 상태 객체로 분리하여 관리되도록 도와주는 객체입니다.


createSlice 함수 호출할 때 인수로 "slice를 설정하는 객체"를 전달하면서 호출합니다.

createSlice 함수의 인수로 전달되는 객체는 "name" 프로퍼티생성될 slice의 이름을 작성합니다. "initialState" 프로퍼티에는 초기 상태 객체를 작성하고, "reducers" 프로퍼티에는 상태 변경 로직을 메서드로 갖는 객체를 작성합니다.

  • name : slice의 이름. 추후에 생성될 action 객체의 type 프로퍼티값의 prefix가 된다.

  • initialState : 초기 상태 객체를 작성합니다.

  • reducers : 상태를 변경하는 메서드를 갖는 객체를 작성합니다. 각 메서드는 기존 상태 객체action 객체를 인수로 전달받습니다.

import { createSlice } from '@reduxjs/toolkit';

const counterSlice = createSlice({
    // slice 이름, action.type의 prefix로 설정됨
    name: 'counter',
    
    // 초기 상태 객체
    initialState: { counter: 0, showCounter: true },
    
    // 상태 변경 메서드를 갖는 객체
    reducers: { 
        // 각 메서드는 인수로 기존 상태 객체와 action 객체 전달받음
        increment(stateObj, action) {
            stateObj.counter++;
        },
        decrement(stateObj, action) {
            stateObj.counter--;
        },
        toggleCounter(stateObj, action) {
            stateObj.showCounter = !stateObj.showCounter;
        }
    }
});

createSlice 함수 호출시 반환되는 slice 객체는 reducer 프로퍼티actions 프로퍼티를 갖는 객체입니다.

  1. "slice.reducer"
    slice가 관리하는 상태 변경 로직을 하나의 함수로 갖고 있다.
    이는 reducers 객체의 각 메서드들을 하나의 함수로 갖는 reducer 함수가 바인딩되어 있다.

  2. "slice.actions"
    객체가 바인딩되어 있으며, 객체에는 reducers 객체의 메서드 이름과 동일한 메서드를 갖고 있다.
    actions 객체의 메서드 호출시 reducers 객체의 메서드에 매칭되는 action 객체가 반환되며, 해당 action 객체를 dispatch 하면 매칭되는 reducers 객체의 메서드가 실행되어 상태값을 변경합니다.

slice.reducer

const counterSlice= createSlice({
    name: 'counter',
    initialState: { counter: 0, showCounter: true },
    reducers: {
        increment(stateObj, action) {
            stateObj.counter++;
        },
        decrement(stateObj, action) {
            stateObj.counter--;
        },
        toggleCounter(stateObj, action) {
            stateObj.showCounter = !stateObj.showCounter;
        }
    }
});

// reducers 객체의 메서드들을 하나로 통합한 함수를 slice.reducer에 바인딩
counterSlice.reducers;

slice 객체의 reducer 프로퍼티에는 slice가 관리하는 상태 변경 로직을 갖는 함수, 즉 reducer 함수가 바인딩되어 있습니다. 이는 reducers 객체의 각 메서드들을 하나의 함수로 합친 것입니다.

다시 말해 reducers 객체의 각 메서드들은 기존 reducer 함수의 상태 변경 로직을 메서드로 분리한 것입니다.

이때 reducers 객체의 메서드가 기존 상태 객체를 직접 변경하는 로직 작성이 가능한 이유는 내부적으로 "immer 패키지"를 사용하고 있기 때문에 작성이 가능합니다. immer 패키지에 대해서는 뒤에서 설명하도록 하겠습니다.

slice.actions

slice 객체의 actions 프로퍼티에는 객체가 바인딩되어 있으며, 바인딩되어 있는 객체에는 reducers 객체의 메서드와 동일한 이름의 메서드들이 존재합니다.

actions 객체의 메서드 호출시 action 객체가 반환되므로 직접 action 객체를 생성하지 않고 메서드 호출시 반화된 action 객체를 dispatch 하여 사용합니다.

이때 action 객체를 dispatch 하면 동일한 이름을 갖는 reducers 객체의 메서드가 실행되어 상태값을 변경하게 됩니다.

즉, actions 객체의 메서드 이름과 동일한 이름의 reducers 객체의 메서드가 실행되어 상태를 변경하게 됩니다.


import { createSlice } from '@reduxjs/toolkit';

const counterSlice= createSlice({
    name: 'counter',
    
    initialState: { counter: 0, showCounter: true },
    
    reducers: {
        increment(stateObj, action) {
            stateObj.counter++;
        },
        decrement(stateObj, action) {
            stateObj.counter--;
        },
        toggleCounter(stateObj, action) {
            stateObj.showCounter = !stateObj.showCounter;
        }
    }
});

// reducers 객체의 메서드와 동일한 이름의 메서드를 갖는 객체
// reducers 객체의 메서드와 slice.actions 객체의 메서드가 서로 1 : 1 매칭
counterSlice.actions;// -> { increment: f, decrement: f, toggleCounter: f}

위 코드의 counterSlice.actions 객체에는 increment, decrement, toggleCounter 메서드가 존재하며 이는 reducers 객체의 메서드와 1 : 1 매칭되도록 갖고 있는 것을 확인할 수 있습니다.

countSlice.actions 객체의 메서드 호출시 action 객체가 반환되며, 반환된 action 객체를 dispatch시 매칭되는 reducers 객체의 메서드가 실행됩니다.

더이상 action.type 오탈자에 대해서 신경쓰지 않아도 되며, action 객체를 직접 만들지 않아도 됩니다. 단지 slice.actions 객체에 존재하는 메서드를 실행하여 action 객체를 생성하고 dispatch해주기만 하면 됩니다.

// reducers의 increment 메서드를 실행하는 
// action 생성자 함수를 호출하여 action 객체 생성
const incrementAction = counterSlice.actions.increment();

// 생성된 action 객체를 dispatch하면 action과 대응되는 increment 메서드가 호출
dispatch(incrementAction);

action.payload

action 객체에 상태 업데이트를 위한 추가적인 정보(payload)는 actions 객체의 메서드를 호출할 때 "인수로 전달"해줍니다.

이때 인수로 전달한 값은 생성된 "action 객체의 payload 프로퍼티"에 저장되어 전달됩니다.

// action 생성자 함수 호출시 
// 인수로 전달한 값은 생성될 action 객체의 payload 프로퍼티에 존재
const incrementAction = counterActions.increment({ amount: 5 });

incrementAction.payload;  // -> { amount: 5 };

dispatch(incrementActions);

actions 객체의 메서드에 인수로 전달한 값은 action 객체의 payload 라는 프로퍼티에 저장됩니다.
payload라는 프로퍼티 이름은 우리가 정하는 것이 아닌 redux toolkit에서 사용하는 기본값입니다.

immer 패키지

reducers 객체의 메서드들은 인수로 기존 상태 객체와 action 객체를 전달받습니다. 이때 메서드들은 인수로 전달받은 기존 상태 객체를 직접 변경하는 방식으로 상태를 변경할 수 있습니다.

reducers: {
    increment(stateObj, action) {
        ++stateObj.count;
    },
    decrement(stateObj, action) {
        --stateObj.count;
    },
    toggleCounter(stateObj, action) {
        stateObj.showCounter = !stateObj.showCounter;
    }
}

이러한 로직이 허용되는 이유는 redux toolkit 내부적으로 "immer이라는 패키지"를 사용하고 있기 때문입니다.

immer은 기존 상태 객체를 변경하는 로직변경된 상태값을 갖는 새로운 객체를 반환하는 로직으로 자동적으로 변환해주는 역할을 합니다.

immer을 통해 직접 기존 상태를 복사할 필요가 없어졌으며, 상태 불변성에 대해서도 신경 쓸 필요가 없어졌습니다.

즉, 상태를 직접 변경하도록 작성을 해도 내부적으로는 알아서 기존 상태를 변경하지 않는 로직으로 변환되어 실행됩니다.

configureStore

만약 여러 slice를 생성하게되면 reducer 함수도 여러 개 생성될 것입니다. 하지만 기존 createStore 함수의 인수로는 하나의 reducer 함수를 인수로 전달받기 때문에 우리는 여러 reducer 함수를 하나로 합쳐 store 객체를 생성하기 위해서 @reduxjs/tooklit 패키지의 "configureStore" 함수로 store 객체를 생성합니다.

configureStore는 store를 생성하는 함수로 여러 reducer 함수를 하나의 reducer 함수로 합쳐주는 기능이 포함되어 있습니다.

configureStore 함수의 인수로 객체를 전달합니다. 이때 인수로 전달하는 객체는 reducer라는 프로퍼티를 갖고 있고 이 reducer 프로퍼티의 값으로 reducer 함수가 여러 개인 경우에는 객체를 전달하고, 하나인 경우에는 reducer 함수를 바로 작성해줍니다.

import { configureStore } from '@reduxjs/toolkit';

// 1. reducer 함수가 하나인 경우
const store = configureStore({
    reducer: slice.reducer
});

2. reducer 함수가 여러 개인 경우
const store = configureStore({
    reducer: {
        reducer식별자: slice.reducer,
        reducer식별자: slice.reducer,
        ,,,
    }
});

만약 여러 slice를 사용하게 된다면 configureStore 함수의 인수로 전달하는 객체의 reudcer 객체에 프로퍼티 키로 "각 reudcer 함수를 식별할 수 있는 값"을 작성하고 프로퍼티 값으로는 "reducer 함수"를 작성합니다.

이때 작성한 reducer 함수 식별자를 통해서 특정 slice가 관리하는 상태 객체에 접근할 수 있습니다.

즉, useSelector 훅으로 특정 상태값을 추출할 때 아래와 같이 추출해야 합니다.

useSelector(stateObj => stateObj.reducer식별자.상태이름);
profile
Frontend Dev

0개의 댓글