React 18에서 도입된 훅으로, 외부 상태 저장소(store)를 리액트 컴포넌트와 동기화하는 데 사용된다.
리액트의 Concurrent 기능과 외부 상태 라이브러리 간의 호환성 문제를 해결하는 것이다.
아래와 같은 상황에서 유용하게 사용된다.
1. React 외부의 상태 관리 라이브러리와 통합하는 경우
2 .브라우저 API와 같은 외부 데이터 소스를 구독하는 경우
💡 리액트의 동시성(Concurrent) 기능이란?
React 18에서 도입된 중요한 개선 사항으로, 렌더링 프로세스를 더 효울적으로 관리하고 사용자 경험을 향상시키는 것을 목표로 한다.
- 동시성 지원: React가 여러 작업을 동시에 처리할 수 있게 해준다.
- 중단 가능한 렌더링: 렌더링 과정을 작은 단위로 나누어 우선순위에 따라 처리할 수 있다.
- 사용자 경험 개선: UI의 반응성을 높이고, 대규모 데이터 처리나 복잡한 애니메이션에서도 부드러운 사용자 경험을 제공한다.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
컴포넌트의 최상위 레벨에서 useSyncExternalStore
를 호출하여 외부 데이터 저장소에서 값을 읽는다.
외부 스토어의 변화를 구독하는 함수이다.
// subscribe 예시 코드
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
// 클린 업(Clean-Up)
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
외부 스토어의 현재 상태의 스냅샷을 반환하는 함수이다.
// getSnapshot 예시 코드
function getSnapshot() {
return navigator.onLine;
}
서버 사이드 렌더링(SSR) 환경에서 사용되는 초기 스냅샷을 반환하는 함수이다.
// getServerSnapshot 예시 코드
function getServerSnapshot() {
return true; // 서버에서는 항상 온라인 상태로 가정
}
1. getSnapshot
함수는 성능을 위해 결과를 메모이제이션(캐시) 하는 것이 좋다.
let lastSnapshot = null;
let lastStoreState = null;
function getSnapshot() {
const storeState = store.getState();
if (storeState !== lastStoreState) { // 변경이 있다면
lastSnapshot = { ...storeState }; // 새로운 객체 생성
lastStoreState = storeState;
}
return lastSnapshot;
}
불변성을 지키면서 React가 변경 사항을 효율적으로 감지하고, 불필요한 렌더링을 방지할 수 있다.
2. useSyncExternalStore
가 반환한 스토어 값을 기반으로 렌더링을 일시 중단하는 것은 권장되지 않는다.
function MyComponent() {
const data = useSyncExternalStore(subscribe, getSnapshot);
const [asyncResult, setAsyncResult] = useState(null);
useEffect(() => {
// 외부 스토어 데이터를 기반으로 비동기 작업 수행
async function fetchData() {
const result = await someAsyncOperation(data);
setAsyncResult(result);
}
fetchData();
}, [data]);
// asyncResult를 사용하여 렌더링
}
외부 스토어 데이터를 기반으로 한 비동기 작업은 useEffect
와 같은 방식으로 처리하는 것이 좋다.
💡 Non-Blocking Transition이란?
React 18에서 도입된 개념으로, 사용자 인터페이스의 반응성을 향상시키는 기능이다.
- 중요한 업데이트와 덜 중요한 업데이트를 구분한다.
- 중요한 업데이트는 즉시 처리된다.
- 덜 중요한 업데이트(Transition)는 백그라운드에서 처리된다.
- 사용자 상호작용이 발생하면 진행 중이 Transition 업데이트를 중단하고 나중에 재개할 수 있다.
// 사용 예시 const [isPending, startTransition] = useTransition(); // 검색 기능: 입력 값 업데이트는 즉시, 검색 결과 필터링은 Transition으로 처리 const handleChange = (e) => { setInputValue(e.target.value); // 즉시 업데이트 startTransition(() => { setSearchResults(filterResults(e.target.value)); // 백그라운드에서 처리 }); };
아래 코드는 브라우저의 온라인 상태를 보여주는 코드이다.
import { useEffect, useCallback, useState } from 'react';
export function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(navigator.onLine);
const update = useCallback(() => {
setIsOnline(navigator.onLine);
}, [])
// ❌ 이벤트 핸들러가 등록되기 전에 외부 저장소 업데이트는 감지 못함
useEffect(() => {
window.addEventListener('online', update);
window.addEventListener('offline', update);
return () => {
window.removeEventListener('online', update);
window.removeEventListener('offline', update);
}
}, [update])
return isOnline;
}
이 코드의 문제점은 외부 저장소는 언제든지 업데이트가 될 수 있기 때문에 useState
와 useEffect
사이에 변경되었을 수도 있다는 것인데, 이벤트 핸들러가 나중에 등록되기 때문에 변경 사항을 감지할 수 없다.
useEffect
보다 실행시점이 빠른 useLayoutEffect
와 useInsertionEffect
를 사용해도 외부 저장소의 업데이트를 보장할 수 없다.
이 문제를 해결하려면 이벤트 핸들러가 등록된 후 update
함수를 호출하여 외부 스토어의 상태를 한번 더 확인해야 하지만, 여전히 위 코드에는 찢어짐(Tearing) 현상이 발생하고 있다.
그래픽 프로그래밍에서 전통적으로 시각적 불일치를 나타내는 데 사용되는 용어이다.
JavaScript는 단일 스레드이기 때문에 이 문제는 일반적인 웹 개발에서는 발생하지 않는다.
하지만 React 18에서는 동시 렌더링이 렌더링 중에 React가 양보하기 때문에 이 문제가 발생할 수 있다.
즉 startTransition
또는 Supense
와 같은 동시 기능을 사용할 때 React는 다른 작업을 수행하기 위해 일시 중지할 수 있다. 이러한 일시 중지 사이에 업데이트가 발생해 렌더링에 사용되는 데이터가 변경될 수 있으며, 이로 인해 동일한 UI 데이터에 대해 두 개의 다른 값이 표시될 수 있다.
💡 React 18의 동시성 렌더링(Concurrent Rendering)에서의 양보란?
- React 18은 렌더링 작업을 작은 단위로 나누어 수행한다. 각 단위 사이에 React는 브라우저에 제어권을 양보할 수 있다.
- React는 더 중요한 작업(ex 사용자 입력 처리)이 있는지 주기적으로 확인하여, 중요한 작업이 있으면 현제 렌더링을 일시 중지하고 그 작업을 처리한다.
- 이러한 양보 매커니즘 덕분에 React는 긴 렌더링 작업 중에도 UI의 응답성을 유지할 수 있다.
렌더링 중 외부 상태가 변경되면, 렌더링의 일부는 이전 상태를 나머지는 새 상태를 반영할 수 있다. 이를 찢어짐(Tearing)이라고 한다.
이 문제는 React에만 국한된 것이 아니라 동시성의 필연적인 결과이다. 사용자 입력에 응답하기 위해 렌더링을 중단할 수 있으려면 렌더링하는 데이터가 변경되어 UI가 찢어지는 것(Tearing)에 대한 복원력이 있어야 한다.
startTransition
(동시성 렌더링) 사용 코드let data = 1;
function getData() {
return data;
}
setTimeout(() => (data = 2), 100); // 100ms 후 데이터 변경
function Cell() {
const start = Date.now();
// 50ms 지연
while (Date.now() - start < 50) {
// 메인 스레드에게 제어권 양보
}
const data = getData();
return <div style={{ padding: "1rem", border: "1px solid red" }}>{data}</div>;
}
export default function Tearing() {
const [showCells, setShowCells] = useState(false);
// startTransition를 사용함으로써 동시성 렌더링을 사용한다.
useEffect(() => {
startTransition(() => setShowCells(true));
}, []);
return (
<>
<p>startTransition(동시성 렌더링) 사용 컴포넌트</p>
{showCells ? (
<div style={{ display: "flex", gap: "1rem" }}>
<Cell />
<Cell />
<Cell />
<Cell />
</div>
) : (
<p>준비 중..</p>
)}
</>
);
}
위 코드는 100ms 후에는 data
가 2로 변하고, 각 Cell 컴포넌트들은 50ms의 지연 후에 getData
를 호출하여 data
값을 가져온다.
startTransition
를 사용하여 동시성 렌더링을 하기 때문에 일부 Cell 컴포넌트는 1, 다른 Cell 컴포넌트는 2를 표시하는 찢어짐(Tearing) 현상이 발생한다.
이러한 찢어짐(Tearing) 현상을 해결하기 위해 useSyncExternalStore
를 사용할 수 있다.
// 첫 번째 인자 subscribe에는 일단 빈 함수 전달
const data = useSyncExternalStore(() => { return () => {} }, getData);
위와 같이 data
를 가져오는 로직에 useSyncExternalStore
를 사용하여 렌더링 도중 외부 스토어의 값이 변경되더라도 컴포넌트가 항상 동일한 데이터를 사용하도록 보장한다.
실제로 useSyncExternalStore
를 사용할 때는 첫 번째 인자(subscribe)에 외부 스토어의 변경을 감지하는 로직을 추가하여 데이터가 변경되면 컴포넌트가 자동으로 리렌더링될 수 있게 해주어야 한다!
useSyncExternalStore
를 사용하여 일관적인 UI 데이터 제공useSyncExternalStore
는 2가지 일을 한다.
외부 저장소의 모든 변경 사항이 감지되는지 확인한다.
동시 모드에서 UI의 동일한 저장소에 대한 동일한 데이터가 렌더링되는지 확인한다.
function mountSyncExternalStore<T>(
subscribe: (() => void) => () => void, // 스토어 변경을 구독하는 함수
getSnapshot: () => T, // 현재 스토어의 상태를 가져오는 함수
getServerSnapshot?: () => T // 서버 렌더링용 스냅샷 함수
): T {
const fiber = currentlyRenderingFiber;
const hook = mountWorkInProgressHook();
let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
if (getServerSnapshot === undefined) {
throw new Error(
'서버 렌더링된 콘텐츠에 필요한 getServerSnapshot이 누락되었습니다. 클라이언트 렌더링으로 되돌아갑니다.'
);
}
nextSnapshot = getServerSnapshot();
} else {
nextSnapshot = getSnapshot();
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'작업 중인 루트가 예상되었습니다. 이것은 React의 버그입니다. 문제를 보고해 주세요.'
);
}
const rootRenderLanes = getWorkInProgressRootRenderLanes();
if (!includesBlockingLane(root, rootRenderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
hook.memoizedState = nextSnapshot;
const inst: StoreInstance<T> = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
createEffectInstance(),
null
);
return nextSnapshot;
}
1. 서버 사이드 렌더링(SSR) 처리
getSeverSnapshot
이 없으면 에러를 발생시킨다.let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
if (getServerSnapshot === undefined) {
throw new Error(
'서버 렌더링된 콘텐츠에 필요한 getServerSnapshot이 누락되었습니다. 클라이언트 렌더링으로 되돌아갑니다.'
);
}
nextSnapshot = getServerSnapshot();
}
2. 클라이언트 렌더링 처리
nextSnapshot = getSnapshot();
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'작업 중인 루트가 예상되었습니다. 이것은 React의 버그입니다. 문제를 보고해 주세요.'
);
}
const rootRenderLanes = getWorkInProgressRootRenderLanes();
if (!includesBlockingLane(root, rootRenderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
3. 훅 상태 설정
hook.memoizedState = nextSnapshot;
const inst: StoreInstance<T> = {
value: nextSnapshot,
getSnapshot,
};
hook.queue = inst;
4. 구독(subscribe) 효과 설정
mountEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [subscribe]);
5. 업데이트 효과 설정
subscribe
, getSnapshot
또는 value
가 변경될 때 실행된다.fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
createEffectInstance(),
null
);
6. 결과 반환
return nextSnapshot;
1. mountSyncExternalStore
함수는 외부 스토어와 React 컴포넌트를 동기화한다.
2. 이 함수는 현재 스토어의 스냅샷을 가져오고, 스토어 변경을 구독하며 데이터 일관성을 유지하기 위한 효과들을 설정한다.
3. 서버 사이드 렌더링(SSR)과 클라이언트 렌더링 상황을 모두 고려하여 동시성 렌더링 환경에서도 안정적으로 외부 데이터를 관리할 수 있게 해준다.
이 함수는 복잡한 로직을 통해 외부 스토어의 데이터를 React의 렌더링 사이클과 효과적으로 통합하여, 일관된 UI 상태를 유지하는 것이 핵심 목적이다.
서버 사이드 렌더링(SSR)과 클라이언트 렌더링 상황에서의 스냅샷을 가져오는 로직은 mountSyncExternalStore
함수와 동일하다.
function updateSyncExternalStore<T>(
subscribe: (() => void) => () => void, // 스토어 변경을 구독하는 함수
getSnapshot: () => T, // 현재 스토어의 상태를 가져오는 함수
getServerSnapshot?: () => T // 서버 렌더링용 스냅샷 함수
): T {
const fiber = currentlyRenderingFiber; // 현재 Fiber
const hook = updateWorkInProgressHook(); // 업데이트 중인 훅
// 스냅샷을 가져오는 로직은 mountSyncExternalStore와 동일
let nextSnapshot;
const isHydrating = getIsHydrating();
if (isHydrating) {
if (getServerSnapshot === undefined) {
throw new Error(
'서버 렌더링된 콘텐츠에 필요한 getServerSnapshot이 없습니다. ' +
'클라이언트 렌더링으로 되돌아갑니다.'
);
}
nextSnapshot = getServerSnapshot();
} else {
nextSnapshot = getSnapshot();
}
const prevSnapshot = (currentHook || hook).memoizedState;
const snapshotChanged = !is(prevSnapshot, nextSnapshot);
if (snapshotChanged) {
hook.memoizedState = nextSnapshot;
markWorkInProgressReceivedUpdate();
}
const inst = hook.queue;
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
subscribe,
]);
if (
inst.getSnapshot !== getSnapshot ||
snapshotChanged ||
(workInProgressHook !== null &&
workInProgressHook.memoizedState.tag & HookHasEffect)
) {
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
createEffectInstance(),
null
);
const root: FiberRoot | null = getWorkInProgressRoot();
if (root === null) {
throw new Error(
'작업 중인 루트가 예상되었습니다. 이는 React의 버그입니다. 문제를 보고해 주세요.'
);
}
if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
}
return nextSnapshot;
}
1. 스냅샷 변경 감지
const prevSnapshot = (currentHook || hook).memoizedState;
const snapshotChanged = !is(prevSnapshot, nextSnapshot); // is함수는 Object.is 함수와 동일하다.
if (snapshotChanged) {
hook.memoizedState = nextSnapshot;
markWorkInProgressReceivedUpdate();
}
2. 구독 효과 업데이트
mountSyncExternalStore
에서는 mountEffect
를 사용했지만 여기서는 updateEffect
를 사용한다. const inst = hook.queue;
updateEffect(subscribeToStore.bind(null, fiber, inst, subscribe), [
subscribe,
]);
3. 조건부 효과 설정
조건 1) 스냅샷을 가져오는 함수가 변경되었을 때
조건 2) 스냅샷 값이 변경되었을 때
조건 3) 현재 작업중인 훅에 이미 효과(Effect)가 있을 때
이 조건들 중 하나라도 참이라면:
updateStoreInstance
함수를 사용하여 새 효과를 추가한다.이는 외부 스토어의 변경사항을 React 컴포넌트에 반영하기 위한 매커니즘이다.
if (
inst.getSnapshot !== getSnapshot ||
snapshotChanged ||
(workInProgressHook !== null &&
workInProgressHook.memoizedState.tag & HookHasEffect)
) {
fiber.flags |= PassiveEffect;
pushEffect(
HookHasEffect | HookPassive,
updateStoreInstance.bind(null, fiber, inst, nextSnapshot, getSnapshot),
createEffectInstance(),
null
);
}
4. 일관성 검사 예약
pushStoreConsistencyCheck
함수를 호출하여 일관성 검사를 예약한다.if (!isHydrating && !includesBlockingLane(root, renderLanes)) {
pushStoreConsistencyCheck(fiber, getSnapshot, nextSnapshot);
}
5. 결과 반환
return nextSnapshot;
1. 외부 스토어의 최신 스냅샷을 가져오고, 이전 스냅샷과 비교하여 변경 사항을 감지한다.
2. 변경이 감지되면 컴포넌트 상태를 업데이트하고, 스토어 구독(subscribe)을 위한 효과를 설정한다.
3. 필요한 경우 추가적인 일관성 검사를 예약하고, 최종적으로 최신 스냅샷을 반환하여 컴포넌트가 항상 최신 데이터로 렌더링되도록 보장한다.
이 함수는 React의 동시성 모드에서 외부 데이터 소스와 컴포넌트 상태를 효율적으로 동기화하는 핵심 매커니즘을 구현한다.
function pushStoreConsistencyCheck<T>(
fiber: Fiber, // 현재 작업중이 Fiber
getSnapshot: () => T, // 현재 스토어 상태를 가져오는 함수
renderedSnapshot: T // 렌더링에 사용된 스냅샷
): void {
fiber.flags |= StoreConsistency;
const check: StoreConsistencyCheck<T> = {
getSnapshot,
value: renderedSnapshot,
};
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue : any);
if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.stores = [check];
} else {
const stores = componentUpdateQueue.stores;
if (stores === null) {
componentUpdateQueue.stores = [check];
} else {
stores.push(check);
}
}
}
1. Fiber 플래그 설정
StoreConsistency
플래그를 추가한다.fiber.flags |= StoreConsistency;
2. 검사 객체 생성
const check: StoreConsistencyCheck<T> = {
getSnapshot, // 최신 스토어의 상태를 가져오는 함수
value: renderedSnapshot, // 렌더링에 사용된 스냅샷 값
};
3. 업데이트 큐 확인
let componentUpdateQueue: null | FunctionComponentUpdateQueue =
(currentlyRenderingFiber.updateQueue : any);
4. 업데이트 큐 생성 또는 업데이트
check
객체를 추가한다.stores
배열이 없으면 새로 생성한다.stores
배열이 있으면 check
객체를 추가한다.if (componentUpdateQueue === null) {
componentUpdateQueue = createFunctionComponentUpdateQueue();
currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
componentUpdateQueue.stores = [check];
} else {
const stores = componentUpdateQueue.stores;
if (stores === null) {
componentUpdateQueue.stores = [check];
} else {
stores.push(check);
}
}
1. 스토어 일관성 검사를 위한 정보를 컴포넌트의 업데이트 큐에 추가한다.
2. 이를 통해 React는 렌더링 과정에서 외부 스토어의 변경을 감지하고 적절히 대응할 수 있다.
3. 동시성 모드에서 데이터의 일관성을 유지하는 데 중요한 역할을 한다.
export function performConcurrentWorkOnRoot(
root: FiberRoot,
didTimeout: boolean
): RenderTaskFn | null {
if (enableProfilerTimer && enableProfilerNestedUpdatePhase) {
resetNestedUpdateFlag();
}
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
throw new Error('Should not already be working.');
}
const originalCallbackNode = root.callbackNode;
const didFlushPassiveEffects = flushPassiveEffects();
if (didFlushPassiveEffects) {
if (root.callbackNode !== originalCallbackNode) {
return null;
}
}
let lanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (lanes === NoLanes) {
return null;
}
const shouldTimeSlice =
!includesBlockingLane(root, lanes) &&
!includesExpiredLane(root, lanes) &&
(disableSchedulerTimeoutInWorkLoop || !didTimeout);
let exitStatus = shouldTimeSlice
? renderRootConcurrent(root, lanes)
: renderRootSync(root, lanes);
if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
if (exitStatus === RootDidNotComplete) {
markRootSuspended(root, lanes, NoLane);
} else {
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
exitStatus = renderRootSync(root, lanes);
renderWasConcurrent = false;
continue;
}
if (exitStatus === RootErrored) {
const lanesThatJustErrored = lanes;
const errorRetryLanes = getLanesToRetrySynchronouslyOnError(
root,
lanesThatJustErrored
);
if (errorRetryLanes !== NoLanes) {
lanes = errorRetryLanes;
exitStatus = recoverFromConcurrentError(
root,
lanesThatJustErrored,
errorRetryLanes
);
renderWasConcurrent = false;
if (exitStatus !== RootErrored) {
continue;
}
}
}
if (exitStatus === RootFatalErrored) {
prepareFreshStack(root, NoLanes);
markRootSuspended(root, lanes, NoLane);
break;
}
root.finishedWork = finishedWork;
root.finishedLanes = lanes;
finishConcurrentRender(root, exitStatus, finishedWork, lanes);
}
break;
} while (true);
}
ensureRootIsScheduled(root);
return getContinuationForRoot(root, originalCallbackNode);
}
렌더링 로직이 포함되어 있어 코드가 길지만 일관성 검사에 관련된 로직만 보면 그렇게 길지 않다.
if (exitStatus !== RootInProgress) {
let renderWasConcurrent = shouldTimeSlice;
do {
// ... 다른 코드 생략 ...
const finishedWork: Fiber = (root.current.alternate: any);
if (
renderWasConcurrent &&
!isRenderConsistentWithExternalStores(finishedWork)
) {
exitStatus = renderRootSync(root, lanes);
renderWasConcurrent = false;
continue;
}
// ... 다른 코드 생략 ...
} while (true);
}
1. 동시성 렌더링 확인
renderWasConcurrent
변수는 렌더링이 동시적으로 수행되었는지를 나타낸다.shouldTimeSlice
로 설정되며, 이는 시간 분할 렌더링을 사용했는지를 나타낸다.2. 일관성 검사 조건
renderWasConcurrent
가 true일 때만 일관성 검사를 수행한다.3. 일관성 검사 수행
isRenderConsistentWithExternalStores(finishedWork)
함수를 호출하여 실제 일관성 검사를 수행한다.finishedWork
)를 검사하여 외부 스토어와의 일관성을 확인한다.4. 불일치 발견 시 처리
isRenderConsistentWithExternalStores
함수가 false를 반환하여 불일치를 발견하면exitStatus = renderRootSync(root, lanes)
를 호출하여 동기적으로 다시 렌더링한다.renderWasConcurrent
를 false로 설정하여 다음 반복에서 일관성 검사를 건너뛰게 한다.continue
를 사용하여 루프를 계속하고 새로운 렌더링 결과를 처리한다.function isRenderConsistentWithExternalStores(finishedWork: Fiber): boolean {
let node: Fiber = finishedWork; // 검사할 노드 설정
while (true) { // Fiber 트리를 순회하기 위한 무한 루프
if (node.flags & StoreConsistency) { // StoreConsistency 플래그 체크
const updateQueue: FunctionComponentUpdateQueue | null =
(node.updateQueue: any);
if (updateQueue !== null) {
const checks = updateQueue.stores;
if (checks !== null) {
for (let i = 0; i < checks.length; i++) {
const check = checks[i];
const getSnapshot = check.getSnapshot;
const renderedValue = check.value;
try {
if (!is(getSnapshot(), renderedValue)) { // 현재 스토어 값과 렌더링 값 비교
return false; // 불일치 시 false
}
} catch (error) {
return false;
}
}
}
}
}
const child = node.child; // 현재 노드의 자식이 StoreConsistency 플래그가 있다면 자식 노드로 이동
if (node.subtreeFlags & StoreConsistency && child !== null) {
child.return = node;
node = child;
continue;
}
if (node === finishedWork) { // 시작 노드로 돌아왔다면 모든 검사 완료
return true;
}
while (node.sibling === null) { // 형제 노드로 이동
if (node.return === null || node.return === finishedWork) { // 루트에 도달하면
return true;
}
node = node.return; // 형제가 없으면 부모로 이동
}
node.sibling.return = node.return;
node = node.sibling;
}
return true;
}
이 함수는 Fiber 트리를 깊이 우선 탐색으로 순회하면서 모든 StoreConsistency
플래그가 설정된 노드에 대해 외부 스토어와의 일관성 검사를 한다.
하나라도 불일치하는 경우 즉시 false를 반환하고, 모든 검사를 통과하면 true를 반환한다.
이를 통해 React는 동시성 모드에서 렌더링 결과가 외부 스토어와의 최신 상태와 일치하는지 확인할 수 있다.
pushStoreConsistencyCheck()
- 플래그 설정
StoreConsistency
플래그를 설정하여 일관성 검사가 필요하다는걸 알린다.updateQueue
에 저장한다.performConcurrentWorkOnRoot()
- 일관성 검사 체크, 호출
isRenderConsistentWithExternalStores
함수를 호출한다.isRenderConsistentWithExternalStores()
- 일관성 검사
StoreConsistency
플래그가 설정된 노드를 찾는다.getSnapshot
함수를 호출하여 현재 스토어 값을 가져오고, 이를 렌더링된 값과 비교한다.false
를 반환하여 리렌더링이 필요함을 알린다.1. useSyncExternalStore
는 렌더링이 완료되고 커밋이 시작되기 전에 일관성 검사를 예약함으로써 찢어짐(Tearing) 문제를 해결한다. 불일치한 데이터가 UI에 그려지는 것을 방지하기 위해 동기 모드에서 강제로 재렌더링을 수행한다.
2. 외부 스토어의 변경을 감지하기 위한 매커니즘을 포함한다. 변경 사항이 있으면 동기 모드에서 리렌더링이 예약된다. 이는 커밋 이후에 발생하므로 사용자는 UI가 깜박이는 것을 볼 수 있다.
💡 동시성 모드와 동기 모드
- 동시성 모드: React가 렌더링을 중단하고 더 중요한 업데이트를 처리할 수 있는 모드이다.
- 동기 모드: React가 렌더링 작업을 한 번에 완료하는 방식이다. 렌더링이 시작되면 멈추지 않고 끝까지 진행된다.
useSyncExternalStore - 리액트 공식 문서
How useSyncExternalStore() works internally in React?
React 18 useSyncExternalStore에 대해서
How to use useSyncExternalStore in React 18
What is tearing?
좋은 글 감사합니다 잘 읽고 있어요 🔥🔥🔥