✨ state는 바로 변경되지 않는다.
setState를 사용했다고 해서, 바로 state의 값이 변경되는 것은 아니다.
setState를 사용하면, 새 state값을 받아서 컴포넌트 리렌더링 큐에 등록한다.
- 다음 리렌더링 시, useState가 반환하는 첫번째 값은 갱신된 최신 state가 된다.
즉, 다음 리렌더링 시에 useState가 호출되면서 돌려주는 값이 업데이트된 state값이 된다.
setState 사용 시, 바로 업데이트 하지않는 이유는 성능 향상 때문이다.
성능 향상을 위해 state를 setState호출 시마다 계속 업데이트하는 것이 아니라, 모아두고 한 번에 업데이트를 진행한다. 이를 Batching이라고 한다.
https://12ahn22.tistory.com/entry/React-18-Batching-%EB%B0%B0%EC%B9%AD
오류가 났던 상황 1
이벤트 핸들러 함수 속에서 event.target.value를 설정하는 부분에서 문제가 발생했다.
// 에러를 발생하던 잘못된 코드
// setState 이후, target의 value를 비워주는 코드
setState((prev)=> prev.concat(e.target.value));
e.target.value = "";
위 코드는 입력받은 e.target.value값이 정상적으로 추가되지않는다. setState는 바로 갱신되지않는다.
e.target.value = ""가 (prev) => prev.concat(e.target.value)보다 먼저 실행된다. (첫번째는 오류가 안나지만 두번째 state업데이트부터 잘못된 값이 들어감)
코드 샌드박스 예제 링크
https://codesandbox.io/s/usestate-error-xybdko?file=/src/App.js
✨ StrictMode 사용 시, setState에 함수를 전달하면 이중호출이 된다.
리액트 Docs를 보면, StrictMode 사용 시, 예상치 못한 부작용을 검사할 수 있다.
그 중 특정 함수를 사용 시, 의도적으로 이중으로 호출해 부작용을 찾을 수 있게한다. 가끔 프로젝트가 첫 렌더링이 두번 되는 현상이 있는 데, 이 또한 StrictMode를 사용하기 때문에 발생한다.
오류가 났던 상황 2
List.js 컴포넌트가 App.js로 부터 setList를 내려받아 버튼 클릭 시, 상태를 업데이트 한다.
list를 props로 내려받지않고 setList에 함수적 업데이트를 사용하려했을 때 원하는 데로 렌더링이 되지않는 에러가 발생했다.
// 잘못 만들었던 코드..
const clickHandler = (id) => {
setList((prev) => {
const idx = prev.findIndex((item) => item.id === id);
// 새 리스트 추가
if (idx === -1) {
return [...prev, { id, name, quantity: 1 }];
}
// 이 아래 코드들이 문제였다.
const newList = [...prev];
newList[idx].quantity = newList[idx].quantity + 1;
return newList;
});
};
기존 리스트에 해당 id값이 존재한다면 새로 추가하는 것이 아닌, quantity를 1씩 증가시켜주는 로직을 원했다.
그러나 첫번째 호출을 빼고 두번째 호출부터는 +2씩 증가하는 문제가 생겼다.
확인해 보니 click이벤트 발생 시, setList에 넘겨준 함수가 2번씩 호출되고 있었다. 원인은 위에서 쓴 것 처럼 StrictMode가 이중 호출을 하고 있던 상황이다.
StrictMode는 부작용을 찾을 수 있도록 도와주는 일을 한다.이중 호출을 했을 때, 내가 원하는 결과로 작동하지 않았다는 것은 잘못된 코드라고 생각한다.
따라서 위 코드를 변경했다.
const clickHandler = (id) => {
// prev를 사용한 방식이 +1씩이 아니라 +2씩 증가하는 이유...?
// Strict Mode는 useState에 함수를 전달하는 경우
// 이중 호출을 한다.
setList((prev) => {
const idx = prev.findIndex((item) => item.id === id);
// 새 리스트 추가
if (idx === -1) {
return [...prev, { id, name, quantity: 1 }];
}
const newList = [...prev].map((item) =>
item.id === id
? {
...item,
quantity: item.quantity + 1
}
: item
);
return newList;
});
};
newList에 직접 접근 해서 값을 바꾸지 않고, map함수를 사용해 아예 새로운 배열을 만들어줬다.
prev를 가지고 새로 만들어서 저장된 newList는 두번 호출이 되더라도 같은 값으로 상태를 변경하기 때문에 문제가 발생하지 않는다.
그러나 이전에 사용한 코드는 직접 접근해서 값을 바꾸기 때문에 이중 호출이 된다면 newList[idx].quantity를 직접 변경해버리기 때문에 +1이 한번 더 작동되어서 총 +2가 되게 된다.
즉, setState에 함수를 전달해 줄 때는 순수 함수를 전달해야한다.
같은 입력에 항상 같은 결과가 나올 수 있는 함수를 setState에 전달해줘야한다. 그렇지 않으면 StrictMode가 이중호출 시, 잘못된 값이 들어가게 된다.
물론, StrictMode는 개발환경에서만 작동한다. 빌드 후, 배포한다면 위 잘못된 코드도 +1씩만 작동하게 된다.
그러나 StrictMode는 발생할 수 있는 부작용을 미리 찾아준다. 즉, StrictMode에서 잘못 렌더링 된다면 추후에 문제가 발생할 수 있다는 걸 의미한다.
- 따라서 setState에 전달하는 함수는 꼭 순수 함수를 사용하자.
✨ 참고 링크
https://github.com/facebook/react/issues/12856
https://ko.reactjs.org/docs/strict-mode.html#detecting-unexpected-side-effects
https://stackoverflow.com/questions/62769684/setstate-of-usestate-hook-occurs-twice-by-one-call
'공부 > 리액트' 카테고리의 다른 글
기존 CRA 프로젝트에 TypeScript 추가하기 (0) | 2023.03.07 |
---|---|
[react-redux] 리액트에서 리덕스 사용하기 (1) | 2022.12.28 |
[react] 조건부 렌더링에 대한 고민.. (0) | 2022.12.16 |
[React 18] Batching 배칭 (1) | 2022.12.12 |
[react-quill] addrange() the given range isn't in document (0) | 2021.08.01 |
댓글