이번 주에는 리덕스에 대해서 공부하고 리덕스를 활용해서 이전에 만들었던 쇼핑몰 애플리케이션을 리팩토링하는 과제를 진행했습니다.

우선 이 과제를 하면서 느꼈던 건 리덕스… 이놈 뭔데… 입니다. 너무 어렵더군요. 그래도 어떻게든 이해하고 과제를 하면서 성장해야하는 것이 코드스테이츠 수강생의 숙명 아니겠습니까.


과제를 하면서 맞닥뜨렸던 오류를 남겨놓지 않으면 이후에 어떻게 문제를 해결했는지 분명 까먹을 것 같아서 기록하려고 합니다.


1️⃣ 삭제버튼을 눌렀을 때 Reducer 함수 코드에서의 오류

삭제버튼을 누르면 dispatch(removeFromCart(itemId))가 실행되고 dispatch 객체는 Reducer 함수로 반환한 Action객체를 전달합니다.

Action 객체는 다음과 같습니다.

1
2
3
4
{
type: REMOVE_FROM_CART,
payload: { itemId },
}

Reducer 함수는 action.type에 따라 state를 변경한 새로운 객체를 반환합니다.
state.cartItems에서 인자로 받은 `action.payload.itemId’만 제거한 후 새로운 객체를 반환하면 되겠다라는 생각으로 코드를 구현했습니다.

오류 코드

1
2
3
4
5
case REMOVE_FROM_CART:
const ID = action.payload.itemId;
return Object.assign({}, state, {
cartItems: state.cartItems.filter((item) => item.itemId !== ID);
})

cartItems에 바로 넣으면 반영을 못하는건가…? filter 함수는 새로운 배열을 반환하고 그 배열을 cartItems에 값으로 넣어주겠다는건데 왜 동작이 안되는걸까?🤔


정상동작 코드

1
2
3
4
5
6
7
case REMOVE_FROM_CART:
const ID = action.payload.itemId;
const cartItems = state.cartItems.filter((item) => item.itemId !== ID);

return Object.assign({}, state, {
cartItems: cartItems,
});

여기서 의문점이 든 건 위 코드는 동작하지 않고 아래 코드는 정상적으로 동작한다는 것이었습니다.

이유를 찾기 위해서 제대로 동작하지 않았던 코드를 살려서 프로그램을 실행시켜보니…. 정상적으로 돌아갑니다…

오류 코드를 제대로 기록해놓지 않아 정확히 어디서 오류가 났는지 찾지 못한 나… 오류와 원인을 찾기 위해선 코드와 오류메세지, 그리고 어떤 환경이었는지까지 기록을 해놔야한다는 것을 깨달았습니다.


2️⃣ 상품을 추가한 후 ‘/shoppingcart’ 페이지로 넘어갔을 때 렌더링되지 않는 오류

장바구니에 상품을 담고 장바구니 페이지로 넘어갔을 때 담은 장바구니 상품들이 렌더링되지 않는 오류가 발생했습니다.

state에 값으로 무엇이 들어오는지 확인하기 위해 콘솔을 출력해보니

1
2
3
export default function ShoppingCart() {
const state = useSelector((state) => state.itemReducer);
console.log('shoppingcart가 렌더링되고 나서의 state', state);

cartItems 배열에 처음 들어가있던 상품 3개를 제외하고 1개의 상품을 잘 추가해서 넣은 것 같지만..?



마지막에 넣은 객체의 상태가 이상한 것을 볼 수 있습니다.

우선 객체를 만드는 곳부터 찾아서 하나씩 확인을 했습니다.

1
2
3
4
5
6
7
8
9
export const addToCart = (itemId) => {
return {
type: ADD_TO_CART,
payload: {
itemId,
quantity: 1,
},
};
};

addToCart 함수에서는 문제가 없으니, dispatch 함수가 Reducer 함수에게 Action 객체를 어떻게 보내는지 확인했습니다.

1
2
3
4
5
6
7
8
9
const handleClick = (item) => {
if (!cartItems.map((el) => el.itemId).includes(item.id)) {
//TODO: dispatch 함수를 호출하여 아이템 추가에 대한 액션을 전달하세요.
dispatch(addToCart());
dispatch(notify(`장바구니에 ${item.name}이(가) 추가되었습니다.`));
} else {
dispatch(notify('이미 추가된 상품입니다.'));
}
};

앗..addToCart에 아무것도 넣어주지 않았군요..

addToCart action creator는 itemId를 받아서 객체를 만들기 때문에 addToCart 함수에는 item.id를 넣어줌으로써 오류를 해결했습니다.


3️⃣ 에러는 아니지만 Reducer 함수 코드에 대한 고민

고민을 하게 된 코드는 다음과 같습니다.

1
2
3
4
5
6
7
8
9
case SET_QUANTITY:
let idx = state.cartItems.findIndex(
(el) => el.itemId === action.payload.itemId
);
state.cartItems[idx].quantity = action.payload.quantity;

return Object.assign({}, state, { cartItems: [...state.cartItems] });
default:
return state;

수량버튼을 누를 때마다 수량을 변경하는 코드입니다.
코드는 정상적으로 동작을 합니다만, Reducer 함수의 기본적 원칙을 벗어났다고 생각했습니다.

바로 Reducer의 Immutability(불변성)입니다. Redux의 state 업데이트는 immutable한 방식으로 변경해야 한다는 것인데요. Redux의 장점 중 하나인 변경된 state를 로그로 남기기 위해서 꼭 필요한 작업입니다.

하지만 제가 작성한 코드는 state.cartItems의 수량을 직접적으로 변경시키고 변경된 state를 새로운 객체로 반환합니다. 새로운 객체로 반환은 했지만 state를 직접 건들였기 때문에 immutable하지 못한 함수가 되었습니다.


immutable 하게 작성하기 위해선 cartItems[idx]를 기준으로 slice를 한 후 새로운 배열에 담는 방법이 있습니다.

1
2
3
4
5
6
7
8
case SET_QUANTITY:
let idx = state.cartItems.findIndex(el => el.itemId === action.payload.itemId)
return Object.assign({}, state, {
cartItems: [...state.cartItems.slice(0, idx), action.payload,
...state.cartItems.slice(idx + 1)]
});
```


Review

트러블슈팅이라고 하기엔 너무 거창한 것 같고 오류 해결 일지가 딱 알맞는 단어인 것 같습니다.
기록을 하면서 내가 어디서 잘 못했고, 어떻게 해결했는지 다시 한 번 새겨넣을 수 있는 시간이 된 것 같습니다. 오류를 해결하는 과정을 계속해서 남겨놓으면 스스로에게 굉장히 도움이 될 것 같다는 생각이 들었습니다. :)