# useSyncExternalStore

## [useSyncExternalStore](https://react.dev/reference/react/useSyncExternalStore)

> 외부 스토어를 구독할 수 있는 React Hook

React는 기본적으로 관심사 밖에서 동작하는 External Store의 상태 흐름을 관찰하지 않는다.

그러나 **useSyncExternalStore** 훅을 사용하면 External Store를 구독(관찰)하여 상태 변화에 대한 알림을 제공 받는다.

### 해당 훅이 나오게된 이유

* [`concurrent feature`](#concurrent-feature)의 [tearing](#tearing) 현상을 해결하기 위해 필요하다.
* Extenral Store의 변경사항을 구독하고, `tearing`이 발생하지 않도록 Extenral Store의 변경사항을 감지하여 리렌더링을 시켜주는 역할을 한다.&#x20;

### 작동 방식

```tsx
function useSyncExternalStore<Snapshot>(
  subscribe: (onStoreChange: () => void) => () => void,
  getSnapshot: () => Snapshot,
  getServerSnapshot?: () => Snapshot
}: Snapshot
```

#### **subscribe**&#x20;

> 스토어에 변경사항을 구독하기 위한 콜백함수

* 전달된 콜백함수를 통해 React에게 스토어가 변경되었으므로 리렌더링이 필요하다고 알린다.

{% hint style="info" %}
함수 컴포넌트는 매 렌더링마다 내부 함수를 다시 선언하게 되어서, 구독 함수도 매 렌더링마다 달라지게 되는데 이로 인해 리액트는 새로 전달된 구독 함수로 스토어를 다시 구독하는 side-effect가 발생하게 된다.

이러한 불필요한 스토어 구독을 방지하기 위해서는 useCallback을 사용하여 함수를 메모이제이션하거나, 클래스를 사용하여 항상 같은 메서드를 참조하도록 만드는 방법이 있다.
{% endhint %}

#### **getSnapshot**

> 스토어의 값을 반환하는 콜백함수
>
> React가 스토어에서 값을 가져오는 방법을 제공한다.

* **특정 시점의 스토어의 상태**를 가져오기 떄문에 snapshot이란 네이밍을 사용한다.
* `subscribe`에 등록한 콜백함수가 호출될 때마다 리엑트는 getSnapshot 콜백함수를 호출하여 최신 스토어 상태를 가져오고 리렌더링 시킨다.
* 리액트는 컴포넌트가 렌더링이 시작될 때 getSnapshot 함수를 호출하여 반환된 값(스토어의 상태)를 캐싱하고, 렌더링 이후 스토어가 변경되지 않았는지 확인하기 위해 한번 더 호출해서 이전에 캐시한 스토어 상태와 비교해 다른 경우에만 컴포넌트를 다시 리렌더링한다.
* 즉, 렌더링이 시작될 때와 렌더링 이후에 한 번더 getSanpshot을 호출하여 최신 스토어 상태와 Sync(동기화)시킨다.

{% hint style="info" %}
스토어의 상태를 캐싱할 때는 값을 복사하는 대신 그대로 저장하며, 이전 스토어의 상태와 비교할 때는 얕은 비교(Object.is)를 사용한다.

따라서 스토어의 상태를 불변(immutable)하게 유지해야 리엑트가 스토어의 변경사항을 감지할 수 있다.
{% endhint %}

{% hint style="warning" %}
getSnapshot 콜백 함수는 원본 데이터를 반환해야 한다.&#x20;

만약 항상 새로운 값을 반환한다면 무한 리렌더링이 발생할 수 있으므로 주의

```tsx
// getSnapshot이 항상 새로운 객체를 반환하므로 무한 리렌더링 발생
const snapshot = useSyncExternalStore(subscribe, () => ({newValue: store.value}))
```

{% endhint %}

#### **getServerSnapshot**&#x20;

> 옵셔널한 값으로, 컴포넌트가 서버에서 렌더링될 때 호출되는 getSnapshot

* 컴포넌트가 SSR 환경에서 렌더링될 때 올바른 HTML을 제공하기 위해 사용된다.
* getSnapshot의 SSR 버전

## Tearing

> 의도치 않은 상태 불일치 시점에서 UI가 렌더링되는 것을 의미

conccurrent 기능을 적용하면 렌더링 작업이 다른 작업(사용자 인터렉션)과 동시에 실행될 수 있는데 렌더링 작업이 완료되기 전에 다른 작업(상태 변경)이 실행되면서 화면의 일부분만 업데이트되거나 깨지는 현상, 즉 변경된 상태에 대한 불일치에 의해 발생하는 문제이다.

### External Store에만 Tearing Issue가 발생한다?

리엑트 내부적으로 상태관리를 하면 우선순위가 같은 것들끼리 묶어 일괄적으로 처리하므로 tearing 현상을 방지할 수 있다.

하지만, 리액트 관심사 밖인 외부 스토어를 사용하면 관심사 밖인 외부 상태 변경의 우선순위를 정할 수 없기 때문에 데이터 변경과 렌더링 작업이 충돌하여 tearing 현상이 발생할 수 있다.

## Concurrent feature

> 빠르게 렌더링되는 부분을 차단하지 않고 느린 부분을 (웹 워커)백그라운드에 렌더링시킴으로써 **빠른 부분과 느린 부분을 분리**하여 각 부분이 자신의 속도로 사용자의 인터렉션에 즉각 반응할 수 있도록 **사용성을 높힌 것**

{% hint style="info" %}
React는 기본적으로 컴포넌트를 동기적으로 렌더링한다. 즉, 한번 렌더링이 시작되면 예외를 제외하고 렌더링을 중단시킬 수 없어, 렌더링이 끝난 후에만 다른 작업을 할 수 있다.

느린 컴포넌트를 렌더링하면 해당 렌더링이 끝나야지만 UI의 다른 부분에 대한 새로운 업데이트를 처리할 수 있기 때문에 느린 컴포넌트 렌더링이 다른 UI의 빠른 부분을 차단하게 된다.
{% endhint %}

### 우선 순위 업데이트

> React Concurrent는 우선순위가 높은 업데이트와 낮은 업데이트로 나뉜다.
>
> 업데이트는 리렌더링이 발생하는 모든 것을 의미한다.

* **우선순위가 높은 업데이트** →  `useState`, `useReducer` 등, 일반적으로익숙한 방식을 사용하면 높은 우선순위 업데이트가 발생한다.
* **우선순위가 낮은 업데이트** →`startTransition`, `useDefferedValue` 호출로 인해 발생한다.
  * 우선순위가 높은 업데이트가 완료된 후에만 실행된다.
  * 중간에 우선순위가 높은 업데이트에 의해 중단되면 기다린다음 처음부터 다시 렌더링한다.

{% hint style="info" %}
업데이트는 같은 우선순위의 업데이트를 모두 한번에 일괄처리한다.
{% endhint %}

### startTransition

> `useTransition`과 독립형 `startTransition`을 이용해서 Concurrent 기능을 사용할 수 있다.

* startTransition을 감싸면 특정 업데이트를 우선순위가 낮은 업데이트로 변경 가능
* startTransition의 콜백 내부의 상태 업데이트는 반드시 콜백 내의 스코프에서 호출해야 한다.
* startTransition은 상태만 적용 가능하다. (ref는 해당 ❌)

```tsx
// import {startTransition} from 'react'; 독립적으로 사용 가능
import {useTransition} from 'react';

//...
const [isPending, startTransition] = useTransition();

<input
  value={filter}
  onChange={(e) => {
    setFilter(e.target.value);
      startTransition(() => {
      // 여기서 `delayedFilter`의 값을 변경하는 우선순위가 낮은 업데이트를 발생
      setDelayedFilter(e.target.value);
    });
  }}
/>
//isPending을 사용해 트랜지션이 보류 중임을 알릴 수 있다.
{isPending && 'Recalculating...'}
```

### useDefferedValue

> 명령형 방식인 startTransition와 달리 선언형으로 동시성 기능을 사용할 수 있다.

* 우선순위가 낮은 업데이트의 결과를 반환하며, 이 상태는 인자로 전달된 값이다.&#x20;

```tsx
export default function App() {
  // 이 상태는 우선순위가 "높은" 업데이트에 의해 업데이트된다.
  const [filter, setFilter] = useState('');
  // 이 상태는 우선순위가 "낮은" 업데이트에 의해 업데이트된다.
  const deferredFilter = useDeferredValue(filter);

  return (
    <div className="container">
      <input
        value={filter}
        onChange={(e) => {
          setFilter(e.target.value);
        }}
      />
      <List filter={deferredFilter} />
    </div>
  );
}
```

### Suspense 활용

> 서스펜스를 사용하면 렌더링 이후에 실행되는 useEffect 내부에서 데이터 패칭하는 대신 렌더링 도중에 데이터 패칭을 가능하게 해준다.

* 데이터를 패칭하는 동안 사용자에게 지정한 Fallback UI를 표시한다.
* Suspense를 사용한 컴포넌트가 데이터 패칭을 시도할 때마다 fallback을 표시한다. (캐싱전략을 사용하지 않을경우)
* 서스펜스와 함께 `stale-while-revalidate` 전략을 사용하려면 concurrent 기능을 사용해야한다.

{% hint style="info" %}
**stale-while-revalidate란?**

* 사용자 경험을 향상시키기 위한 HTTP 캐싱 전략, 사용자에게 캐시된 데이터를 반환하고 동시에 백그라운드에서 새로운 데이터를 가져와 캐시를 갱신한다.
* 즉, 페이지를 변경할 때 로딩 상태로 돌아가지 않고, 새 데이터를 표시할 수 있을 때 까지 이전 데이터를 표시하는 방식
  {% endhint %}
