티스토리 뷰

다음 렌더링 전에 동일한 상태 변수를 여러 번 업데이트해야 하는 경우가 있을 수 있다.

 

이런 경우 다음 상태 값을 전달하는 대신 다음 상태 값을 기반으로 새로운 상태 값을 계산하도록 할 수 있다.

이는 React에게 상태 값을 단순히 교체하는 대신 “이전 상태”를 기반으로 무언가를 수행하도록 지시하는 방법이다.

 

예를 들어 장바구니의 담겨있는 동일 상품을 하나가 아닌 여러 개를 사고 싶어 + 버튼을 통해 이전 수량에서 1씩 추가를 해야 한다고 가정을 해보자.

 

상품 데이터가 있는 json 파일을 먼저 만들어준다.

 

[product.json]

{
  "id": "nf-uni-padding",
  "imageSrc": "NFPadding.jpg",
  "imageAlt": "검정색 노스페이스 남여공용 패딩",
  "name": "노스페이스 패딩",
  "birthday": "2022-1-21"
}

 

해당 노스페이스 패딩 제품이 장바구니에 담겨있다고 가정하고 수량을 추가해보는 부분만 구현해보자.

import { useState } from 'react';
import { getStaticImage } from '../utils'; // 이미지 경로 설정 함수
import productData from '../data/product.json';

ㄴ 상태 관리를 위해 useState 를 불러온다.

ㄴ getStaticImage 유틸 함수는 인수에 파일명을 넣어주면 images 폴더에 저장되어 있는 이미지 중 일치하는 이미지를 가져오는 함수이다.

ㄴ 위에서 만든 상품 정보가 담겨있는 json 파일을 productData 이름으로 가져와서 사용한다.

 

const [product, setProduct] = useState(0);

ㄴ 초깃값은 0으로 설정해주고 state 변수 이름은 product 로 설정한다.

(상품의 개수가 담겨있는 거라 네이밍이 적절치 않지만 예제로 만든거니 대충 넘어가자)

ㄴ 상품 개수를 업데이트 해주는 setter 함수의 이름은 setProduct 로 설정한다.

 

상태 변수를 새로운 값으로 단순히 교체하고자 하는 것이기 때문에 아래와 같이 코드를 작성해준다.

const handleIncreaseProduct = () => {
  setProduct(product + 1);
};

ㄴ 이 핸들러는 상품 수량을 증가시켜주는 핸들러이다.

 

귀찮은 사용자가 있다는 가정을 하고 학습에 대한 이해를 위해 2개씩 늘리는 코드를 작성하였다고 가정해보자.

const handleDoubleIncreaseProduct = () => {
  setProduct(product + 1); // 0 + 1
  setProduct(product + 1); // 0 + 1
};

ㄴ 이 핸들러는 setProduct(product + 1); 를 두 번 호출하고 있다.

ㄴ 만약 해당 함수를 실행할 경우 product 의 값이 2가 아닌 1로 표시된다.

 

이 문제는 새로운 값 업데이트의 주의사항에서 다룬 내용과 비슷한데 setter 함수를 호출해도 이미 실행 중인 코드의 상태 변수가 업데이트 되지 않기 때문이다.

따라서 각 호출은 setProduct(1) 이 되는 것이다.

 

이 문제를 해결하려면 다음 상태 대신 setProduct 에 업데이트 함수를 전달하면 된다.

const handleDoubleIncreaseProduct = () => {
  setProduct((product) => product + 1); // setProduct(0 => 1)
  setProduct((product) => product + 1); // setProduct(1 => 2)
};

ㄴ 해당 코드는 이전 상태 를 기반으로 업데이트를 시도한다. 다시 말해 여러 번 상태를 업데이트 하는 것이다.

ㄴ (product) product + 1 이 함수는 업데이트 함수이다.

 

이 안에서는 보류 상태(pending state)를 가져와서 다음 상태를 계산한다.

React는 업데이트 함수들을 대기열(큐)에 넣는다.

그리고 나서 다음 렌더링 중에 동일한 순서로 호출한다.

  • 처음 (product) product + 1 은 보류 상태로 0을 받고 다음 상태로 1을 반환한다.
  • 그 다음 (product) product + 1 은 보류 상태로 1을 받고 다음 상태로 2를 반환한다.

이 과정으로 대기 중인 다른 업데이트가 없어 React는 결국 현재 상태(current state)로 2를 저장하는 것이다.

 

return (
	<>
    <figcaption>
      <img
        height={250}
        width={250}
        src={getStaticImage(productData.imageSrc)}
        alt={productData.imageAlt}
      />
      <span>수량 {product}</span>
      <button
        type="button"
        aria-label="상품 수량 1 증가"
        onClick={handleIncreaseProduct}
      >
        1개씩 더 담기 +
      </button>
      <button
        type="button"
        aria-label="상품 수량 2 증가"
        onClick={handleDoubleIncreaseProduct}
      >
        2개씩 더 담기 +
      </button>
    </figcaption>
  </>
);

 

마크업의 구조는 위와 같다.

span 요소에는 장바구니에 담긴 상품의 수량이 표시된다.

첫 번째 버튼은 onClick 이벤트를 걸어 handleIncreaseProduct 핸들러를 참조한다.

(상품이 1개씩 증가)

두 번째 버튼에도 마찬가지도 onClick 이벤트를 걸어 handleDoubleIncreaseProduct 핸들러를 참조한다.

(상품이 2개씩 증가)

 

[렌더링 결과]

 

[JSX 파일 전체 코드]

import { useState } from 'react';
import { getStaticImage } from '../utils'; // 이미지 경로 설정 함수
import productData from '../data/product.json';

function Cart() {
    const [product, setProduct] = useState(0);

    const handleIncreaseProduct = () => {
      setProduct(product + 1);
    };
    
    const handleDoubleIncreaseProduct = () => {
      setProduct((product) => product + 1); // setProduct(0 => 1)
      setProduct((product) => product + 1); // setProduct(1 => 2)
    };
    
    return (
        <>
        <figcaption>
          <img
            height={250}
            width={250}
            src={getStaticImage(productData.imageSrc)}
            alt={productData.imageAlt}
          />
          <span>수량 {product}</span>
          <button
            type="button"
            aria-label="상품 수량 1 증가"
            onClick={handleIncreaseProduct}
          >
            1개씩 더 담기 +
          </button>
          <button
            type="button"
            aria-label="상품 수량 2 증가"
            onClick={handleDoubleIncreaseProduct}
          >
            2개씩 더 담기 +
          </button>
        </figcaption>
      </>
    );
}

export default Cart;

 

참고: https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state