리액트 동시성 모드 (Concurrent Mode) 에 대해서 설명해주세요
리액트 동시성 모드는 처음들어본것 같다. 기능을 들으면 알 것 같기도한데. 기본적으로 낯선 단어인듯..?
리액트의 동시성 모드는 여러 작업을 비동기적으로 동시에 처리하면서도 중간에 더 중요한 작업이 들어오면 우선순위를 바꿔서 그 작업을 먼저 처리하는 기능을 의미한다. 예전 리액트는 스택 구조로 이루어졌기에 한번 렌더링을 시작하면 끝까지 멈추지 않고 다 처리해야 했다. 하지만 동시성 모드가 생기면서 중간에 멈추거나 작업을 잠시 뒤로 미뤄둘 수 있게 되어서 중요 작업을 먼저 끝낼 수 있게 되었다.
이 동시성을 활용하여 리액트는 중요한 작업과 덜 중요한 작업을 나눠서, 덜 중요한 작업은 백그라운드에서 진행하고 중요한 부분은 바로 사용자에게 보여준다.
예를들어 검색창에 뭔가를 입력하고 있을 때, 그에 맞추어 검색결과가 업데이트되더라도 리액트가 해당 작업을 백그라운드에서 처리하여 화면이 느려지지 않게 해준다
동시성을 활용한 기능
첫번째로 startTranstition 이라는 기능을 이용하면 특정 상태 업데이트를 '덜 중요한 작업'으로 분류해서, 사용자가 클릭하거나 입력하는 반응 같은 중요한 업데이트가 우선적으로 처리 된다. 또 useDeferredValue 라는 훅을 사용하면 값의 업데이트를 잠깐 지연시킬 수 있어서, 사용자가 뭔가 빠르게 입력할 때마다 리렌더링되지 않게 최적화할 수 있다.
동시성 모드의 장점은 사용자와 상호작용하는 부분이 훨씬 매끄럽게 느껴진다는 것이다. 예를 들어, 사용자가 스크롤할 때 다른 무거운 작업이 있다 하더라도, 동시성 모드 덕분에 스크롤이 우선적으로 부드럽게 작동하게 만들 수 있다.
동시성 기능을 활용할때 주의할 점
모든 컴포넌트에 이 동시성 모드를 무분별하게 적용하면 오히려 성능이 떨어질 수 있다는 점이다. 필요한 부분에만 이 동시성 모드를 잘 활용하는 것이 중요하다.
동시성이 필요한 때는 언제인가
동시성이 필요한 상황은 주로 사용자와의 상호작용이 빈번하고 응답성이 중요한 경우이다.
첫번째 예로, 검색 필터링이나 자동 완성과 같은 기능이다. 사용자가 검색어를 입력할 때마다 결과가 업데이트되는 경우, 모든 입력마다 화면이 리렌더링된다면 앱이 느려지고 입력할 때마다 끊김을 느낄 수 있다. 이때 동시성 모드를 사용하면 검색어 입력 자체가 더 중요한 작업이 되어 검색 결과 업데이트는 백그라운드에서 처리되므로, 입력이 빠르고 부드럽게 유지된다.
두번째로, 무거운 데이터나 리스트를 로딩하는 경우이다. 긴 스크롤 목록을 보면서 네트워크를 통해 데이터를 로딩할 때, 새로운 항목을 추가로 불러오는 작업보다 사용자가 현재 보고 있는 화면의 스크롤이 더 중요한 작업이다. 이때 동시성을 사용하면 로딩은 백그라운드로 넘기고, 스크롤을 최우선으로 부드럽게 렌더링할 수 있다.
또한, 애니메이션이 포함된 화면 전환이나 중요도가 높은 사용자 입력 작업도 동시성을 고려할 만한 케이스이다. 사용자가 버튼을 클릭했을 때 UI가 즉각적으로 반응하고, 이후에 비동기 작업이 처리되도록 설정해 주면 클릭 시의 지연 없이 상호작용이 자연스러워진다.
리액트 동시성 모드 라는 용어는 이전 실험적 개념이며 현재는 동시성 기능 이라고 부른다.
React 18 버전 이후
주요 동시성 기능
startTransition
상태 업데이트를 “덜 중요한 업데이트”로 지정 가능.
예: 검색창 입력은 즉각 반영, 결과 필터링은 background에서.
useDeferredValue
특정 값 업데이트를 지연시켜서 빠른 입력/스크롤에 영향을 안 주도록 함.
예: 리스트 필터링 값.
useTransition
로딩 중 상태 관리 가능 (isPending 제공).
React 18 자동 기능들
Suspense와 결합 시 자연스럽게 비동기 UI 제어.
useId도 동시성 안전성 보장.
Suspense 를 통해 동시성 기능을 달성하는 경우가 많았던 것 같다. 자동으로 데이터를 기다렸다가 렌더링이 되는 과정 자체가 동시성이라고 볼 수 있겠다.
또한 input 에서 자동으로 리액트가 onChange 를 최 우선으로 하기 때문에 입력이 끊기지는 않지만, 입력에 따라 파생되는 연산인 검색등에는 사용할 필요가 있다. 나는 디바운스를 통해서 해결하는 방식을 썼는데 새로운 방식을 고려해보자
예시
검색창 입력 + 디바운스
const [query, setQuery] = useState('');
const debouncedQuery = useDebounce(query, 300);
const { data } = useQuery({
queryKey: ['search', debouncedQuery],
queryFn: () => fetchSearch(debouncedQuery),
});
useDeferredValue로 렌더링 최적화
const deferredQuery = useDeferredValue(query);
const { data } = useQuery({
queryKey: ['search', deferredQuery],
queryFn: () => fetchSearch(deferredQuery),
});
startTransition으로 데이터 렌더링 늦추기
const [query, setQuery] = useState('');
const [search, setSearch] = useState('');
const { data } = useQuery({
queryKey: ['search', search],
queryFn: () => fetchSearch(search),
});
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setQuery(e.target.value);
startTransition(() => setSearch(e.target.value));
};