간단한 웹 페이지 기능 소개
쇼핑몰 기능 중 하나인 장바구니 기능을 구현한 웹 페이지입니다.
장바구니에 등록된 상품 중에서 결제할 상품을 선택하고, 상품 갯수에 따라 최종 금액을 유저에게 보여줍니다.
리액트 컴포넌트 설명
컴포넌트 2개가 있습니다.
1. 상품 컴포넌트 : 장바구니 상품 정보를 보여주는 컴포넌트
2. 상품 리스트 컴포넌트 : 상품 컴포넌트들을 보여주고, 필요한 비즈니스 로직을 제공합니다.
2번 컴포넌트는 컨테이너 - 프레젠테이션 패턴의 컨테이너라고도 볼 수 있을 텐데요.
컨테이너를 별도로 구별하려면, ~ 컨테이너라는 코드컨벤션이 생기고 별도의 디렉토리 폴더를 관리해줘야 하는데
개인적인 생각으로는 코드의 양이 늘어나고, 비즈니스 로직을 가지고 있는 컴포넌트들을 컨테이너로 엄격하게 구분하기 어렵다는 생각을 가지고 있습니다.
위의 이유로 비즈니스 로직의 유무에 상관없이 컴포넌트들을 한 곳에 정리하고 있습니다.
하단의 결제 금액을 보여주는 UI는 설명에서 제외하겠습니다.
1. 상품 컴포넌트 (CartProductItem)
function CartProductItem({
productItem,
onIncrease,
onDecrease,
onCheck,
}: CartProductItemProps) {
const { detailImageUrl, itemName, price, count, itemNo, isPurchase } = productItem;
return (
<StyledCartProductItem>
<CartProductImage>
<img src={detailImageUrl} alt={itemName} />
</CartProductImage>
<CartProductInfo>
<div>{itemName}</div>
<div>{price.toLocaleString()}원</div>
<div>
<button onClick={() => onIncrease(productItem)}>+</button>
<span>{count}</span>
<button onClick={() => onDecrease(productItem)}>-</button>
</div>
<div>
<input
id={`purchase-${itemNo}`}
type="checkbox"
checked={isPurchase}
onChange={() => onCheck(productItem)}
/>
<label htmlFor={`purchase-${itemNo}`}>결제할 상품에 포함하기</label>
</div>
</CartProductInfo>
</StyledCartProductItem>
);
}
상품 컴포넌트는 상품 데이터와 핸들러 함수들을 전달받습니다.
UI에서는 위와 같습니다.
2. 상품 리스트 컴포넌트 (CartProductItems)
export default function CartProductItems() {
const { cartProductItems, handleIncrease, handleDecrease, handleCheck } = useCartProductItems();
return (
<StyledCartProductItems>
{cartProductItems.map((productItem) => (
<CartProductItem
key={productItem.itemNo}
productItem={productItem}
onIncrease={handleIncrease}
onDecrease={handleDecrease}
onCheck={handleCheck}
/>
))}
</StyledCartProductItems>
);
}
상품 리스트 데이터를 받아서 상품 컴포넌트들을 렌더링해줍니다.
상품수량 조정, 결제상품에 포함(handleCheck) 하는 비즈니스 로직도 제공합니다.
문제 발생 - 리렌더링이 될 필요 없는 상품 컴포넌트가 리렌더링된다.
리액트 최적화는 리렌더링과 관련이 있습니다.
State나 Props가 변경되면 리액트 컴포넌트는 리렌더링을 합니다.
리액트 컴포넌트는 주로 함수로 구현을 하는데 내부의 변수, 함수 등이 다시 실행되는 것입니다.
리액트 DevTools를 이용하면 리렌더링이 얼마나 자주 일어나는 지 육안으로 확인할 수 있습니다.
상품수량을 수정하거나, 결제상품에 포함/취소할 때 상품리스트 데이터를 수정해야 합니다.
상품리스트 데이터는 State로 구현이 되어 있고, 상품리스트 컴포넌트에서 구독하고 있는데요.
State가 바뀌면 상품리스트 컴포넌트가 리렌더링됩니다.
그러면, 상품리스트 내부의 비즈니스 로직 (데이터, 핸들러 함수) 들도 재생성됩니다.
상품 컴포넌트의 Props로 전달하는 데이터, 핸들러 함수들이 바뀌기 때문에, 상품 컴포넌트가 리렌더링되는 것입니다.
문제는 리렌더링될 필요가 없는 상품 컴포넌트들도 다시 리렌더링된다는 점입니다.
특정 상품의 정보가 바뀌면 해당 상품만 리렌더링이 발생하면 되는데, 다른 상품들도 리렌더링되고 있습니다.
첫번째 상품수량을 증가시키면, 첫번째 상품만 리렌더링되면 되는데, 두번째, 세번째 상품도 리렌더링 된다는 것이죠.
1. React.memo 적용하기
memo를 적용하면 props가 변경이 되지 않는 이상, 예전에 렌더링된 컴포넌트(메모이제이션된 컴포넌트)가 반환됩니다.
불필요한 리렌더링을 막을 수 있습니다.
memo에서는 이전 props와 현재 props를 비교하는 데요.
기본적으로 리액트에서 제공하는 shallowEqual 함수를 사용합니다.
shallowEqual 함수는 두 props가 원시 타입일 때, 값이 같으면 true를 반환하고
두 props가 객체일 때는 한 depth의 동일한 key-value를 가지고 있으면 true를 반환합니다.
그 밖의 경우에는 모두 false를 처리합니다.
const valA = 10;
const valB = 10;
const objA = { a: 1, b: 2 };
const objB = { a: 1, b: 2 };
const objC = { a: 1, b: { c: 2 } };
const objD = { a: 1, b: { c: 2 } };
// test
shallowEqual(valA, valB) // true
shallowEqual(objA, objB) // true
shallowEqual(objC, objD) // false
// 상품 컴포넌트 (CartProductItem)
...
export default React.memo(CartProductItem)
위와 같이 memo를 적용하면 되는데요.
현재로써는 이 상태를 적용해도, 상품정보가 바뀌지 않은 상품 컴포넌트들이 아직도 리렌더링되는 문제가 발생합니다.
상품 컴포넌트는 props로 productItem, onIncrease, onDecrease, onCheck
를 전달받고 있는데요.
productItem은 객체 타입을 가지고 있고, 나머지는 함수 타입입니다.
shallowEqual 함수는 함수의 내부 로직이 같아도 함수의 레퍼런스가 다르면 false 처리를 처리하기 때문입니다.
2. useCallback 적용하기
// 상품리스트 컴포넌트
export default function CartProductItems() {
const { cartProductItems, handleIncrease, handleDecrease, handleCheck } = useCartProductItems();
return (
<StyledCartProductItems>
{cartProductItems.map((productItem) => (
<CartProductItem
key={productItem.itemNo}
productItem={productItem}
onIncrease={handleIncrease}
onDecrease={handleDecrease}
onCheck={handleCheck}
/>
))}
</StyledCartProductItems>
);
}
상품리스트 State (cartProductItems
)가 업데이트 되더라도 핸들러 함수는 동일한 함수, 즉 메모리에서 같은 레퍼런스를 가리키고 있어야 합니다.
useCallback
을 사용하면 메모이제이션된 함수를 유지할 수 있습니다.
// useCartProductItems hook 내부
const handleIncrease = useCallback((productItem) => {
... 로직
}, [])
const handleDecrease = useCallback((productItem) => {
... 로직
}, []);
const handleCheck = useCallback((productItem) => {
... 로직
}, []);
최적화 적용 후, 결과
상품 정보가 바뀌는 상품만 렌더링되는 것을 확인할 수 있습니다..!!
참고
'React' 카테고리의 다른 글
[React] 컴포넌트를 어떻게 잘 구현할 수 있을까? (0) | 2023.07.29 |
---|---|
[React] React-Query 기본 사용법 정리 (useQuery, useMutation) (0) | 2023.01.20 |
[React] React-Query 상태(Status) & StaleTime, CacheTime 정리 (0) | 2023.01.18 |
[React] React-Query를 이용해서 페이지네이션 구현하기 (0) | 2022.11.27 |
[React] React + Swiper.js를 이용해서 카드 UI를 인라인 형식으로 보여주기 (0) | 2022.07.29 |