혹시나 잘못된 개념 전달이 있다면 댓글 부탁드립니다. 저의 성장의 도움이 됩니다.
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한 방법으로 변경됨
},
},
})
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?
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타입이 거슬린다;; 이벤트 함수 이름은 컴포넌트 내부 스코프에서만 사용되어서 변경 안했는데, 이것도 변경하는게 좋은건지 의문이 든다.
요즘 리펙토링하고, 클린코드에 대한 열망(?)이 있어서 계속 더 좋은 방법이 없나 고민하게 된다. 리펙토링 하면서 변수 이름도 변경했다. 시간없다고 대충쓰면 미래의 내가 고생한다는걸 다시 느꼈다.😅