1. useState의 setter 함수는 동기적으로 호출되지만 비동기적으로 업데이트된다.(batch update)
리엑트는 state 변경 요청을 일괄적으로 처리한다. 이는 ui 업데이트를 효율적으로 하기 적합하지만, state 업데이트가 순차적으로 이뤄지길 원할 때는 문제가 된다. 아래 예시를 보자.
export default function App() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
console.log("렌더링");
const onClick = (e: any) => {
setCounter1(counter1 + 1);
setCounter2(counter1 + 1);
};
return (
<div className="App">
<button onClick={onClick}>+</button>
<p>counter1 : {counter1}</p>
<p>counter2 : {counter2}</p>
</div>
);
}
이 예시에서 버튼을 클릭하면 counter는 어떻게 변화할까?
setCounter1과 setCounter2는 동기적으로 실행된다. 따라서 0,0 => 1,2=>2,3 =>....=>n,n+1 로 예상할 수 있지만, 실제로 클릭해보면 0,0 =>1,1 => 2,2 순으로 변화한다.
그 이유는 두 setter함수의 업데이트 요청이 일괄적으로 처리되며, 갱신된 counter1 값은 리렌더링 이후에 사용가능하기 때문에 setCounter2함수는 갱신 이전의 counter1 값을 받기 때문이다.
그렇다면 여러 state간의 업데이트가 순차적으로 이뤄지도록 순서를 보장하려면 어떻게 해야 할까.
2. 잘못된 해결 방식: useEffect
useEffect는 최초 렌더링 시점이나 특정 상태값의 업데이트 시점, 컴포넌트의 언마운트 시점에 사이드 이펙트를 일으킬 수 있다. 그래서 나는 useEffect로 순차적인 state 변경을 보장하도록 코드를 짰다(하나의 state를 업데이트하면 리렌더링 이후 useEffect로 다음 state를 업데이트하도록).
export default function App() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
console.log("렌더링");
const onClick = (e: any) => {
setCounter1(counter1 + 1);
};
useEffect(() => {
if (counter1 === 0) return;
setCounter2(counter1 + 1);
}, [counter1]);
return (
<div className="App">
<button onClick={onClick}>+</button>
<p>counter1 : {counter1}</p>
<p>counter2 : {counter2}</p>
</div>
);
}
이게 문제가 뭐냐면, 작동과정이 state 업데이트 => ui 업데이트 => state 업데이트 => ui 업데이트... 의 방식으로 이뤄지면서 한번 상태값을 변경할 때 여러번의 렌더링이 연속적으로 발생하는 것이다.
(이 코드를 실행해보면 콘솔에 "렌더링" 문자가 두번 씩 찍힌다.)
2. 해결 방식1: state setter 함수 내에 update 함수 사용하기
state setter 함수에서는 특정 값 대신 함수를 이용하여 변경이 가능하다. 이를 update 함수라 부른다. update 함수의 인자는 이전에 호출된 state setter함수의 변경된 값에 기반하므로 순차적 업데이트가 가능하다. 아래 예시를 보자.
export default function App() {
const [counter1, setCounter1] = useState(0);
console.log("렌더링");
const onClick = (e: any) => {
setCounter1((prev) => prev + 1);
setCounter1((prev) => prev + 1);
};
return (
<div className="App">
<button onClick={onClick}>+</button>
<p>counter1 : {counter1}</p>
{/* <p>counter2 : {counter2}</p> */}
</div>
);
}
위 코드에서 두번 째 setState 함수의 업데이트 함수 인자는 첫번째 setState 함수의 변경값에 기반하므로 의도했던대로 작동한다.(2씩 증가)
3. 문제: state setter 함수 내에서 다른 state setter 함수 사용하기
서로 다른 두 개의 state를 일괄 업데이트하면서도 서로 의존성에 따른 순서가 지켜지도록 다음같이 코드를 짜보았다.
export default function App() {
const [counter1, setCounter1] = useState(0);
const [counter2, setCounter2] = useState(0);
console.log("렌더링", counter1, counter2);
const onClick = (e) => {
setCounter1((prev) => {
const returnValue = prev + 1;
setCounter2(returnValue + 1);
return returnValue;
});
};
return (
<div className="App">
<button onClick={onClick}>+</button>
<p>counter1 : {counter1}</p>
<p>counter2 : {counter2}</p>
</div>
);
}
setCounter1 내에서 setCounter2를 불러냄으로써 setCounter2의 업데이트 값을 확실히 보장하려고 했다.(지금 단계에서는 굳이 이렇게 짤 필요가 없긴 하지만, 나중의 경우를 위해 확인해보고 싶었다) 그런데, 이 코드는 예상대로 작동했지만, 콘솔에 로그가 두개 씩 연달아 찍히며 두번씩 렌더링되었다.
이 부분의 정확한 원리는 찾아도 알 수가 없었다. 원래 리엑트는 한 렌더링 주기 내에 있는 state 변경 요청을 일괄적으로 처리한다. 그러니 추측이지만, setCounter1를 실행하면 이벤트 큐에 등록된 후에 setCounter2도 실행되어 이벤트 큐에 등록되고 onClick 함수의 호출이 끝난 이후에 비동기적으로 같이 처리되어야 하는데, setter 함수를 내부에서 호출하면 어떤 문제가 생기는 듯 하다.
방안
크게 두 가지가 있다.
- setCounter1 에서 의존성이 있는 부분을 외부의 변수에 할당한 후 setCounter2에 넘겨주기
- setCounter1과 setCounter2을 하나의 state로 합쳐서 객체로 관리하기
전자의 방식
const onClick = (e) => {
let eventValue = 0;
setCounter1((prev) => {
const returnValue = counter1 + 1;
eventValue = returnValue;
// 외부 변수에 주요 의존성 값을 할당하여 다른 state 변경 요청에 사용
return returnValue;
});
setCounter2(eventValue + 1);
};
후자의 방식
const [counters, setCounters] = useState({counter1:0, counter2:0});
const onClick = (e) => {
setCounters((prev) => ({
...counters,
counter1:counters.counter1+1,
counter2: counters.counter1+2
}));
setCounter2(eventValue + 1);
};
4. 그 외
원래는 사이드 이펙트에 사용해야할 useEffect가 무분별하게 사용되는 경우를 막기 위해서 다양한 방법이 있을 것이다. 다른 좋은 방법을 소개하는 좋은 설명글 링크를 달아놓는다.
'react' 카테고리의 다른 글
[react] FLUX 디자인 패턴의 이해2 : 내 방식대로 설명하기 (0) | 2024.03.17 |
---|---|
[react] flux 디자인 패턴의 이해1: 데이터 바인딩과 데이터 흐름 (0) | 2024.03.17 |
[React] 파이버 아키텍처에 대한 개요 (0) | 2024.03.15 |
[react]리엑트의 비교조정(reconciliation) 알고리즘 이해 (0) | 2024.03.15 |
[React] 리엑트 디자인 패턴: 컴포넌트와 합성 컴포넌트 (0) | 2024.03.12 |