Redux-Toolkit. reducer의 반환값

1

상태관리

목록 보기
1/1
post-thumbnail

혹시나 잘못된 개념 전달이 있다면 댓글 부탁드립니다. 저의 성장의 도움이 됩니다.

Immer란?

Immer (German for: always) is a tiny package that allows you to work with immutable state in a more convenient way.

한마디로 개발자가 편리하게 state를 immutable(원본을 유지한 상태)하게 사용할 수 있도록 도와주는 패키지이다.
Immer 덕분에 RTK(Redux-toolkit)에서 mutable하게 state를 변경해도 내부에서 immutable하게 유지된다.

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    todoAdded(state, action) {
      state.push(action.payload)  // mutable한 표현식이지만 immer 덕분에 immutable한 방법으로 변경됨
    },
  },
})

RTK에서 state 변경하는 2가지 방법

Immer expects that you will either mutate the existing state, or construct a new state value yourself and return it, but not both in the same function!

  • return 적용 X (A 방식)
    : 반환값 없이 mutable한 표현식으로 state 갱신
    -> immer 덕분에 가능

  • return 적용 (B 방식)
    : state에 재할당할 새로운 값을 반환

const todosSlice = createSlice({
  name: 'todos',
  initialState: [],
  reducers: {
    todoAdded(state, action) {
      // A 방식 : "Mutate" the existing state, no return value needed
      state.push(action.payload)
    },
    todoDeleted(state, action.payload) {
      // B 방식: Construct a new result array immutably and return it
      return state.filter(todo => todo.id !== action.payload)
    }
  }
})

화살표 함수에서 {}를 생략하는 경우 주의!

화살표 함수에서 ()=>{return A}()=>A 와 같은 의미이므로, state 갱신할 때 주의하도록 한다!

state = newValue 주의!

A common mistake is to try assigning state = someValue directly. This will not work! This only points the local state variable to a different reference. That is neither mutating the existing state object/array in memory, nor returning an entirely new value, so Immer does not make any actual changes.

즉, 새로운 값으로 state를 업데이트하려면 return 표현식 을 사용해야한다. 이 때, 표현식은 하나의 값으로 평가될 수 있는 것이다. return state = someValue 이 아니다.

const initialState = []
const todosSlice = createSlice({
  name: 'todos',
  initialState,
  reducers: {
    brokenTodosLoadedReducer(state, action) {
      // ❌ ERROR: does not actually mutate or return anything new!
      state = action.payload
    },
    fixedTodosLoadedReducer(state, action) {
      // ✅ CORRECT: returns a new value to replace the old one
      return action.payload
    },
    correctResetTodosReducer(state, action) {
      // ✅ CORRECT: returns a new value to replace the old one
      return initialState
    },
  },
})

참조 자료

공식문서: Writing Reducers with Immer
스택오버플로우 : What exactly should you return from a redux slice reducer?


TIL

pre-project 스택오버플로우 클론 프로젝트할 때, redux-toolkit을 사용하고 mmz에 적용하려니 이 부분이 헷갈려서 다시 찾아봤다. 공식문서를 보니 찜찜했던 부분이 해결되었다.

  • reducer의 재사용 방법

  • state 구조상 return으로 재할당 할 수 없는 경우
    : state.inputTexts[keyValue] = state.inputTexts[keyValue].filter()로 구현은 했는데, 반복되는 부분이 있어서 의심이 되었다. 하지만 공식문서에서 이 방법으로 하는게 맞다고 한다.

    // 재사용을 위해 함수를 따로 분리 가능
    const filterByIndex = (state: any, action: any) => {
      const { keyValue, indexValue } = action.payload;
      state.inputTexts[keyValue] = state.inputTexts[keyValue].filter(
        (el: TypeOfInputSectionsWithRemoveButton) => el.index !== indexValue
      );
    };
    
    const recipeSlice = createSlice({
      name: "recipe",
      initialState: initialForm,
      reducers: {
        ...
        removeIngredientsInputSection: filterByIndex, 
        removeStepsInputSection: filterByIndex,
      },
    });
// slice 선언
const recipeSlice = createSlice({
  name: "recipe",
  initialState: initialForm,
  reducers: {
    setTitleOrBody: (state, action) => {
      state.inputTexts = { ...state.inputTexts, ...action.payload };
    },
    setCategory: (state, action) => {
      state.inputTexts.category = action.payload;
    },
    addIngredientInputSection: (state, action) => {
      state.inputTexts.ingredients.push(action.payload);
    },
    removeInputSection: (state: any, action: any) => {
      const { keyValue, indexValue } = action.payload;
      state.inputTexts[keyValue] = state.inputTexts[keyValue].filter(
        (el: TypeOfInputSectionsWithRemoveButton) => el.index !== indexValue
      );
    },
  },
});


// 재료 입력 관련 컴포넌트 내부의 이벤트 함수
  const removeHandler = () => {
    dispatch(
      recipeActions.removeInputSection({
        keyValue: "ingredients",
        indexValue: idx,
      })
    );
  };

배운걸 써보려고, 따로 분리했는데 action.payload 에 객체를 전달해서 reducer를 따로 분리할 필요가 없어보였다. 한번더 고치기... 그런데 any타입이 거슬린다;; 이벤트 함수 이름은 컴포넌트 내부 스코프에서만 사용되어서 변경 안했는데, 이것도 변경하는게 좋은건지 의문이 든다.
요즘 리펙토링하고, 클린코드에 대한 열망(?)이 있어서 계속 더 좋은 방법이 없나 고민하게 된다. 리펙토링 하면서 변수 이름도 변경했다. 시간없다고 대충쓰면 미래의 내가 고생한다는걸 다시 느꼈다.😅

0개의 댓글