리액트에서 상태의 비동기 처리와 자바스크립트의 일반적인 비동기 처리는 서로 다른 방식으로 작동합니다. 리액트는 성능 최적화와 효율적인 렌더링을 위해 상태 변경을 비동기적으로 처리하며, 상태 변경이 일어날 때마다 즉시 렌더링하는 대신, 변경된 상태를 **일괄 처리(batch processing)**하여 한 번에 렌더링하는 방식으로 동작합니다.
이를 이해하기 위해 상태의 비동기 처리가 어떻게 동작하는지, 자바스크립트의 비동기 처리와는 어떤 차이가 있는지, 그리고 렌더링 주기가 어떻게 상태와 관련되는지 구체적인 예시와 함께 설명하겠습니다.
1. 리액트 상태의 비동기 처리: 구체적 예시
리액트에서 상태 변경이 발생할 때, 그 상태가 즉시 변경되지 않고 비동기적으로 처리되는 이유를 예시로 살펴보겠습니다.
예시 코드:
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1); // 상태를 1 증가시키는 요청
setCount(count + 2); // 상태를 2 증가시키는 요청
console.log(count); // 이 시점에서 count 값은 0으로 출력됨 (비동기적 처리)
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
동작 과정:
1. setCount(count + 1) 호출: setCount는 상태를 1 증가시키는 요청입니다. 하지만 리액트는 이 요청을 **즉시 처리하지 않고 대기열(queue)**에 추가합니다. 이 시점에서 상태는 아직 업데이트되지 않았고, count는 여전히 0입니다.
2. setCount(count + 2) 호출: 두 번째 setCount도 대기열에 추가됩니다. 하지만 이 호출 역시 즉시 처리되지 않고 대기열에서 처리됩니다.
3. console.log(count) 실행: 상태 업데이트가 아직 완료되지 않았기 때문에 이 시점에서 count는 여전히 초기값인 0입니다. setCount는 비동기적으로 처리되기 때문에, console.log(count)는 변경된 값을 알지 못하고 기존 값인 0을 출력합니다.
4. 상태 업데이트 처리: 리액트는 상태 업데이트를 일괄 처리합니다. 여기서 중요한 점은, 두 번의 setCount 호출 중 마지막 호출만 적용됩니다. 두 번째 setCount(count + 2)가 이전 setCount(count + 1)을 덮어쓰게 됩니다. 따라서 count는 count + 2가 적용된 값만 반영되며, 첫 번째 상태 변경 요청은 무시됩니다.
왜 마지막 setCount만 적용되나?
리액트는 여러 상태 업데이트가 있을 때, 성능을 최적화하기 위해 **일괄 처리(batch processing)**를 수행합니다. 이 과정에서 첫 번째 setCount(count + 1)은 count가 0일 때의 값을 기반으로 상태를 1 증가시키는 요청입니다. 그러나 그 다음 setCount(count + 2)도 여전히 count가 0인 상태를 기반으로 계산하므로, 결국 이 두 요청이 충돌합니다. 리액트는 마지막 상태 업데이트만 반영하여 count + 2를 적용하게 됩니다.
두 번의 상태 업데이트를 모두 적용시키는 방법
상태를 업데이트할 때 이전 상태를 기반으로 계산하려면, 상태 업데이트 함수를 함수 형태로 사용해야 합니다. 함수형 업데이트는 리액트가 최신 상태 값을 기반으로 상태를 업데이트하도록 보장해 줍니다.
수정된 코드 (함수형 업데이트 사용):
function Counter() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(prevCount => prevCount + 1); // 이전 상태 기반으로 1 증가
setCount(prevCount => prevCount + 2); // 그다음 상태 기반으로 2 증가
console.log(count); // 여전히 0이 출력됨 (비동기 처리)
};
return (
<div>
<p>Count: {count}</p>
<button onClick={handleClick}>Increment</button>
</div>
);
}
동작 방식:
• setCount(prevCount => prevCount + 1): 리액트는 prevCount라는 이전 상태 값을 함수로 받아서 그 값을 기반으로 상태를 1 증가시킵니다.
• setCount(prevCount => prevCount + 2): 그다음 prevCount는 첫 번째 상태 업데이트 후의 최신 값(prevCount + 1)이므로, 그 값을 기반으로 2를 더합니다.
• 결과적으로: 두 번의 상태 업데이트가 차례대로 적용되어, count는 count + 3이 됩니다.
리액트의 setState 또는 useState에서 제공하는 상태 업데이트 함수는 **함수형 업데이트(functional update)**라는 특별한 기능을 제공합니다. 이 기능은 현재 상태 값을 함수로 전달받아 그 상태를 기반으로 새로운 상태를 계산할 수 있게 해줍니다.
결론:
• 리액트에서 상태 업데이트는 비동기적으로 처리되며, 여러 상태 업데이트가 발생하면 리액트는 이를 일괄 처리합니다.
• 상태 업데이트가 여러 번 일어나면, 마지막 상태 업데이트만 적용될 수 있습니다. 이를 방지하려면 함수형 업데이트를 사용해야 하며, 이는 리액트가 최신 상태 값을 기반으로 상태를 업데이트하게 해 줍니다.
• 상태 업데이트는 비동기적으로 처리되므로, 상태 변경 후에 바로 console.log를 호출하면 예상했던 값이 출력되지 않을 수 있습니다.
2. 자바스크립트의 비동기 처리와의 차이
자바스크립트에서 비동기 작업은 **이벤트 루프(event loop)**와 **콜백 큐(callback queue)**에 의해 처리됩니다. 예를 들어 setTimeout은 비동기적으로 동작하며, 지정한 시간이 지나면 콜백 함수가 호출됩니다. 자바스크립트는 싱글 스레드로 동작하며, 비동기 작업이 처리되는 동안 다른 코드가 계속 실행될 수 있도록 설계되었습니다.
자바스크립트의 비동기 처리 예시:
console.log("Start");
setTimeout(() => {
console.log("This is a timeout");
}, 1000);
console.log("End");
실행 결과:
Start
End
This is a timeout
• setTimeout은 1초 뒤에 실행되므로, **“Start”**와 **“End”**는 즉시 출력되고, 1초 뒤에 **“This is a timeout”**이 출력됩니다. 이는 자바스크립트의 이벤트 루프 메커니즘 때문입니다. 자바스크립트는 먼저 동기 코드(콜스택에 있는 코드)를 모두 처리한 후 비동기 코드를 실행합니다.
리액트 상태 업데이트와 자바스크립트 비동기 처리의 차이점
• 자바스크립트의 비동기 작업은 이벤트 루프를 통해 비동기 작업(예: setTimeout, fetch)을 처리한 후, 이벤트 큐에서 콜백 함수가 실행됩니다.
• 리액트의 상태 업데이트는 자바스크립트의 이벤트 루프와 달리, 리액트 내부의 렌더링 주기와 관련이 있습니다. 리액트는 상태 변경 요청을 받으면 이를 대기열에 넣고, 다음 렌더링 주기가 시작될 때 상태 변경을 일괄 처리합니다.
3. 리액트의 렌더링 주기와 상태 업데이트
리액트에서 **렌더링 주기(rendering cycle)**는 컴포넌트가 상태나 props의 변경에 따라 UI를 다시 그리는 주기입니다. 상태가 변경되면 리액트는 컴포넌트를 다시 렌더링하기 위해 일련의 과정을 거치는데, 이때 **reconciliation(조정)**이라는 과정을 통해 최적화된 방식으로 UI를 업데이트합니다.
렌더링 주기:
1. 상태 또는 props 변경:
• 상태(state)나 props가 변경되면 리액트는 해당 컴포넌트를 다시 렌더링할 준비를 합니다.
2. 대기열에 상태 변경 요청 추가:
• 상태 변경 요청(setState 또는 useState)은 즉시 실행되지 않고 대기열에 추가됩니다.
3. 렌더링 주기 내에서 일괄 처리:
• 리액트는 대기열에 있는 상태 변경 요청을 일괄 처리합니다. 이를 통해 여러 상태 변경이 있더라도 한 번의 렌더링만 발생하게 됩니다.
4. DOM 업데이트:
• 리액트는 가상 DOM에서 변경 사항을 확인하고, 실제 DOM에 반영할 최소한의 업데이트만 수행합니다. 이 과정에서 불필요한 렌더링을 방지합니다.
4. 리액트 상태 비동기 처리의 장점
1. 성능 최적화:
• 리액트는 여러 상태 업데이트를 **일괄 처리(batching)**하여, 컴포넌트를 한 번만 리렌더링합니다. 이는 애플리케이션의 성능을 크게 향상시키는 중요한 최적화 기법입니다.
2. 불필요한 렌더링 방지:
• 만약 상태가 동기적으로 처리되었다면, 각각의 상태 변경이 발생할 때마다 리렌더링이 발생할 것입니다. 하지만 리액트는 비동기적으로 상태를 처리함으로써 불필요한 렌더링을 방지합니다.
3. 예측 가능한 렌더링:
• 리액트는 상태 변경이 일어나도 즉시 렌더링하지 않으므로, 개발자가 언제 렌더링이 발생할지 예측할 수 있습니다. 모든 상태 변경은 리액트의 렌더링 주기에 맞춰 일괄 처리되므로, 상태 관리와 렌더링 시점을 쉽게 파악할 수 있습니다.
5. React 18에서 도입된 automatic batching
React 18에서는 **automatic batching(자동 일괄 처리)**가 도입되었습니다. 이 기능은 더 많은 상황에서 상태 업데이트를 자동으로 일괄 처리하여, 성능 최적화를 더 강화한 방식입니다. 예를 들어, 이전에는 이벤트 핸들러 내에서만 상태 업데이트가 일괄 처리되었지만, 이제는 비동기 작업(setTimeout, fetch) 내에서도 상태 업데이트가 자동으로 일괄 처리됩니다.
예시:
import { useState } from 'react';
function Example() {
const [count, setCount] = useState(0);
const [text, setText] = useState('Hello');
const handleClick = () => {
setTimeout(() => {
setCount(count + 1);
setText('Updated Text');
}, 1000);
};
return (
<div>
<p>{count}</p>
<p>{text}</p>
<button onClick={handleClick}>Update</button>
</div>
);
}
• React 18 이전에는 setTimeout 내에서 상태 업데이트가 각각 렌더링을 트리거했습니다.
• React 18에서는 automatic batching 덕분에 setTimeout 내에서도 상태 변경이 한 번의 렌더링으로 처리됩니다.
요약
• 리액트의 상태 업데이트는 비동기적으로 처리되며, 성능 최적화와 불필요한 렌더링을 방지하기 위한 메커니즘입니다.
• 자바스크립트의 비동기 처리(setTimeout 같은 이벤트 루프)와는 달리, 리액트의 상태 업데이트는 렌더링 주기와 관련이 있으며, 상태 변경 요청이 일괄 처리됩니다.
• 리액트의 렌더링 주기는 상태가 변경되었을 때 컴포넌트를 다시 렌더링하는 과정이며, 이 과정에서 최적화된 방식으로 DOM 업데이트가 이루어집니다.
• React 18의 자동 일괄 처리 기능은 상태 변경을 더 많은 상황에서 일괄 처리하여 성능을 더욱 향상시킵니다.
이 방식 덕분에 리액트는 복잡한 상태 업데이트가 발생하더라도 최적화된 방식으로 렌더링을 수행하여 성능을 크게 향상시킬 수 있습니다
'React' 카테고리의 다른 글
[React] Hooks (0) | 2024.09.13 |
---|---|
[React] 컴포넌트 라이프사이클 (1) | 2024.09.13 |
[React] State (0) | 2024.09.13 |
[React] Props (0) | 2024.09.13 |
[React] 컴포넌트 개념과 역할 (0) | 2024.09.12 |