본문으로 건너뛰기
개발 머꼬
개발 노트React
hohyeon.dev18

setCount를 세 번 불렀는데 1만 늘어난 이유

  • #Common Pitfall
  • #Engineering Note
  • #React

문제 발생

장바구니 수량을 한 번에 3 늘리려고 했습니다.

function onClick() {
  setCount(count + 1);
  setCount(count + 1);
  setCount(count + 1);
}

결과는 항상 +1이었습니다. console.log(count)를 바로 뒤에 찍어봐도 이전 값이었습니다.

원인 분석

렌더마다 state 값이 고정되어 있기 때문입니다. React 문서가 이 동작을 그대로 설명합니다 — React는 state 업데이트를 처리하기 전에 이벤트 핸들러의 모든 코드가 실행될 때까지 기다립니다. 이것이 배칭입니다.

그래서 count0인 렌더에서는 세 줄 모두 setCount(0 + 1)이 됩니다. 같은 값을 세 번 예약한 것이라 최종 결과는 1이고, 리렌더는 세 번이 아니라 한 번만 일어납니다.

문서는 이 설계의 이유도 적습니다 — 배칭이 앱을 훨씬 빠르게 만들고, 일부 변수만 갱신된 "덜 끝난" 렌더를 방지한다는 것입니다.

값을 누적하려면 값이 아니라 함수를 넘겨야 합니다. 문서 설명대로 갱신 함수를 넘기면 React가 큐에 넣고 다음 렌더에서 순서대로 처리하며, 각 함수는 앞선 갱신의 결과를 인자로 받습니다.

해결 방안

  1. 이전 값을 기준으로 하는 갱신에는 함수를 넘깁니다.
setCount((c) => c + 1);
setCount((c) => c + 1);
setCount((c) => c + 1);   // 최종 3
  1. 비동기 작업 뒤의 갱신에도 같은 규칙이 적용됩니다. await 이후의 count는 그 시점의 최신 값이 아니라 여전히 그 렌더의 스냅샷입니다. 오래된 값 위에 덮어쓰는 버그가 여기서 나옵니다.

  2. setState 직후에 값을 읽으려 하지 않습니다. 다음 값이 필요하면 지역 변수로 계산해서 함께 씁니다.

const next = count + 1;
setCount(next);
track("count_changed", next);
  1. 여러 state가 함께 움직이면 하나로 묶습니다. 서로를 참조하며 갱신하는 state가 셋 이상이면 useReducer가 대개 더 읽기 쉽습니다.

  2. 인자 이름은 짧게 씁니다. 문서가 소개하는 관례대로 state 이름의 첫 글자(c, e, fc)나 prev 접두사를 씁니다 — 바깥의 count를 실수로 다시 참조하는 것을 눈으로 막아줍니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.