언어 및 도구들 공부/React

React에서 상태 업데이트와 비동기처리

Dr.Mlem 2024. 12. 5. 15:49
Q1.

React에서,

 

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

setCount(count + 1);

setCount(count + 1);

 

이런 코드가 있으면, 최종적으로 count는 1이야? 2야? 그 이유도 자세히 설명해줘

 
 
 
A1.

ChatGPT의 말:

 

count의 최종 값은 1입니다.

이유:

setCount는 비동기로 작동합니다. 이 때문에 setCount를 호출할 때 즉시 상태(count)가 업데이트되지 않고, React가 상태를 배치(batch)로 처리하여 업데이트를 스케줄링합니다.

코드 분석:

const [count, setCount] = useState(0);
setCount(count + 1);  // 첫 번째 업데이트
setCount(count + 1);  // 두 번째 업데이트
  1. useState로 초기 count 값이 0으로 설정됩니다.
  2. 첫 번째 setCount(count + 1)가 호출됩니다. 이때의 count는 아직 0입니다. 따라서 count + 1의 결과는 1이고, 이 값이 React에 상태 업데이트로 전달됩니다.
  3. 두 번째 setCount(count + 1)도 호출됩니다. 하지만 이 시점에서도 count는 여전히 0입니다. React가 아직 첫 번째 업데이트를 처리하지 않았기 때문입니다. 따라서 두 번째 호출도 동일하게 1을 전달합니다.

결과적으로 두 번의 setCount 호출 모두 1이라는 동일한 값으로 상태 업데이트를 요청하게 됩니다.


React의 상태 처리 방식:

  • React는 같은 렌더링 사이클 내에서 여러 상태 업데이트를 배치(batch)하여 처리합니다.
  • 각 setCount는 독립적으로 현재의 상태(count)를 기준으로 계산됩니다.
  • 첫 번째와 두 번째 setCount 호출이 모두 동일한 count 값(0)을 참조하기 때문에 결과가 같아집니다.

해결 방법:

만약 count 값을 2씩 증가시키고 싶다면 이전 상태를 기반으로 업데이트를 해야 합니다. 이를 위해 setCount에 업데이트 함수를 전달할 수 있습니다:

setCount(prevCount => prevCount + 1);
setCount(prevCount => prevCount + 1);

이 방식에서는 prevCount가 React에 의해 최신 상태 값으로 제공되므로, 두 번의 호출이 순차적으로 처리됩니다.

  1. 첫 번째 setCount는 prevCount를 기준으로 prevCount + 1을 계산합니다.
  2. 두 번째 setCount는 업데이트된 상태 값(prevCount + 1)을 기준으로 다시 계산합니다.

결과적으로 count 값은 2가 됩니다.


핵심 요약:

  • setCount(count + 1)를 연속으로 호출하면 최종적으로 count는 1이 됩니다.
  • 이를 해결하려면 이전 상태를 참조하는 업데이트 함수를 사용하는 것이 중요합니다. (setCount(prev => prev + 1))

 

 
 
Q2.

나의 말:

만약, setCount(count+1); 이후 충분히 count가 1이 더해졌을 시간이 경과한 후에 한 번 더 setCount(count+1); 코드가 들어갔거나, 아니면 setCount(count+1); 의 후속처리로 다시 setCount(count+1); 코드가 작동하게 한다면, 그때에는 2가 되는건가?

 
 
 
A2.

ChatGPT의 말:

 

아니요, 그때에도 count의 최종 값은 2가 되지 않습니다. 이유는 여전히 count 값의 업데이트와 React 상태 관리의 동작 방식 때문입니다.

React의 상태 관리에서 중요한 점은 count의 값을 즉시 변경하지 않고, 상태 업데이트는 배치로 처리된다는 것입니다. 상태를 읽을 때 항상 "현재 렌더링된 값"을 참조하게 됩니다.


상황 분석

setCount(count + 1); // 첫 번째 호출
// (충분한 시간이 지난 후 또는 후속 처리로)
setCount(count + 1); // 두 번째 호출

상태 처리 방식:

  1. 첫 번째 setCount(count + 1)가 호출됩니다.
    • 이때 count는 현재 렌더링된 값(예: 초기 값이 0)을 참조합니다.
    • 따라서 count + 1은 1로 계산되고, React는 상태를 업데이트합니다.
    • 하지만 React는 즉시 count를 1로 변경하지 않습니다. 대신 React는 "다음 렌더링 시 count를 1로 업데이트"하도록 스케줄링합니다.
  2. 시간이 충분히 경과한 후, 두 번째 setCount(count + 1)가 호출됩니다.
    • 이 시점에서 count는 여전히 이전 렌더링의 값인 0을 참조합니다. (첫 번째 업데이트는 아직 렌더링에 반영되지 않았습니다.)
    • 따라서 두 번째 호출에서도 count + 1은 1이 되고, React는 동일한 값 1로 상태를 다시 스케줄링합니다.

최종 결과:

  • React는 두 번 모두 count를 1로 설정하도록 업데이트를 요청했으므로, 최종적으로 count는 1로 유지됩니다.

왜 이렇게 동작하는가?

React에서 useState를 사용할 때 상태 값은 렌더링 사이클마다 고정됩니다. 상태가 변경되더라도 그 값은 해당 렌더링 내에서는 변경되지 않습니다. 즉, 상태가 업데이트되어도 그 변경 사항은 다음 렌더링에 반영됩니다.

따라서 setCount(count + 1)가 호출될 때마다 count는 현재 렌더링된 값(예: 0)을 참조하게 됩니다.


두 번째 호출에서 2가 되려면?

방법 1: 이전 상태 값을 참조

React의 상태 업데이트는 함수형 업데이트를 통해 해결할 수 있습니다. 함수형 업데이트를 사용하면 React가 상태 값을 최신 값으로 유지하면서 업데이트를 처리합니다:

setCount(prevCount => prevCount + 1); // 첫 번째 호출
setCount(prevCount => prevCount + 1); // 두 번째 호출

이 경우:

  1. 첫 번째 호출에서 prevCount는 0이고, prevCount + 1은 1로 설정됩니다.
  2. 두 번째 호출에서는 첫 번째 호출 후의 값(1)을 기반으로 prevCount + 1을 계산하여 최종 값이 2가 됩니다.

방법 2: 상태 업데이트 후 명시적으로 새로운 상태 참조

React에서 상태가 업데이트된 후 명시적으로 새로운 상태 값을 참조하려면 상태 값이 변경된 뒤 새로운 렌더링이 이루어진 이후에 setCount를 호출해야 합니다. 예를 들어:

setCount(count + 1); // 첫 번째 호출
setTimeout(() => {
  setCount(currentCount => currentCount + 1); // 두 번째 호출
}, 0); // 렌더링 후 실행

이 경우, 두 번째 호출에서는 첫 번째 호출 후의 최신 상태 값을 참조하게 됩니다.


핵심 요약

  • React 상태 값은 해당 렌더링 사이클에서 고정된 값을 참조합니다.
  • 동일한 렌더링 내에서 setCount(count + 1)를 여러 번 호출하면 항상 동일한 count 값에 기반해 계산되므로 결과는 1로 유지됩니다.
  • 2로 만들려면 이전 상태 값을 참조하는 함수형 업데이트 방식(setCount(prev => prev + 1))을 사용하거나, 상태 업데이트 후 렌더링이 완료된 뒤 새로운 상태를 참조해야 합니다.
 
 
 
Q3.

나의 말:

React는 비동기 처리를 스케줄링해두고 렌더링하면서 한 번에 몰아서 처리하는거야?

 
 
 
A3.

ChatGPT의 말:

 

맞습니다! React는 비동기적으로 상태 업데이트를 스케줄링하고, 렌더링 과정에서 한 번에 배치(batch) 처리합니다. 이 방식은 React의 성능 최적화를 위해 중요한 설계 철학 중 하나입니다.


React의 상태 업데이트와 렌더링 원리

  1. 상태 업데이트는 비동기적:
    • React에서 상태를 업데이트하면 (setState 또는 setCount), React는 즉시 상태를 변경하지 않습니다.
    • 대신 상태 업데이트 요청을 스케줄링하고, 이를 처리할 최적의 시점을 기다립니다.
    • React는 업데이트를 처리하기 전에 여러 개의 상태 업데이트를 배치(batch) 처리하여 렌더링 횟수를 줄이고 성능을 최적화합니다.
  2. 렌더링은 동기적:
    • React는 상태 변경이 완료된 후 한 번에 컴포넌트를 다시 렌더링합니다.
    • 상태 변경이 발생할 때마다 매번 렌더링하지 않고, 동일한 렌더링 사이클 내에서 발생한 상태 업데이트를 하나로 묶어 처리합니다.

상태 업데이트의 비동기적 처리 방식

예제:

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

function handleClick() {
  setCount(count + 1); // 첫 번째 상태 업데이트
  setCount(count + 1); // 두 번째 상태 업데이트
  console.log(count);  // 여전히 이전 값 출력 (0)
}

return <button onClick={handleClick}>Click Me</button>;

실행 흐름:

  1. 버튼 클릭 시, handleClick 함수가 호출됩니다.
  2. setCount(count + 1)가 두 번 호출되지만, React는 이를 즉시 처리하지 않고 스케줄링합니다.
  3. console.log(count)는 여전히 현재 렌더링된 count 값(0)을 출력합니다.
  4. React는 두 번의 상태 업데이트를 배치하여 최적화된 방식으로 한 번의 렌더링을 수행합니다.

React의 배치 처리 (Batching)

React는 효율성을 위해 동일한 이벤트 핸들러나 동기 함수 내에서 발생하는 여러 상태 업데이트를 하나의 렌더링으로 묶어서 처리합니다.

예제:

const [state1, setState1] = useState(0);
const [state2, setState2] = useState(0);

function handleClick() {
  setState1(state1 + 1);
  setState2(state2 + 1);
  console.log(state1, state2); // 이전 값 출력
}
  1. setState1과 setState2는 각각 상태 업데이트를 요청합니다.
  2. React는 두 업데이트를 **배치(batch)**로 묶습니다.
  3. 한 번의 렌더링에서 state1과 state2의 업데이트가 동시에 반영됩니다.

React 18 이후의 업데이트

React 18부터는 비동기 상태 업데이트의 배치 처리가 더욱 강력해졌습니다. 이제는 React가 비동기 함수(async/await)나 타이머(setTimeout) 내에서도 상태 업데이트를 배치 처리합니다.

예제:

async function handleAsync() {
  setCount(count + 1); // 첫 번째 업데이트
  await someAsyncTask();
  setCount(count + 1); // 두 번째 업데이트 (React 18 이후에도 배치 처리)
}

React 18 이전에는 비동기 코드 내의 상태 업데이트가 각각 렌더링을 트리거했지만, 이제는 이러한 업데이트도 배치 처리됩니다.


배치 처리가 중요한 이유

  1. 성능 최적화:
    • 렌더링 횟수를 최소화하여 성능을 크게 개선합니다.
    • 불필요한 렌더링을 줄임으로써 애플리케이션이 더 빠르고 부드럽게 작동합니다.
  2. 일관성 유지:
    • 상태 업데이트를 한 번에 처리하여 일관된 상태를 유지합니다.
    • 여러 상태 변경이 있을 때, 중간 단계가 아닌 최종 상태를 기반으로 렌더링이 이루어집니다.

정리

  • React는 상태 업데이트를 비동기적으로 스케줄링합니다.
  • 여러 상태 업데이트를 **배치(batch)**하여 렌더링 과정에서 한 번에 처리합니다.
  • 이는 React가 성능을 최적화하고 일관된 UI를 유지하는 중요한 원리입니다.
  • React 18 이후로 배치 처리의 범위가 더 확장되어, 비동기 코드에서도 안정적으로 작동합니다.
 
 
 
Q4.

나의 말:

스케줄링하는 것은, 비단 상태변수에 한하는 내용인가? 아니면 다른 비동기 처리에 대한 내용들 모두에 해당되는 내용인가?

 
 
 
A4.

ChatGPT의 말:

 

React에서 "스케줄링"은 주로 상태 업데이트와 렌더링과 관련된 처리에 초점이 맞춰져 있습니다. 그러나 React의 설계 철학과 이벤트 루프를 기반으로 한 JavaScript의 동작 방식 때문에, 다른 비동기 작업과의 상호작용에도 영향을 미칠 수 있습니다.


1. React의 스케줄링: 상태와 렌더링 중심

React에서 스케줄링은 상태 업데이트렌더링 최적화에 국한됩니다. 상태 업데이트는 비동기적으로 처리되며, 여러 상태 변경이 한 번의 렌더링으로 배치됩니다. 이는 React 내부의 렌더링 스케줄러(React Reconciler) 덕분입니다.

상태 스케줄링의 특징:

  • 상태 업데이트(setState, useState)가 호출되면, React는 즉시 새로운 값을 렌더링하지 않습니다.
  • React는 현재 렌더링 중인 작업이 끝난 후, 여러 상태 업데이트를 묶어서 한 번의 렌더링으로 처리합니다.
  • React의 동작은 JavaScript 이벤트 루프와 조화롭게 작동하지만, React는 UI 업데이트와 관련된 상태를 제어할 때만 개입합니다.

2. 일반적인 비동기 작업과 React의 관계

React의 스케줄링은 상태 관리 및 렌더링에 초점이 맞춰져 있지만, 일반적인 비동기 작업(Promise, async/await, setTimeout)도 React의 흐름에 영향을 줄 수 있습니다.

(a) React는 이벤트 루프를 따름

React는 JavaScript 런타임 위에서 동작하기 때문에, 일반적인 비동기 작업(setTimeout, fetch)은 JavaScript의 이벤트 루프 규칙에 따라 실행됩니다. React는 이러한 비동기 작업 자체를 제어하지 않지만, 그 결과를 상태 업데이트로 처리할 때 개입합니다.

(b) React 상태 업데이트와 비동기 코드의 상호작용

React 18부터는 비동기 함수 내 상태 업데이트도 배치 처리됩니다. 이로 인해 React의 스케줄링이 일반적인 비동기 코드와 더 밀접하게 동작합니다.

예제:

async function handleAsync() {
  setCount(count + 1); // 첫 번째 상태 업데이트
  await someAsyncTask(); // 비동기 작업 수행
  setCount(count + 1); // 두 번째 상태 업데이트
}

React 18 이전:

  • 두 번의 setCount 호출이 각각 별도의 렌더링을 트리거합니다.

React 18 이후:

  • 두 번의 setCount 호출이 배치 처리되어 한 번의 렌더링으로 완료됩니다.

3. React 외 비동기 처리와의 차이점

React의 스케줄링과 JavaScript의 일반적인 비동기 처리(Promise, async/await, setTimeout)는 서로 다르게 동작합니다.

(a) 일반 비동기 작업의 처리 순서:

JavaScript에서는 비동기 작업이 이벤트 루프의 순서에 따라 실행됩니다.

  • setTimeout은 태스크 큐에 등록되어, 현재 실행 중인 코드와 마이크로태스크(Promise)가 완료된 후 실행됩니다.
  • Promise는 마이크로태스크 큐에 등록되며, 현재 실행 중인 코드가 완료되면 가장 먼저 실행됩니다.
console.log("Start");

setTimeout(() => {
  console.log("Timeout");
}, 0);

Promise.resolve().then(() => {
  console.log("Promise");
});

console.log("End");

출력 결과:

Start
End
Promise
Timeout

(b) React의 상태 스케줄링은 독립적

React는 위의 비동기 처리 흐름과 별도로, 상태와 렌더링의 스케줄링을 관리합니다. React의 상태 업데이트는 이벤트 루프의 실행 순서와는 독립적으로, React 내부 스케줄러에 의해 최적화됩니다.


4. React 스케줄링과 외부 비동기 작업의 혼합

React와 일반 비동기 작업이 함께 사용되면, 두 스케줄링 방식이 서로 영향을 주기도 합니다.

예제: 비동기 작업 내에서 React 상태 업데이트

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

  async function handleClick() {
    console.log("Before:", count);
    setCount(count + 1); // 첫 번째 상태 업데이트
    await new Promise(resolve => setTimeout(resolve, 1000));
    setCount(count + 1); // 두 번째 상태 업데이트
    console.log("After:", count);
  }

  return <button onClick={handleClick}>Click Me</button>;
}
  • 첫 번째 setCount는 count + 1로 업데이트하고 렌더링합니다.
  • 두 번째 setCount는 별도로 처리되어 또 한 번 렌더링합니다.
  • 출력 결과는 Before: 0, After: 0입니다(렌더링이 끝나야 상태 값이 업데이트되므로).

React 18 이후:

  • 두 번의 setCount가 배치 처리되어 한 번의 렌더링만 발생합니다.
  • 하지만 console.log("After:", count)는 여전히 이전 렌더링의 count 값을 참조하므로 0으로 출력됩니다.

5. React의 스케줄링 범위

React의 스케줄링은 상태 업데이트와 렌더링에 초점이 맞춰져 있습니다. 외부의 일반적인 비동기 작업은 React의 스케줄링과 독립적이지만, 상태 업데이트가 포함될 경우 React의 배치 처리 방식에 영향을 받습니다.


요약

  • React의 스케줄링은 상태 업데이트와 렌더링 최적화에 집중됩니다.
  • React는 JavaScript의 비동기 처리 규칙(이벤트 루프)을 따르지만, React 내부적으로 상태 업데이트를 배치하여 처리합니다.
  • React 18 이후, 비동기 코드(async/await, setTimeout) 내 상태 업데이트도 배치 처리되며, React의 스케줄링과 더 긴밀하게 연동됩니다.
  • 비상태 관련 일반 비동기 작업은 React의 스케줄링과 독립적으로 동작합니다.