useSyncExternalStore
은 외부 저장소를 구독할 수 있도록 만들어주는 리액트 훅이에요.
const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
Reference | 레퍼런스
useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
외부 데이터 저장소의 값을 읽어오고 싶다면 최상위 컴포넌트에서 useSyncExternalStore
을 호출하세요.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
저장소에 있는 데이터의 스냅샷을 반환해요. 인자로는 두 개의 함수를 전달해야해요.
subscribe
함수는 데이터를 구독하고 구독을 취소하는 함수를 반환해요.getSnapshot
함수는 저장소에서 데이터의 스냅샷을 읽어와요.
Parameters | 파라미터
subscribe
: 단일callback
인자를 받아서 저장소에 구독하는 함수. 저장소가 변경되면 받은callback
을 호출해요. 그러면 컴포넌트가 리렌더링돼요.subscribe
함수는 구독을 해제하는 함수를 반환해요.getSnapshot
: 저장소에서 컴포넌트가 필요로하는 데이터의 스냅샷을 반환하는 함수. 저장소가 변경되지 않는다면getSnapshot
은 계속 같은 값을 반환해요. 만약 저장소가 변경되고 (Object.js
로 비교했을 때) 반환값이 바뀌었다면 리액트는 컴포넌트를 리렌더링해요.getServerSnapshot
(선택적) : 저장소 내부 데이터의 초기 스냅샷을 반환하는 함수. 서버 렌더링과 서버에서 렌더링 된 콘텐츠를 클라이언트에 하이드레이션 시킬 때만 사용해요. 서버 스냅샷은 클라이언트와 서버 모두에서 동일해야만해요. 또한 보통은 직렬화되어있으며, 서버에서 클라이언트로 전달돼요. 만약 이 인자를 생략한다면 서버에서 컴포넌트를 렌더링할 때 에러를 던질 거예요.
Returns | 반환값
렌더링 로직에서 사용할 수 있는 저장소의 현재 스냅샷.
Caveats | 주의사항
getSnapshot
에서 반환된 저장소 스냅샷은 불변해야해요. 만약 저장소가 가변적인 데이터를 갖고 있다면, 데이터가 변경될 때 새로운 불변 스냅샷을 반환하세요. 그렇지 않다면 캐싱되어있는 마지막 스냅샷을 반환하세요.- 만약 다른
subscribe
함수가 렌더링 동안 전달된다면, 리액트는 새롭게 전달된subscribe
함수를 사용해서 저장소를 재구독해요.subscribe
를 컴포넌트 외부에서 선언한다면 이 동작을 방지할 수 있어요. - 만약 논블로킹 전환 업데이트가 진행되는 동안 저장소가 변경되었다면 리액트는 해당 업데이트를 블로킹 업데이트처럼 수행해요. 더 구체적으로 말하자면 모든 전환 업데이트에서 리액트는
getSnapshot
을 DOM에 변경사항을 반영하기 직전에 한 번 더 호출해요. 만약 일전에 호출했을 때 받은 값과 다른 값을 반환받는다면 리액트는 화면에 보이는 모든 컴포넌트는 동일한 버전의 저장소를 반영하고 있다는 것을 보장하기 위하여 스크래치에서 블로킹 업데이트로 이 업데이트를 재시작해요. useSyncExternalStore
에서 반환된 저장소의 값에 따라렌더링을 유예하는 것은 추천하지 않아요. 외부 저장소를 변경하는 것은 논블로킹 전환 업데이트로 표시되지 않기 때문에 이미 화면에 렌더링된 콘텐츠를 대체하는 가장 가까운Suspense
폴백을 로딩 스피너로 대체하며 이는 사용자 경험(UX)를 저하시켜요.
예를 들어, 아래와 같은 형식은 권장하지 않아요.
const LazyProductDetailPage = lazy(() => import('./ProductDetailPage.js'));
function ShoppingApp() {
const selectedProductId = useSyncExternalStore(...);
// ❌ `selectedProductId`에 의존적인 프로미스로 `use` 호출하기
const data = use(fetchItem(selectedProductId))
// ❌ `selectedProductId`에 기반하여 조건적으로 레이지 컴포넌트 렌더링하기
return selectedProductId != null ? <LazyProductDetailPage /> : <FeaturedProducts />;
}
Usage | 용법
Subscribing to an external store | 외부 저장소 구독하기
대부분의 리액트 컴포넌트는 props, 상태 그리고 컨텍스트에서만 데이터를 읽어와요. 그러나 시간이 지나면 변하기도 하는 리액트 외부에 있는 저장소에서 데이터를 읽어와야해요. 이는 아래를 포함하고 있어요.
- 리액트의 서드 파티 상태 관리 라이브러리
- 변하는 값을 노출하고 해당 변화에 이벤트를 구독하는 브라우저 API
외부 데이터 저장소의 값을 읽어오고 싶다면 최상위 컴포넌트에서 useSyncExternalStore
을 호출하세요.
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';
function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}
저장소에 있는 데이터의 스냅샷을 반환해요. 인자로는 두 개의 함수를 전달해야해요.
subscribe
함수는 데이터를 구독하고 구독을 취소하는 함수를 반환해요.getSnapshot
함수는 저장소에서 데이터의 스냅샷을 읽어와요.
리액트는 이 함수들을 사용하여 컴포넌트에 저장소를 구독시키고 변경사항이 있을 때 컴포넌트를 리렌더링해요.
이 예시에서 todoStore
는 리액트 외부의 데이터를 저장하는 외부 저장소로 구현되었어요. TodosApp
컴포넌트는 useSyncExternalStore
훅을 사용하여 외부 저장소와 연결해요.
NOTE
가능하다면useState
와useReducer
와 같은 내장 리액트 상태를 사용하는 것을 추천해요.useSyncExternalStore
API는 주로 기존에 있던 리액트가 아닌 코드는 통합해야할 때 유용해요.
Subscribing to a browser API | 브라우저 API 구독하기
useSyncExternalStore
를 추가하는 또 다른 이유로는 시간이 지나면 변하는 브라우저에 노출된 값을 구독하고 싶을 때가 있어요. 예를 들어 컴포넌트가 네트워크 연결이 활성화되어있는지를 표시하고 있다고 가정해볼게요. 브라우저는 navigator.onLine
이라는 속성을 통해 이 정보를 보여줘요.
이 값은 리액트가 알아차리지 못하여 변경할 수 있기 때문에 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);
};
}
이제 리액트는 외부 navigator.onLine
API에서 값을 읽는 방법과 해당 API의 변화를 구독하는 방법을 알아요. 네트워크 연결을 해제하고 컴포넌트가 이에 반응하여 리렌더링 되는지 살펴보세요.
Extracting the logic to a custom Hook | 커스텀 훅으로 로직 추출하기
보통은 useSyncExternalStore
를 컴포넌트 안에서 직접 사용하진 않을 거예요. 대신 일반적으로는 커스텀 훅에서 호출할 거예요. 이렇게 하면 다른 컴포넌트에서 동일한 외부 저장소를 사용할 수 있어요.
예를 들어, 커스텀 훅인 useOnlineStatus
훅은 네트워크가 온라인 상태인지를 추적해요.
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}
function getSnapshot() {
// ...
}
function subscribe(callback) {
// ...
}
이제 다른 컴포넌트들은 기본 구현을 반복하지 않고 useOnlineStatus
를 호출할 수 있어요.
Adding support for server rendering | 서버 렌더링을 위한 지원 추가하기
만약 당신의 리액트 앱이 서버 렌더링을 사용한다면 리액트 컴포넌트는 초기 HTML을 생성하기 위해 브라우저 환경 바깥에서 작동해요. 이 상황에서 외부 저장소와 연결하면 몇 가지 문제가 생겨요.
- 만약 브라우저 전용 API에 연결하려면 서버에는 이 API가 존재하지 않기 때문에 작동하지 않을 거예요.
- 만약 서드 파티 데이터 저장소에 연결하려면 서버와 클라이언트의 데이터가 서로 일치해야해요.
이 이슈들을 해결하려면 useSyncExternalStore
에 getServerSnapshot
함수를 3번쨰 인자로 넘기세요.
import { useSyncExternalStore } from 'react';
export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot);
return isOnline;
}
function getSnapshot() {
return navigator.onLine;
}
function getServerSnapshot() {
return true; // 서버에서 생성된 HTML은 "Online"을 항상 보여줘요.
}
function subscribe(callback) {
// ...
}
getServerSnapshot
함수는 getSnapshot
과 비슷하지만 두 가지 상황에서만 작동해요.
- HTML을 생성할 때, 서버에서만 작동해요.
- 리액트가 서버 HTML을 가져와 상호 작용이 가능하도록 만들어주는 하이드레이션을 하는 동안에 클라이언트에서 작동해요.
엡에서 상호 작용이 되기 전에 사용되는 초기 스냅샷 값을 제공하도록 해줘요. 만약 서버 렌더링에 유의미한 초기 데이터가 없다면 클라이언트에서 렌더링을 강제하기 위해 이 인자를 생략하세요.
노트getServerSnapshot
이 서버에서 반환된 것과 동일한 초기 클라이언트 렌더링에서 데이터를 반환해야해요. 예를 들어getServerSnapshot
이 서버에서 미리 채워진 저장소 콘텐츠를 반환했다면 이 콘텐츠를 클라이언트로 전환해야해요. 전환 방법 중 하나는 서버 렌더링동안window.MY_STORE_DATA
와 같은 전역 변수를 설정하는<script>
태그를 방출하여 클라이언트에서는getServerSnapshot
안의 전역 변수에서 읽어들이는 거예요. 외부 저장소는 이 작업을 하는 방법에 대한 안내를 제공해야해요.
Troubleshooting | 트러블슈팅
I'm getting an error : "The result of getSnapshot
should be cached" | "getSnapshot
의 결과가 캐싱되어야해요." 라는 에러가 떠요
이 에러는 getSnapshot
함수가 호출될 때마다 새로운 객체 배열을 반환한다는 것을 의미해요.
function getSnapshot() {
// 🔴 getSnapshot에서 항상 다른 객체를 반환하지 마세요.
return {
todos: myStore.todos
};
}
리액트는 getSnapshot
의 반환값이 이전과 달라졌다면 컴포넌트를 리렌더링해요. 항상 다른 값을 반환한다면 무한 반복에 빠지고 이 에러가 발생하는 이유가 바로 이 때문이에요.
getSnapshot
객체는 실제로 무언가가 변했을 때만 다른 객체를 반환해야해요. 만약 저장소가 변할 수 없는 데이터를 포함한다면 해당 데이터를 직접 반환할 수도 있어요.
function getSnapshot() {
// ✅ 변할 수 없는 데이터를 반환할 수 있어요.
return myStore.todos;
}
만약 저장소 데이터가 가변적이라면 getSnapshot
함수는 그 데이터의 불변한 데이터가 반환되어야해요. 이는 새 객체를 생성해야하지만 모든 단일 호출마다 이 행동을 할 필요가 없음을 의미해요. 대신, 가장 마지막에 계산된 스냅샷을 저장하고 저장소 안의 데이터가 변하지 않았다면 마지막과 동일한 스냅샷을 반환해요. 가변적인 데이터가 실제로 변했는지를 결정하는 방법은 가변적인 저장소에 의존해요.
My subscribe
function gets called after every re-render | subscribe
함수가 리렌더링이 이루어질 때마다 호출돼요
이 subscribe
함수는 컴포넌트 내부에서 정의되었기 때문에 매 리렌더링마다 달라져요.
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// 🚩 항상 다른 함수가 되기 때문에 리액트는 매 렌더링마다 재구독해요
function subscribe() {
// ...
}
// ...
}
렌더링이 진행될 때마다 다른 subscribe
함수를 전달한다면 리액트는 저장소를 재구독해요. 이 동작이 성능 문제를 발생시키고 재구독을 피하고 싶다면 subscribe
함수를 외부로 옮기세요.
function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}
// ✅ Always the same function, so React won't need to resubscribe
function subscribe() {
// ...
}
다른 방법으로는 일부 인자가 수정될 때만 재구독하기 위해 useCallback
으로 subscribe
를 감싸는 방법이 있어요.
function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ✅ userId가 변하지 않는 한 같은 함수에요.
const subscribe = useCallback(() => {
// ...
}, [userId]);
// ...
}
'리액트 공식문서 | React Docs > Reference > react@18.2.0' 카테고리의 다른 글
[Hooks] useTransition | useTransition 훅 (1) | 2024.02.07 |
---|---|
[Hooks] useState | useState 훅 (1) | 2024.02.06 |
[Hooks] useRef | useRef 훅 (0) | 2024.02.04 |
[Hooks] useReducer | useReducer 훅 (1) | 2024.02.04 |
[Hooks] useOptimistic | useOptimistic 훅 (0) | 2024.02.03 |