컴포넌트는 간략하게 제 기능만 수행하는 게 제일 좋다고 생각한다. 근데 한 컴포넌트 내에서 너무 많은 상태를 관리하다보면 이 컴포넌트를 작성자가 아닌 사람이 봤을 때 좋은 컴포넌트라고 생각할까? (즉, 가독성이 똥망)
어쩔 수 없이 만들어진 자이언트 컴포넌트를 코드를 분리하는 방법, 가독성을 높이는 방법을 함께 찾아보자!
상태를 관리하기 위해서는 useState, useReducer가 있고, 전역으로 관리하고 싶다면 상태관리 라이브러리나 useContext를 사용하면 된다. 전역적으로 상태를 관리하면 코드의 의존성이 많아져서 불필요한 리렌더링을 야기할 수 있기 때문에 전역 상태 관리는 모든 곳에서 쓰이는 게 아니라면 웬만해서 줄이는 게 베스트라고 생각한다. 그래서 리액트에서 제공하는 훅 중에 하나인 useReducer를 사용해보자!
상태 관리 코드를 컴포넌트 외부에서 관리하기 위해 쓰는 훅, useReducer
첫 번째 관문인 useReducer의 구성요소를 아는 게 중요하다
- state
- dispatch
- reducer
- initialState
state는 상태를 담는 곳이며, dispatch는 상태 변화를 알려주는 발송기 (디스패치), reducer는 실제로 상태를 변환해주는 변환기, initialState는 상태의 초깃값을 의미한다.
한 블로그에서 재미난 예시를 들면서 설명했는데 인용하자면,
- state : 상태 이름 (컴포넌트에서 사용할 상태) > 빵(재료) 담는 접시
- dispatch : 상태(state)를 변경 시 필요한 정보를 전달하는 '함수' > 주문서
- reducer : dispatch를 확인해서 state를 변경해 주는 '함수' > 주방(공장)
- initialState : state에 전달할 초기 값 > 빵(및 재료 등) 개수 설정
import React, { useReducer } from 'react';
function reducer(state, action) {
switch (action.type) {
case 'PLUS':
return state + 1;
case 'MINUS':
return state - 1;
default:
return state;
}
}
function Baking() {
// 3을 value 저장
// 위에 선언했던 값을 변경하는 reducer 함수를 넣어주기!
// reducer속 로직들을 실행시킬 명령어가 담겨있는 dispatch 선언
const [value, dispatch] = useReducer(reducer, 3);
const onPlus = () => {
dispatch({ type: 'PLUS' });
};
const onMinus = () => {
dispatch({ type: 'MINUS' });
};
return (
<div>
<h1>{number}</h1>
<button onClick={onPlus}>+1</button>
<button onClick={onMinus}>-1</button>
</div>
);
}
export default Baking;
각 구성요소의 역할을 레스토랑처럼 생각하고 나니깐 손님들이 주문하는 곳이 컴포넌트 안과 같고, 음식을 만드는 곳이 컴포넌트 밖이라고 생각하니 useReducer를 왜 쓰는지(상태를 다루는 코드를 분리) 알 것 같다.
주문서인 dispatch가 reducer에게 음식을 만들어달라는 게 보인다. 그리고 dispatch 안에 들어가는 객체를 액션 객체라고 한다! 그래서 reducer 인자에서 action가 들어가는 걸 자연스럽게 이해할 수 있다. 액션에 있는 타입에 따라 각각 다른 조리방법을 사용한다고 생각하니 재밌다...
충분히 위 내용을 이해하셨더라면 [개선해보기]로 넘어가셔도 됩니다.
간단한 예제인 카운터를 만들어보자!
import { useReducer } from "react";
const counterReducer = (state, action) => {
switch (action.type) {
case "PLUS" : return state + 1
case "MINUS" : return state - 1
}
};
export const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, 1);
const onPlus = () => {
dispatch({ type: "PLUS" });
};
const onMinus = () => {
dispatch({ type: "MINUS" });
};
return (
<>
<div>{state}</div>
<button onClick={onPlus}>+</button>
<button onClick={onMinus}>-</button>
</>
);
};
적당한 예시를 찾다가 발견한 코드! 참고 링크에 있는 코드를 한번 개선해보자!
// 📃 App.jsx
function App() {
// ...생략
const [cartItems, setCartItems] = useState([]);
const [totalPrice, setTotalPrice] = useState(0);
// 📌 장바구니 안에서 + 버튼을 누르면 수량을 1 증가시키는 함수
const onAdd = id => {
const updatedArr = cartItems.map((cur) => {
if (cur.id === id) {
cur.amount++;
}
return cur;
});
setCartItems(updatedArr);
const newTotalPrice = updatedArr.reduce((acc, cur) => {
return acc + (cur.amount * cur.price);
}, 0);
// 📌 장바구니 안에서 - 버튼을 누르면 수량을 1 감소시키는 함수
const onRemove = id => {
const updatedArr = cartItems.map((cur) => {
if (cur.amount > 0 && cur.id === id) {
cur.amount--;
} else if (cur.amount === 0 && cur.id === id) {
cur.amount;
}
return cur;
});
const newTotalPrice = updatedArr.reduce((acc, cur) => {
return acc + (cur.amount * cur.price);
}, 0);
setTotalPrice(newTotalPrice);
const removedArr = updatedArr.filter((cur) => {
return cur.amount > 0;
})
setCartItems(removedArr);
}
// 📌 `+ 담기` 버튼을 누르면 장바구니에 선택한 아이템을 담아주는 함수
const onSave = selectedItemData => {
const newItemData = cartState.items.concat(selectedItemData);
const mergedItemData = newItemData.reduce((acc, cur) => {
const found = acc.find((item) => item.id === cur.id);
if (found) {
found.amount += Number(cur.amount);
} else {
acc.push({...cur});
}
return acc;
}, []);
setCartItems(mergedItemData);
newItemData.forEach(item =>{
setTotalPrice(totalPrice + item.amount * item.price);
})
};
}
export default App;
여러 개의 state를 한 곳으로 몰아주자! 그리고 useReducer를 구성 요소에 맞게 쓰자.
const initialState = {
cartItems: [],
totalPrice: 0
}
const cartReducer = () => {
}
export function Order() {
// const [cartItems, setCartItems] = useState([]);
// const [totalPrice, setTotalPrice] = useState(0);
const [state, dispatch] = useReducer(cartReducer, initialState)
...
reducer에 쓸 수 있는 로직들을 옮겨보자
const cartReducer = (state, action) => {
switch (action.type) {
case "ADD": {
const updatedArr = state.items.map((cur) => {
if (cur.id === action.id) {
return {
...cur,
amount: cur.amount + 1,
};
// cur.amount++;
}
return cur;
});
const newTotalPrice = updatedArr.reduce(
(acc, cur) => acc + cur.amount * cur.price,
0
);
return {
items: updatedArr,
totalPrice: newTotalPrice,
};
}
case "REMOVE": {
...
}
case "SAVE": {
...
}
}
};
export function Order() {
...
}
reducer에 전달될 수 있게 dispatch 코드를 작성하자
export function Order() {
const [state, dispatch] = useReducer(cartReducer, initialState);
...
const handleAddItem = id => {
dispatch({
type: 'added',
id: id,
})
}
...
}
[주문앱 7탄] reducer로 리팩토링하기
[React] - useReducer()란 간단하고 쉽게 이해하기(예제코드, useReducer 사용예제)