[Redux-toolkit] Redux 3원칙으로 알아보는 Immer draft

Yunhye Park·2024년 4월 5일
0
post-thumbnail

상황

redux-toolkit으로 슬라이스를 만들고, dispatch로 initialState가 배열인 리덕스 값의 일부만 업데이트 하려는 중.

Cart.js

import { Button, Table } from 'react-bootstrap';
import { useDispatch, useSelector } from 'react-redux';
import { downCount, upCount } from '../module/store';

export default function Cart() {
  const stock = useSelector((state) => state.stock);
  console.log(stock);
  const dispatch = useDispatch();

  return (
    <Table striped>
      <thead>
        <tr>
          <th>상품번호</th>
          <th>상품명</th>
          <th>수량</th>
          <th>변경하기</th>
        </tr>
      </thead>
      <tbody>
        {stock.map((stock) => (
          <tr key={stock.id + 'cartT'}>
            <td>{stock.id}</td>
            <td>{stock.name}</td>
            <td>{stock.count}</td>
            <td>
              <Button
                type="button"
                variant="info"
                size="small"
                style={{ marginRight: '10px' }}
                onClick={dispatch(upCount())} {/* <--- 이 부분 /*}
              >
                +
              </Button>
              ...

store.js

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

const stock = createSlice({
  name: 'stock',
  initialState: [
    { id: 0, name: 'White and Black', count: 2 },
    { id: 2, name: 'Grey Yordan', count: 1 },
  ],
  reducers: {
    upCount(state) {
      return state.count;
    },
  },
});

export const { upCount } = stock.actions; // 액션으로 export

export default configureStore({
  reducer: {
    stock: stock.reducer, // 리듀서 생성
  },
});

Error Messages

1. 데이터 타입을 제대로 확인하자

Expected onClick listener to be a function, instead got a value of object type.

dispatch(action함수)에서 action함수가 object 타입이라서 발생한 에러다.

💡 initialState가 배열 내 객체다. 그래서 인덱싱을 먼저 해준 후에 key로 접근해야 한다.

컴포넌트에서 파라미터에 인덱스를 넘겨주고, 아래처럼 슬라이스를 수정했다.

const stock = createSlice({
  name: 'stock',
  initialState: [
    { id: 0, name: 'White and Black', count: 2 },
    { id: 2, name: 'Grey Yordan', count: 1 },
  ],
  reducers: {
    upCount(state, index) {
      return state[index].count + 1; // <-- Error!
    },

2. 직접 state에 접근할 수는 없다?

Cannot read properties of undefined (reading 'count')

파라미터로 넣어도 읽히지 않고, 개별 변수로 사용해야만 쓰인다.

💡 아마 이런 방식으로 쓰는 게 아닌 듯하다. 공식문서를 참고해보자.

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
}

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

initialState가 객체일 땐 그냥 점 접근법으로 하면 된다. 사용할 때도 dispatch(액션함수()) 이렇게 쓰면 되고.

💡 그럼 배열이 initialState일 때, 다른 말로 하자면 컴포넌트에서 dispatch를 통해 데이터를 파라미터로 넘겨줄 때 redux에서 어떻게 받아올지가 관건이다.

위 예시에서 보이듯 action을 인자로, action.payload에 데이터가 담긴다.

  reducers: {
    upCount(state, action) {
      return (state[action.payload].count += 1);
    },

하지만 이렇게 해도 에러는 발생하는데...

3. Redux의 3원칙

[Immer] An immer producer returned a new value and modified its draft. Either return a new value or modify the draft.

2번과 연결되는 이야기다. Redux-toolkit에서 객체 타입을 저장하면, return 하지 않고 직접 값을 변경할 수 있다.
ex) state.name = 'park'

그래서 action 함수를 통해서도 state에 직접 접근하려고 했던 건데, 엄밀히 말하자면 createSlice 함수로 상태를 생성하면 리덕스에 내장된 Immer.js에서 draft를 만들어 이를 통해 값을 관리한다.

개발자가 draft를 업데이트 하면, Immer에서 이러한 변경 사항을 추적하여 새로운 state를 만들고 이를 store에 반영한다. 이는 Redux 3원칙 중 하나인 '기존 state는 불변성 유지'를 위함이다.

Redux 3원칙

  1. 어플리케이션 state는 모두 store에 객체 트리로 관리된다.
  2. 상태는 불변(Read-only)한다.
    ➡️ action을 통해서만 상태를 변경할 수 있다.
  3. reducer는 순수 함수여야 한다.
    ➡️ 이전 state와 action을 파라미터로 받고, 이를 통해 새로운 state 객체를 반환하는 Reducer는 인자가 같다면 결과값도 언제나 동일해야 한다.

무한 리렌더링 주의

그리고 마지막 에러. 이벤트 핸들러에 파라미터 있는 함수를 두려면 콜백 함수로 작성해야 한다. 함수명에 ()를 붙이는 건 '실행'의 의미이기 때문이다.


참고

2. 리덕스의 3가지 규칙

profile
일단 해보는 편

0개의 댓글