본문 바로가기

이것저것 스터디📚/React - 공식 문서

React 공식문서 -useSyncExternalStore

- useSyncExternalStore : 외부 스토어를 구독할 수 있는 React 훅이다.

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

- 스토어에 있는 데이터의 스냅샷을 반환한다.

- 두 개의 함수를 인수로 전달해야 한다.

 

- subscribe : 하나의 callback 인수를 받아 스토어를 구독하는 함수. 스토어가 변경되면 제공된 callback을 호출해야 하고, 이로부터 컴포넌트가 리렌더링된다. subscribe 함수는 구독을 해제하는 함수를 반환해야 한다.(subscribe 함수는 처음 마운트될때 호출되고, 이후 상태가 변화될때 호출된다)

- getSnapshot 함수의 경우, 스토어에서 데이터의 스냅샷을 읽어야 한다. 저장소가 변경되어 반환된 값이(Object.is 비교시) 달라지면, React는 컴포넌트를 리렌더링 한다.

- getServerSnapshot(optional) : 스토어에 있는 데이터의 초기 스냅샷을 반환하는 함수.

 

- useSyncExternalStore API는 주로 기존의 비 React 코드와 통합해야 할 때 유용하고, 가능하면 useState와 useReducer를 사용하는 것이 좋다.

 

* 사용 예제

// 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>
    </>
  );
}
// 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.

// todoStore.js
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();
  }
}

- 위 예제에서 todoStore는 React 외부에 데이터를 저장하는 외부 store로 구현되어 있다.

- TodosApp 컴포넌트는 useSyncExternalStore 훅으로 해당 외부 스토어에 연결한다.

- 🤔 그렇다면 subscribe 함수에 전달되는 listener 매개변수에는 과연 무엇이 들어가는 것이며, getSnapshot 함수는 누가 호출하는 것인가?


Extracting the logic to a custom Hook(사용자 정의 훅으로 로직 추출하기)

- 일반적으로 컴포넌트에서 직접 useSyncExternalStore를 작성하지는 않고, 사용자 정의 훅에서 호출한다.

// 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 { 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 />
    </>
  );
}

- 위 예제에서는 컴포넌트에 네트워크 연결이 활성화되어 있는지 여부를 표시하는 예제이다.

- 브라우저는 navigator.onLine이라는 속성을 통해 해당 정보를 노출하고, React가 알지 못하는 사이에 변경될 수 있으므로 useSyncExternalStore로 값을 읽고 있다.


Adding support for server rendering(서버 렌더링의 지원 추가하기)

- React 앱이 서버 렌더링을 하는 경우, React 컴포넌트는 브라우저 환경 외부에서도 실행되어 초기 HTML을 생성한다.

- 이때 브라우저 전용 API에 연결하는 경우 서버에는 해당 API가 없기 때문에 작동하지 않는다.

- 또한 타사 데이터 저장소에 연결하는 경우 서버와 클라이언트 간에 일치하는 데이터가 필요하다.

- 이러한 문제를 해결하기 위해서는 getServerSnapshot(optional) 함수를 useSyncExternalStore의 세 번째 임수로 전달하면 된다.

- getServerSnapshot은 HTML을 생성할 때 서버에서 실행되고 React가 서버 HTML을 가져와서 인터렉티브하게 만들 때 클라이언트에서 실행된다.(hydration)

- 즉, 앱이 상호작용하기 전에 사용될 초기 스냅샷 값을 제공할 수 있다.


I'm getting an error: "The result of getSnapshot should be cached"(getSnapshot의 결과를 캐시해야 합니다)

- 이 오류는 getSnapShot 함수가 호출될 때마다 새 객체를 반환한다는 의미이다.

function getSnapshot() {
  // 🔴 Do not return always different objects from getSnapshot
  // 🔴 getSnapshot에서 항상 다른 객체를 반환하지 마세요
  return {
    todos: myStore.todos
  };
}

function getSnapshot() {
  // ✅ You can return immutable data
  // ✅ 불변데이터는 반환할 수 있습니다
  return myStore.todos;
}

- 위 예제에서처럼 항상 다른 값을 반환하면 무한 루프에 빠지게 되어 이 오류가 발생한다.

- 대신 스토어에 불변 데이터가 포함된 경우에는 해당 데이터를 직접 반환할 수 있다.


My subscribe function gets called after every re-render(다시 렌더링할 때마다 subscribe 함수가 호출됩니다)

- subscribe 함수는 컴포넌트 내부에 정의되면, 컴포넌트가 다시 렌더링 될때마다 달라지게 되어 React가 store를 다시 구독한다.

function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  
  // 🚩 Always a different function, so React will resubscribe on every re-render
  // 🚩 항상 다른 함수이므로 React는 매 렌더링시마다 다시 구독합니다
  function subscribe() {
    // ...
  }

  // ...
}

- 위와 같은 스토어 재구독을 피하고 싶다면 subscribe 함수를 외부로 이동하거나 일부 인수가 변경될 때만 다시 구독하도록 useCallback을 사용할 수도 있다.

// subscribe 함수를 외부로 이동
function ChatIndicator() {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  // ...
}

// ✅ Always the same function, so React won't need to resubscribe
// ✅ 항상 동일한 함수이므로 React는 이를 재구독할 필요가 없습니다
function subscribe() {
  // ...
}

// useCallback 사용
function ChatIndicator({ userId }) {
  const isOnline = useSyncExternalStore(subscribe, getSnapshot);
  
  // ✅ Same function as long as userId doesn't change
  const subscribe = useCallback(() => {
    // ...
  }, [userId]);

  // ...
}

* 참고 : React 공식문서(https://react-ko.dev/learn)