React - useSyncExternalStore hook

이소라·2023년 9월 3일
0

React

목록 보기
23/23

useSyncExternalStore

useSyncExternalStore가 나타난 배경

  • React에 동시성이 적용되면서 렌더링이 중간에 중단될 수 있게 되었습니다.
  • 또한 React가 렌더링 도중 우선순위가 높은 일에 스레드를 양보하기 때문에, 네트워크 통신이나 사용자와의 상호 작용이 컴포넌트가 참조하는 상태 값을 변경시킬 수 있는 위험성이 존재합니다.
  • 이로 인해 각 컴포넌트가 상태를 읽어오는 시점이 다르고, 이 상태가 도중에 변경되므로 한 앱에서 상태와 UI가 불일치하는 tearing 문제가 발생합니다.

  • 외부 상태 관리 라이브러리인 reduxzustand를 사용하면서 tearing 문제가 발생하지 않도록, selector 함수를 useCallbackuseMemo로 감싸주어야 했습니다.
  • React는 이와 같이 외부 상태를 구독할 때 tearing이 발생하지 않게 하기 위해서, 구독하고 있는 외부 상태 변경이 관찰되면 컴포넌트를 리렌더링시키는 useSyncExternalStore 훅을 추가했습니다.



useSyncExternalStore

  • useSyncExternalStore
    • 외부 스토어에 구독할 수 있게 해주는 React 훅

    • 반환값 : 외부 스토어 데이터의 snapshot을 반환함

    • subscribe 매개변수 : 스토어를 구독하고 구독 취소 함수를 반환하는 함수

      • callback 인수을 받고 스토어에 callback을 등록함
      • 스토어가 변경될 때, callback 함수가 실행되고 컴포넌트가 리렌더링됨
    • getSnapshot 매개변수 : 스토어 데이터의 snapshot을 읽는 함수

      • 컴포넌트에 필요한 스토어 데이터의 snapshot을 반환함
      • 스토어가 변경되지 않을 때, 같은 값을 반환함
      • 스토어가 변경될 떄, 다른 값을 반환하고 컴포넌트가 리렌더링됨
    • getServerSnapshot 매개변수 (선택적) : 스토어 데이터의 초기 snapshot을 반환하는 함수

      • 클라이언트의 서버 렌더링된 컨텐츠가 hydration되는 동안에만 사용됨
import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodoApp() {
  const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getsnapshot)
}

주의사항

  • getSnapshot으로부터 반환된 스토어 snapshot은 불변성이어야 함
    • 내재된 스토어가 가변 데이터라면, 데이터가 변경되었을 때 새로운 불변성의 snapshot을 반환해야 함
  • 컴포넌트가 리렌더링될 때마다 다른 subscribe 함수가 전달될 경우, React는 새로 전달된 subscribe 함수를 사용함
    • 컴포넌트 외부에서 subscribe 함수를 선언하여 이를 방지할 수 있음



사용법

외부 스토어에 구독할 때

  • 때때로 컴포넌트는 React 외부에서 변하는 스토어로부터의 데이터를 읽어야할 수 있음
    • 3rd party 상태 관리 라이브러리가 React 외부에서 상태를 가지고 있을 경우
    • 가변값과 이벤트의 변화를 구독하기 위해 그 값들을 노출한 브라우저 API들
// 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();
  }
}


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

브라우저 API을 구독할 때

  • 시간에 따라 변하는 브라우저에 의해 노출된 값을 구독하고자 할 때
    • 이러한 값들은 React가 모르는 상태로 변경될 수 있기 때문에, useSyncExternalStore를 사용해야 함
    • 예: 컴포넌트가 네트워크 연결의 활성화 여부를 보여줄 때
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);
  };
}

server rendering 지원

  • React 앱에서 server rendering을 사용할 때, 초기 HTML을 생성하기 위해 React 컴포넌트를 브라우저 환경 바깥에서도 실행함

    • 브라우저 API를 연결했다면, 서버에는 그 API가 없으므로 동작하지 않음
    • 3rd party 데이터 스토어를 연결했다면, 서버와 클라이언트의 데이터가 일치해야 함
  • 이러한 문제들을 해결하기 위해 getServerSnapshot 매개변수에 함수를 전달함

  • getServerSnapshot 함수는 아래 2 가지 경우에만 실행됨

    • 서버에서 HTML을 생성할 때
    • 클라이언트에서 hydration할 때



참고

0개의 댓글