부트캠프/TIL

Day 49. React Hooks: state를 직접 수정하는 건에 관하여

하이고니 2023. 2. 23. 16:59

 

 

리액트가 상태 변경을 감지하기 위해 만족되어야 하는 조건


 

const [count, setCount] = useState(0);

 

1) 상태 변경 함수가 호출되어야 한다.

 

count = 2;

이런 식의 재할당은 X

 

 

2) 상태 변경 함수에 전달된 인자와 기존의 상태가 달라야 한다.

 

setCount(0) // => X : 0 === 0 true

setCount(1) // => O : 0 === 1 false

 

상태가 참조형 데이터라면? => 참조형 데이터는 자신의 값으로 주소를 담고 있음.

주소가 달라져야 상태 변경을 인지한다.

 

 


 

 

아래는 코드스테이츠에서 제공 받은 'CMarket' 이라는 싱글 페이지 어플리케이션이다.

 

 

path="/"

 

상품리스트 페이지

 

 

 

 

 

path="/shoppingcart"

 

장바구니 페이지 

 

 

 

 

더미 데이터

 

export const initialState =
{
  "items": [
    {
      "id": 1,
      "name": "노른자 분리기",
      "img": "../images/egg.png",
      "price": 9900
    },
    {
      "id": 2,
      "name": "2020년 달력",
      "img": "../images/2020.jpg",
      "price": 12000
    },
    {
      "id": 3,
      "name": "개구리 안대",
      "img": "../images/frog.jpg",
      "price": 2900
    },
    {
      "id": 4,
      "name": "뜯어온 보도블럭",
      "img": "../images/block.jpg",
      "price": 4900
    },
    {
      "id": 5,
      "name": "칼라 립스틱",
      "img": "../images/lip.jpg",
      "price": 2900
    },
    {
      "id": 6,
      "name": "잉어 슈즈",
      "img": "../images/fish.jpg",
      "price": 3900
    },
    {
      "id": 7,
      "name": "웰컴 매트",
      "img": "../images/welcome.jpg",
      "price": 6900
    },
    {
      "id": 8,
      "name": "강시 모자",
      "img": "../images/hat.jpg",
      "price": 9900
    }
  ],
  "cartItems": [
    {
      "itemId": 1,
      "quantity": 1
    },
    {
      "itemId": 5,
      "quantity": 7
    },
    {
      "itemId": 2,
      "quantity": 3
    }
  ]
}

 

 

'상품 리스트' 페이지에서 장바구니 담기 버튼을 클릭하면 호출되는 함수

 

const handleClick = (e, id) => {
  for(const el of cartItems) {	// cartItems는 App.js에서 props로 전송 받음
      
    if(el.itemId === id) {
      el.quantity++;
      // const copy = [...cartItems];
      // setCartItems(copy);
      console.log(cartItems);
      return; 
    }
  }

  const newCartItem = {
    "itemId": id,
    "quantity": 1
  }

  const copy = [...cartItems];
  copy.push(newCartItem);
  setCartItems(copy);
}

 

위 함수 내부에는 두 가지 상황이 있다.

 

1. 장바구니에 이미 있는 item을 또 담을 경우

2. 장바구니에 없는 item을 담을 경우

 

1번의 경우 기존 cartItems 배열에서 입력받은 id와 같은 itemId를 가지고 있는 객체 요소를 찾아서 그 객체의 quantitiy를 1 증가시켜줘야 한다. 2번의 경우 새로운 객체를 만들어서 배열에 push해주면 된다.

 

그런데 내가 작성한 코드(1번)에는 몇가지 의문점이 있다.

 

function App() {
  const [items, setItems] = useState(initialState.items);
  const [cartItems, setCartItems] = useState(initialState.cartItems);
  
  ...
}

 

1. el은 App에서부터 전송 받아온 cartItems라는 state의 요소인데, 그걸 직접 수정하는 것이 옳은 방법인가?

2. setCartItems로 수정하지 않아도 페이지가 리렌더링되는 현상은 왜 그런 것인가?

 

 

1 A) 옳지 않다. 다른 로직이 중간에 끼어들 경우 오류가 발생할 수 있다. 지금은 코드가 간단해서 괜찮은 것 뿐..

2 A) 리렌더링되는 것이 아니다. cartItems만 변경된 것이다. setCartItems가 호출되지 않았기 때문에 리액트는 상태 변경을 감지할 수 없다. useEffect로 콘솔을 찍어보자.

 

  useEffect(()=>{
    console.log('re-rendered');
  }, [cartItems])

 

 

 

배열이 수정된 것에 대한 콘솔은 찍히지만 're-rendered'가 찍히지 않았다. 리렌더링되지 않은 것임.

 

cartItems를 직접 건들지 않는 식으로 코드를 수정해보았다.

수정한 코드는 아래와 같다.

 

const handleClick = (e, id) => {
  const index = cartItems.findIndex((el) => el.itemId === id); // 있는지 없는지 체크
  if (index === -1) {
    const newItem = {
      "itemId": id,		// itemId: id,
      "quantity": 1,		// quantity: 1  로 써도 된다. 객체의 키값은 무조건 문자열임
    }
    const copy = [...cartItems];
    copy.push(newItem);
    setCartItems(copy);
  } else {
    const copy = [...cartItems];	// 기존의 배열을 건드리지 않도록 새로 만들어서
    copy[index].quantity++;		// quantity 증가시켜준다.
    setCartItems(copy);
  }
  console.log(cartItems);
}

 

 

+ input type 'number'

 

 

input type을 'number'로 하면 위와 같이 1씩 증감시킬 수 있는 버튼이 기본적으로 생긴다.

처음에는 저걸 몰랐어서 저 버튼 하나 하나에 increase, decrease 기능을 주려고 했는데 그게 아니었다.

 

<input
  type="number"
  min={1}
  className="cart-item-quantity"
  value={quantity}
  onChange={(e) => {
    handleQuantityChange(Number(e.target.value), item.id)
  }}>
</input>

 

onChange 이벤트로 handleQuantityChange 함수를 호출하고, 인자로는 e.target.value(숫자)와 item의 id를 넘겨준다.

 

const handleQuantityChange = (quantity, itemId) => {
  setCartItems(cartItems.map(el => {
    if (el.itemId === itemId) {
      return {
        "itemId": itemId,
        "quantity": quantity
      };
    } else return el;
  }));
}

 

 

 

 

피드백

 

findIndex를 쓰는 게 뭔가 손에 잘 안 붙는다. map, filter 사용하는 건 익숙한데 forEach, findIndex 이런 건 아직 좀 더 많이 써봐야할 것 같다. 그때 그때 필요한 메서드는 분명히 존재한다. filter나 for + if 로 다 퉁치려고 하지 말자.

 

가급적이면 state로 정의된 요소는 직접 수정하지 않고 우회해서 수정하는 방식을 찾아내자.