리액트 동시성 모드(Concurrent Mode)는 React가 UI 렌더링을 보다 부드럽고 효율적으로 관리할 수 있도록 도와주는 기능이다.
기존 동기 렌더링 방식과 달리, UI 업데이트를 비동기적으로 실행할 수 있어 더 나은 사용자 경험(UX)을 제공한다.
기존 React 동기 렌더링 방식에서는 렌더링이 한 번 시작되면 완료될 때까지 중단할 수 없었다.
시간이 지나면서 애플리케이션이 복잡해지고, 사용자 인터렉션이 많아질수록 렌더링이 길어질 수 있다. 이 경우 UI가 멈춘 것처럼 보이거나 입력이 지연되는 현상이 발생할 수 있다.
| 기존 React (동기 렌더링) | Concurrent Mode (비동기 렌더링) |
|---|---|
| 모든 작업이 순차적으로 실행 | 작업 중단 가능, 더 중요한 작업을 우선 실행 가능 |
| 렌더링이 길어지면 UI가 멈춘 것처럼 보일 수 있음 | 긴 작업을 작게 나누어 유휴 시간에 백그라운드에서 실행 |
| 브라우저가 사용자의 입력을 처리하기 전에 모든 렌더링을 완료해야 함 | UI가 멈추지 않고 사용자의 입력이 즉시 반영됨 |
startTransition을 사용하면 긴급한 업데이트(예: 버튼 클릭, 텍스트 입력)는 즉시 실행하고, 상대적으로 덜 중요한 업데이트(예: 데이터 로드, 애니메이션)는 나중에 처리할 수 있다.import React, { useState, useTransition } from "react";
export default function App() {
const [text, setText] = useState("");
const [list, setList] = useState([]);
const [isPending, startTransition] = useTransition(); // ✅ 동시성 모드 적용
const handleChange = (e) => {
setText(e.target.value);
// ✅ 이 작업은 긴급하지 않으므로 startTransition으로 처리
startTransition(() => {
const newList = Array(20000).fill(e.target.value);
setList(newList);
});
};
return (
<div>
<input type="text" value={text} onChange={handleChange} />
{isPending && <p>Rendering...</p>}
{list.map((item, index) => (
<p key={index}>{item}</p>
))}
</div>
);
}
startTransition을 사용하여, 입력 필드의 값 변경은 즉시 반영되지만 목록 업데이트는 백그라운드에서 실행된다.isPending 상태를 활용하면 백그라운드에서 렌더링이 진행 중인지 확인할 수 있다.동시성 기능은 렌더링 성능을 최적화하고 부드러운 사용자 경험을 제공하지만, 적절히 사용하지 않으면 의도치 않은 동작이나 성능 저하가 발생할 수 있다.
모든 경우에 동시성을 사용하지 않고 필요한 부분에만 적용할 것
동시성 모드는 여러 상태 업데이트를 병렬적으로 처리할 수 있지만, 상태 간 불일치(Inconsistent State)가 발생할 가능성이 있으므로 신중히 관리해야한다.
const [searchQuery, setSearchQuery] = useState("");
const [filteredData, setFilteredData] = useState([]);
useEffect(() => {
const results = data.filter((item) =>
item.includes(searchQuery)
);
setFilteredData(results);
}, [searchQuery]);
→ 위 코드에서 searchQuery와 filteredData는 서로 의존적이다. 동시성 모드에서는 searchQuery가 업데이트되어도 filteredData가 늦게 업데이트 될 수 있어 사용자 경험에 영향을 미칠 수 있다.
긴급한 작업은 startTransition 없이 즉시 실행되어야 하고, 비긴급 작업만 startTransition을 활용하여 긴급 업데이트와 비긴급 업데이트를 구분한다.
비동기 작업(fetch, setTimeout)과 혼동하지 않도록 주의
동시성 기능은 브라우저 환경에서 동작하며, SSR(Server Side Rendering)에서는 사용되지 않는다. useTransition이나 useDeferredValue를 사용하더라도 서버 사이드 렌더링 시에는 무시되므로, 클라이언트에서만 영향을 미친다는 점을 기억해야한다.
useMemo, useCallback, useTransition 등은 의존성 배열을 기반으로 캐싱 동작이 수행되는데, 의존성 배열을 제대로 설정하지 않으면 필요한 업데이트가 발생하지 않거나 불필요한 재계산이 일어날 수 있어 주의해야한다.
동시성 기능은 내부적으로 작업 스케줄링을 관리하므로, 기존의 동기적 코드보다 디버깅이 더 어려워질 수 있다. (*React DevTools를 사용해 상태, 렌더링 과정 추적하는 것이 필요)
useTransition을 사용해 검색 입력은 즉시 반영하고, 필터링 작업이나 검색 결과 렌더링은 비긴급 작업으로 처리import React, { useState, useTransition } from "react";
function SearchComponent({ data }) {
const [searchQuery, setSearchQuery] = useState("");
const [filteredData, setFilteredData] = useState([]);
const [isPending, startTransition] = useTransition();
const handleInputChange = (e) => {
const query = e.target.value;
setSearchQuery(query);
// 비긴급 작업으로 데이터 필터링
startTransition(() => {
const filtered = data.filter((item) =>
item.toLowerCase().includes(query.toLowerCase())
);
setFilteredData(filtered);
});
};
return (
<div>
<input type="text" value={searchQuery} onChange={handleInputChange} />
{isPending && <p>Loading...</p>}
<ul>
{filteredData.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}useDeferredValue를 사용해, 입력값이 즉시 반영되지만, 렌더링은 지연되게 만들기import React, { useState, useDeferredValue } from "react";
function BigList({ items }) {
const [input, setInput] = useState("");
const deferredInput = useDeferredValue(input); // 입력값 렌더링 지연
const filteredItems = items.filter((item) =>
item.toLowerCase().includes(deferredInput.toLowerCase())
);
return (
<div>
<input type="text" value={input} onChange={(e) => setInput(e.target.value)} />
<ul>
{filteredItems.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
</div>
);
}“Concurrent Mode는 React가 UI를 더욱 부드럽고 효율적으로 렌더링할 수 있도록 도와주는 기능입니다.
기존의 동기 렌더링 방식과 달리, 렌더링을 중단하고 나중에 다시 실행할 수 있으며, 긴급한 업데이트를 우선적으로 처리할 수 있습니다.
이를 통해 사용자의 입력이 지연되지 않고, 렌더링 성능을 최적화할 수 있습니다.
React에서는 useTransition과 같은 훅을 제공하여, 중요한 업데이트(예: 사용자 입력)는 즉시 반영하고,
덜 중요한 작업(예: 데이터 로드)은 백그라운드에서 실행할 수 있습니다.
이러한 기능 덕분에, 복잡한 UI에서도 더욱 부드럽고 최적화된 사용자 경험을 제공할 수 있습니다."
“첫번째로 startTransition이란 기능을 이용하면 특정 상태 업데이트를 “덜 중요한 작업”으로 분류해서, 사용자가 클릭하거나 입력하는 반응 같은 중요한 업데이트가 우선적으로 처리 됩니다. 또 useDeferredValue라는 훅을 사용하면 값의 업데이트를 잠깐 지연시킬 수 있어서, 사용자가 뭔가 빠르게 입력할 때마다 리렌더링되지 않게 최적화할 수 있습니다.
동시성 모드의 장점은 사용자와 상호작용하는 부분이 훨씬 매끄럽게 느껴진다는 것입니다. 예를 들어, 사용자가 스크롤할 때 다른 무거운 작업이 있다 하더라도, 동시성 모드 덕분에 스크롤이 우선적으로 부드럽게 작동하게 만들 수 있습니다.”
“리액트의 동시성 기능(Concurrent Mode, useTransition, useDeferredValue 등)은 렌더링 성능을 최적화하고 부드러운 사용자 경험을 제공하지만, 적절히 사용하지 않으면 의도치 않은 동작이나 성능 저하가 발생할 수 있습니다. 그래서 필요한 부분에만 이 동시성 모드를 잘 활용하는 것이 중요하다고 볼 수 있습니다.”
“동시성이 필요한 상황은 주로 사용자와의 상호작용이 빈번하고 응답성이 중요한 경우입니다.
useDeferredValue를 사용해 데이터 필터링이나 리스트 렌더링을 지연시켜, 렌더링 성능을 최적화합니다.이 외에도 여러 API 호출과 같이 중요한 작업과 덜 중요한 작업을 분리해야 하는 다양한 상황에서 활용됩니다.
동시성 기능은 렌더링을 유연하게 관리해 사용자 경험을 크게 향상시킬 수 있습니다."
useTransition과 useDeferredValue의 차이점은 무엇인가요?"useTransition은 상태 업데이트를 백그라운드에서 실행하도록 스케줄링하는 반면, useDeferredValue는 기존 상태 값을 유지하면서 새로운 값의 렌더링을 지연시키는 방식입니다. useTransition은 상태 변경을 제어하고, useDeferredValue는 UI 업데이트 시점을 조절하는 역할을 합니다."
📚 참고자료