외부 store를 구독할 수 있는 React Hook
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
컴포넌트의 최상위 레벨에서 useSyncExternalStore를 호출하여 외부 데이터 저장소에서 값을 읽음.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
Store에 있는 데이터의 스냅샷을 반환함. 두 개의 함수를 인수로 전달해야 함:
subscribe 함수는 store를 구독하고, 구독을 취소하는 함수를 반환해야 함.getSnapshot 함수는 store에서 데이터의 스냅샷을 읽어야 함.subscribe: 단일 callback 인수를 받아 store에 구독하는 함수. Store가 변경되면 제공된 callback을 호출해야함. 그러면 컴포넌트가 다시 렌더링됨. subscribe 함수는 구독을 정리하는 함수를 반환해야함.
getSnapshot: 컴포넌트에 필요한 store 데이터의 스냅샷을 반환하는 함수. Store가 변경되지 않은 상태에서 getSnapshot을 반복적으로 호출하면 동일한 값을 반환해야함. Store가 변경되고 반환된 값이 다른 경우(Object.is에 의해 비교됨), React는 컴포넌트를 다시 렌더링함.
getServerSnapshot (optional): Store에 있는 데이터의 초기 스냅샷을 반환하는 함수. 서버 렌더링 도중과 클라이언트에서 서버 렌더링된 콘텐츠의 hydration 중에만 사용됨. 서버 스냅샷은 클라이언트와 서버 간에 동일해야 하며, 일반적으로 직렬화되어 서버에서 클라이언트로 전달됨. 이 인수를 생략하면 서버에서 컴포넌트를 렌더링할 때 오류가 발생함.
렌더링 로직에 사용할 수 있는 store의 현재 스냅샷을 반환함.
getSnapshot이 반환하는 store 스냅샷은 변경 불가능 해야함. 기본 store에 변경 가능한 데이터가 있는 경우, 데이터가 변경되었을 때 변경 불가능한 새 스냅샷을 반환함. 그렇지 않으면 캐시된 마지막 스냅샷을 반환함.
다시 렌더링하는 동안 다른 subscribe 함수가 전달되면 React는 새로 전달된 subscribe 함수를 사용하여 저장소를 다시 구독함. 컴포넌트 외부에서 subscribe을 선언하면 이를 방지할 수 있음.
Non-blocking transition update 중에 store가 변경되면 React는 해당 업데이트를 블로킹으로 수행하도록 되돌아감. 구체적으로, 모든 트랜지션 업데이트에 대해 React는 DOM에 변경 사항을 적용하기 직전에 getSnapshot을 한 번 더 호출함. 처음 호출했을 때와 다른 값을 반환하면 React는 업데이트를 처음부터 다시 시작하고 이번에는 블로킹 업데이트로 적용하여 화면의 모든 컴포넌트가 동일한 버전의 store를 반영하도록 함.
useSyncExternalStore가 반환한 store 값을 기반으로 렌더링을 일시 중단하는 것은 권장하지 않음. 그 이유는 외부 store에 대한 변형을 non-blocking transition update로 표시할 수 없기 때문에 가장 가까운 Suspense fallback을 트리거하여 화면에 이미 렌더링된 콘텐츠를 로딩 스피너로 대체하게 되고, 이는 일반적으로 좋지 않은 UX이기 때문.
예를 들어, 다음과 같은 코드는 권장되지 않음:
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));
function ShoppingApp() {
const selectedProductId = useSyncExternalStore(...);
// ❌ Calling `use` with a Promise dependent on `selectedProductId`
const data = use(fetchItem(selectedProductId))
// ❌ Conditionally rendering a lazy component based on `selectedProductId`
return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;
}
대부분의 React 컴포넌트는 props, state, context에서만 데이터를 읽음. 그러나 때로는 컴포넌트가 시간이 지남에 따라 변경되는 React 외부의 저장소에서 일부 데이터를 읽어야 할 때가 있음. 예를 들면:
컴포넌트의 최상위 수준에서 useSyncExternalStore를 호출하여 외부 데이터 저장소에서 값을 읽음:
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
이 함수는 스토어에 있는 데이터의 스냅샷을 반환하고, 두 개의 함수를 인수로 받음:
subscribe 함수는 스토어를 구독하고, 구독을 취소하는 함수를 반환해야함.getSnapshot 함수는 스토어에서 데이터의 스냅샷을 읽어야함.React는 이 함수를 사용하여 컴포넌트를 스토어에 구독한 상태로 유지하고 변경 사항이 있을 때 다시 렌더링함.
예를 들어, 아래 에제에서 todosStore는 React 외부에 데이터를 저장하는 외부 store로 구현되어 있음. TodosApp 컴포넌트는 useSyncExternalStore Hook으로 해당 외부 스토어에 연결됨.
// App.js
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
export default function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
return (
<>
<button onClick={() => todosStore.addTodo()}>Add todo</button>
<hr />
<ul>
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
</ul>
</>
);
}
// todoStore.js
// This is an example of a third-party store
// that you might need to integrate with React.
// If your app is fully built with React,
// we recommend using React state instead.
let nextId = 0;
let todos = [{ id: nextId++, text: 'Todo #1' }];
let listeners = [];
export const todosStore = {
addTodo() {
todos = [...todos, { id: nextId++, text: 'Todo #' + nextId }]
emitChange();
},
subscribe(listener) {
listeners = [...listeners, listener];
return () => {
listeners = listeners.filter(l => l !== listener);
};
},
getSnapshot() {
return todos;
}
};
function emitChange() {
for (let listener of listeners) {
listener();
}
}
Note
가능하면 내장된 React state를
useState및useReducer와 함께 사용하는 것이 좋음.useSyncExternalStoreAPI는 기존의 비 React 코드와 통합해야 할 때 주로 유용함.
useSyncExternalStore를 추가해야 하는 또 다른 상황은 시간이 지남에 따라 변경되는 브라우저의 값을 구독하려는 경우임. 예를 들어, 컴포넌트에 네트워크 연결이 활성 상태인지 여부를 표시하고 싶다면, 브라우저가 navigator.onLine이라는 속성을 통해 제공하는 정보를 사용해야함.
이 값은 React가 모르는 사이에 변경될 수 있으므로,useSyncExternalStore로 이 값을 읽어야함.
import { useSyncExternalStore } from 'react';
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
getSnapshot 함수를 구현하려면 브라우저 API에서 현재 값을 읽어야 함:
function getSnapshot() {
return navigator.onLine;
}
다음으로 subscribe 함수를 구현해야함. 예를 들어 navigator.onLine이 변경되면 브라우저는 window 개체에서 online 및 offline 이벤트를 실행함. callback 인수를 해당 이벤트에 구독한 다음 구독을 정리하는 함수를 반환해야함:
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
이제 React는 외부 navigator.onLine API에서 값을 읽는 방법과 그 변경 사항을 구독하는 방법을 알고 있음. 디바이스에서 네트워크의 연결이 끊어지면 컴포넌트가 response로 다시 렌더링되는 것을 확인할 수 있음:
import { useSyncExternalStore } from 'react';
export default function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}
일반적으로 컴포넌트에서 직접 useSyncExternalStore를 작성하지는 않음. 대신 일반적으로 사용자 정의 Hook에서 호출함. 이렇게 하면 서로 다른 컴포넌트에서 동일한 외부 저장소를 사용할 수 있음.
예를 들어, 이 사용자 정의 useOnlineStatus Hook은 네트워크가 온라인 상태인지 여부를 추적함:
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
이제 다른 컴포넌트에서 기본 구현을 반복하지 않고도 useOnlineStatus를 호출할 수 있음:
// App.js
import { useOnlineStatus } from './useOnlineStatus.js';
function StatusBar() {
const isOnline = useOnlineStatus();
return <h1>{isOnline ? '✅ Online' : '❌ Disconnected'}</h1>;
}
function SaveButton() {
const isOnline = useOnlineStatus();
function handleSaveClick() {
console.log('✅ Progress saved');
}
return (
<button disabled={!isOnline} onClick={handleSaveClick}>
{isOnline ? 'Save progress' : 'Reconnecting...'}
</button>
);
}
export default function App() {
return (
<>
<SaveButton />
<StatusBar />
</>
);
}
// useOnlineStatus.js
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function subscribe(callback) {
window.addEventListener('online', callback);
window.addEventListener('offline', callback);
return () => {
window.removeEventListener('online', callback);
window.removeEventListener('offline', callback);
};
}