useSyncExternalStore๋Š” ์™ธ๋ถ€ store๋ฅผ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋Š” React Hook์ž…๋‹ˆ๋‹ค.

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

๋ ˆํผ๋Ÿฐ์Šค

useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)

์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์—์„œ useSyncExternalStore๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์—์„œ ๊ฐ’์„ ์ฝ์Šต๋‹ˆ๋‹ค.

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

store์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐœ์˜ ํ•จ์ˆ˜๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. subscribe ํ•จ์ˆ˜๋Š” store๋ฅผ ๊ตฌ๋…ํ•˜๊ณ  ๊ตฌ๋…์„ ์ทจ์†Œํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. getSnapshot ํ•จ์ˆ˜๋Š” store์—์„œ ๋ฐ์ดํ„ฐ์˜ ์Šค๋ƒ…์ƒท์„ ์ฝ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

์•„๋ž˜ ์˜ˆ์ œ ์ฐธ์กฐ

ํŒŒ๋ผ๋ฏธํ„ฐ

  • subscribe: ํ•˜๋‚˜์˜ callback ์ธ์ˆ˜๋ฅผ ๋ฐ›์•„ store์— ๊ตฌ๋…ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์Šคํ† ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜๋ฉด ์ œ๊ณต๋œ callback์„ ํ˜ธ์ถœํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋Ÿฌ๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ๋ฆฌ๋ Œ๋”๋ง๋ฉ๋‹ˆ๋‹ค. subscribe ํ•จ์ˆ˜๋Š” ๊ตฌ๋…์„ ์ •๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  • getSnapshot: ์ปดํฌ๋„ŒํŠธ์— ํ•„์š”ํ•œ store ๋ฐ์ดํ„ฐ์˜ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์Šคํ† ์–ด๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ getSnapshot์„ ๋ฐ˜๋ณต์ ์œผ๋กœ ํ˜ธ์ถœํ•˜๋ฉด ๋™์ผํ•œ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ €์žฅ์†Œ๊ฐ€ ๋ณ€๊ฒฝ๋˜์–ด ๋ฐ˜ํ™˜๋œ ๊ฐ’์ด ๋‹ค๋ฅด๋ฉด (Object.is์™€ ๋น„๊ตํ•˜์—ฌ) React๋Š” ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

  • optional getServerSnapshot: store์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ ์ดˆ๊ธฐ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๋ Œ๋”๋ง ๋„์ค‘๊ณผ ํด๋ผ์ด์–ธํŠธ์—์„œ ์„œ๋ฒ„ ๋ Œ๋”๋ง ๋œ ์ฝ˜ํ…์ธ ์˜ ํ•˜์ด๋“œ๋ ˆ์ด์…˜ ์ค‘์—๋งŒ ์‚ฌ์šฉ๋ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„ ์Šค๋ƒ…์ƒท์€ ํด๋ผ์ด์–ธํŠธ์™€ ์„œ๋ฒ„ ๊ฐ„์— ๋™์ผํ•ด์•ผ ํ•˜๋ฉฐ ์ผ๋ฐ˜์ ์œผ๋กœ ์ง๋ ฌํ™”๋˜์–ด ์„œ๋ฒ„์—์„œ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „๋‹ฌ๋ฉ๋‹ˆ๋‹ค. ์ด ํ•จ์ˆ˜๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์œผ๋ฉด ์„œ๋ฒ„์—์„œ ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ Œ๋”๋งํ•  ๋•Œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

๋ฐ˜ํ™˜ ๊ฐ’

๋ Œ๋”๋ง ๋กœ์ง์— ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ๋Š” store์˜ ํ˜„์žฌ ์Šค๋ƒ…์ƒท์ž…๋‹ˆ๋‹ค.

์ฃผ์˜ ์‚ฌํ•ญ

  • getSnapshot์ด ๋ฐ˜ํ™˜ํ•˜๋Š” store ์Šค๋ƒ…์ƒท์€ ๋ถˆ๋ณ€์ด์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์Šคํ† ์–ด์— ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ๋Š” ๊ฒฝ์šฐ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋œ ๊ฒฝ์šฐ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ์ƒˆ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡์ง€ ์•Š์œผ๋ฉด ์บ์‹œ ๋œ ๋งˆ์ง€๋ง‰ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค.

  • ๋ฆฌ๋ Œ๋”๋งํ•˜๋Š” ๋™์•ˆ ๋‹ค๋ฅธ subscribe ํ•จ์ˆ˜๊ฐ€ ์ „๋‹ฌ๋˜๋ฉด React๋Š” ์ƒˆ๋กœ ์ „๋‹ฌ๋œ subscribe ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์ €์žฅ์†Œ๋ฅผ ๋‹ค์‹œ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. ์ปดํฌ๋„ŒํŠธ ์™ธ๋ถ€์—์„œ subscribe ๋ฅผ ์„ ์–ธํ•˜๋ฉด ์ด๋ฅผ ๋ฐฉ์ง€ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

  • If the store is mutated during a non-blocking transition update, React will fall back to performing that update as blocking. Specifically, for every transition update, React will call getSnapshot a second time just before applying changes to the DOM. If it returns a different value than when it was called originally, React will restart the update from scratch, this time applying it as a blocking update, to ensure that every component on screen is reflecting the same version of the store.

  • Itโ€™s not recommended to suspend a render based on a store value returned by useSyncExternalStore. The reason is that mutations to the external store cannot be marked as non-blocking transition updates, so they will trigger the nearest Suspense fallback, replacing already-rendered content on screen with a loading spinner, which typically makes a poor UX.

    For example, the following are discouraged:

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

์‚ฌ์šฉ๋ฒ•

์™ธ๋ถ€ store ๊ตฌ๋…

๋Œ€๋ถ€๋ถ„์˜ React ์ปดํฌ๋„ŒํŠธ๋Š” props, state, ๊ทธ๋ฆฌ๊ณ  context์—์„œ๋งŒ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์Šต๋‹ˆ๋‹ค. ํ•˜์ง€๋งŒ ๋•Œ๋กœ๋Š” ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‹œ๊ฐ„์ด ์ง€๋‚จ์— ๋”ฐ๋ผ ๋ณ€๊ฒฝ๋˜๋Š” React ์™ธ๋ถ€์˜ ์ผ๋ถ€ ์ €์žฅ์†Œ์—์„œ ์ผ๋ถ€ ๋ฐ์ดํ„ฐ๋ฅผ ์ฝ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ๊ฐ€ ์žˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์Œ์ด ํฌํ•จ๋ฉ๋‹ˆ๋‹ค.

  • React ์™ธ๋ถ€์— state๋ฅผ ๋ณด๊ด€ํ•˜๋Š” ์„œ๋“œํŒŒํ‹ฐ ์ƒํƒœ ๊ด€๋ฆฌ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ.
  • ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๊ฐ’์„ ๋…ธ์ถœํ•˜๋Š” ๋ธŒ๋ผ์šฐ์ € API์™€ ๊ทธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ตฌ๋…ํ•˜๋Š” ์ด๋ฒคํŠธ.

์™ธ๋ถ€ ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์—์„œ ๊ฐ’์„ ์ฝ์œผ๋ ค๋ฉด ์ปดํฌ๋„ŒํŠธ์˜ ์ตœ์ƒ์œ„ ๋ ˆ๋ฒจ์—์„œ useSyncExternalStore๋ฅผ ํ˜ธ์ถœํ•˜์„ธ์š”.

import { useSyncExternalStore } from 'react';
import { todosStore } from './todoStore.js';

function TodosApp() {
const todos = useSyncExternalStore(todosStore.subscribe, todosStore.getSnapshot);
// ...
}

store์— ์žˆ๋Š” ๋ฐ์ดํ„ฐ์˜ snapshot์„ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค. ๋‘ ๊ฐœ์˜ ํ•จ์ˆ˜๋ฅผ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

  1. subscribe ํ•จ์ˆ˜๋Š” store์— ๊ตฌ๋…ํ•˜๊ณ  ๊ตฌ๋…์„ ์ทจ์†Œํ•˜๋Š” ํ•จ์ˆ˜๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
  2. getSnapshot ํ•จ์ˆ˜ ํ•จ์ˆ˜๋Š” store์—์„œ ๋ฐ์ดํ„ฐ์˜ ์Šค๋ƒ…์ƒท์„ ์ฝ์–ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.

React๋Š” ์ด ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•ด ์ปดํฌ๋„ŒํŠธ๋ฅผ store์— ๊ตฌ๋…ํ•œ ์ƒํƒœ๋กœ ์œ ์ง€ํ•˜๊ณ  ๋ณ€๊ฒฝ ์‚ฌํ•ญ์ด ์žˆ์„ ๋•Œ ๋ฆฌ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์•„๋ž˜ ์ƒŒ๋“œ๋ฐ•์Šค์—์„œ todosStore๋Š” React ์™ธ๋ถ€์— ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋Š” ์™ธ๋ถ€ store๋กœ ๊ตฌํ˜„๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค. TodosApp์ปดํฌ๋„ŒํŠธ๋Š” useSyncExternalStore Hook์œผ๋กœ ํ•ด๋‹น ์™ธ๋ถ€ store์— ์—ฐ๊ฒฐํ•ฉ๋‹ˆ๋‹ค.

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

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

๊ฐ€๋Šฅํ•˜๋ฉด ๋‚ด์žฅ๋œ React state๋ฅผ useState ๋ฐ useReducer์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค. useSyncExternalStore API๋Š” ๊ธฐ์กด ๋น„ React ์ฝ”๋“œ์™€ ํ†ตํ•ฉํ•ด์•ผ ํ•  ๋•Œ ์ฃผ๋กœ ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.


๋ธŒ๋ผ์šฐ์ € API ๊ตฌ๋…

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์—์„œ ๊ฐ’์„ ์ฝ๋Š” ๋ฐฉ๋ฒ•๊ณผ ๊ทธ ๋ณ€๊ฒฝ ์‚ฌํ•ญ์„ ๊ตฌ๋…ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. ๋„คํŠธ์›Œํฌ์—์„œ ๋””๋ฐ”์ด์Šค์˜ ์—ฐ๊ฒฐ์„ ๋Š์–ด๋ณด๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์‘๋‹ต์œผ๋กœ ๋ฆฌ๋ Œ๋”๋ง๋˜๋Š” ๊ฒƒ์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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


custom Hook์œผ๋กœ ๋กœ์ง ์ถ”์ถœํ•˜๊ธฐ

์ผ๋ฐ˜์ ์œผ๋กœ ์ปดํฌ๋„ŒํŠธ์—์„œ ์ง์ ‘ useSyncExternalStore๋ฅผ ์ž‘์„ฑํ•˜์ง€๋Š” ์•Š์Šต๋‹ˆ๋‹ค. ๋Œ€์‹  ์ผ๋ฐ˜์ ์œผ๋กœ custom Hook์—์„œ ํ˜ธ์ถœํ•ฉ๋‹ˆ๋‹ค. ์ด๋ ‡๊ฒŒ ํ•˜๋ฉด ์„œ๋กœ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๋™์ผํ•œ ์™ธ๋ถ€ ์ €์žฅ์†Œ๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์˜ˆ๋ฅผ ๋“ค์–ด ์ด custom useOnlineStatus Hook์€ ๋„คํŠธ์›Œํฌ๊ฐ€ ์˜จ๋ผ์ธ ์ƒํƒœ์ธ์ง€ ์—ฌ๋ถ€๋ฅผ ์ถ”์ ํ•ฉ๋‹ˆ๋‹ค.

import { useSyncExternalStore } from 'react';

export function useOnlineStatus() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
return isOnline;
}

function getSnapshot() {
// ...
}

function subscribe(callback) {
// ...
}

์ด์ œ ๋‹ค๋ฅธ ์ปดํฌ๋„ŒํŠธ์—์„œ ๊ธฐ๋ณธ ๊ตฌํ˜„์„ ๋ฐ˜๋ณตํ•˜์ง€ ์•Š๊ณ ๋„ useOnlineStatus๋ฅผ ํ˜ธ์ถœํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

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


์„œ๋ฒ„ ๋ Œ๋”๋ง ์ง€์› ์ถ”๊ฐ€

React ์•ฑ์ด server rendering์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒฝ์šฐ React ์ปดํฌ๋„ŒํŠธ๋Š” ๋ธŒ๋ผ์šฐ์ € ํ™˜๊ฒฝ ์™ธ๋ถ€์—์„œ๋„ ์‹คํ–‰๋˜์–ด ์ดˆ๊ธฐ HTML์„ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์™ธ๋ถ€ store์— ์—ฐ๊ฒฐํ•  ๋•Œ ๋ช‡ ๊ฐ€์ง€ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

  • ๋ธŒ๋ผ์šฐ์ € ์ „์šฉ API์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒฝ์šฐ ์„œ๋ฒ„์— ํ•ด๋‹น API๊ฐ€ ์กด์žฌํ•˜์ง€ ์•Š์œผ๋ฏ€๋กœ ์ž‘๋™ํ•˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
  • third-party ๋ฐ์ดํ„ฐ ์ €์žฅ์†Œ์— ์—ฐ๊ฒฐํ•˜๋Š” ๊ฒฝ์šฐ ์„œ๋ฒ„์™€ ํด๋ผ์ด์–ธํŠธ ๊ฐ„์— ์ผ์น˜ํ•˜๋Š” ๋ฐ์ดํ„ฐ๊ฐ€ ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.

์ด๋Ÿฌํ•œ ๋ฌธ์ œ๋ฅผ ํ•ด๊ฒฐํ•˜๋ ค๋ฉด getServerSnapshot ํ•จ์ˆ˜๋ฅผ useSyncExternalStore์˜ ์„ธ ๋ฒˆ์งธ ์ธ์ˆ˜๋กœ ์ „๋‹ฌํ•˜์„ธ์š”.

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์„ ์ƒ์„ฑํ•  ๋•Œ ์„œ๋ฒ„์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.
  • hydration ์ค‘ ์ฆ‰ React๊ฐ€ ์„œ๋ฒ„ HTML์„ ๊ฐ€์ ธ์™€์„œ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒํ•˜๊ฒŒ ๋งŒ๋“ค ๋•Œ ํด๋ผ์ด์–ธํŠธ์—์„œ ์‹คํ–‰๋ฉ๋‹ˆ๋‹ค.

์ด๋ฅผ ํ†ตํ•ด ์•ฑ์ด ์ƒํ˜ธ์ž‘์šฉํ•˜๊ธฐ ์ „์— ์‚ฌ์šฉ๋  ์ดˆ๊ธฐ ์Šค๋ƒ…์ƒท ๊ฐ’์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„ ๋ Œ๋”๋ง์— ์˜๋ฏธ ์žˆ๋Š” ์ดˆ๊ธฐ๊ฐ’์ด ์—†๋‹ค๋ฉด ์ปดํฌ๋„ŒํŠธ๊ฐ€ ํด๋ผ์ด์–ธํŠธ์—์„œ๋งŒ ๋ Œ๋”๋ง๋˜๋„๋ก ๊ฐ•์ œ ์„ค์ •ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

์ค‘์š”ํ•ฉ๋‹ˆ๋‹ค!

getServerSnapshot์ด ์ดˆ๊ธฐ ํด๋ผ์ด์–ธํŠธ ๋ Œ๋”๋ง์—์„œ ์„œ๋ฒ„์—์„œ ๋ฐ˜ํ™˜ํ•œ ๊ฒƒ๊ณผ ๋™์ผํ•œ ์ •ํ™•ํ•œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”. ์˜ˆ๋ฅผ ๋“ค์–ด getServerSnapshot์ด ์„œ๋ฒ„์—์„œ ๋ฏธ๋ฆฌ ์ฑ„์›Œ์ง„ store ์ฝ˜ํ…์ธ ๋ฅผ ๋ฐ˜ํ™˜ํ•œ ๊ฒฝ์šฐ ์ด ์ฝ˜ํ…์ธ ๋ฅผ ํด๋ผ์ด์–ธํŠธ๋กœ ์ „์†กํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ด๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ์ผ๋ฐ˜์ ์ธ ๋ฐฉ๋ฒ• ์ค‘ ํ•˜๋‚˜๋Š” ์„œ๋ฒ„ ๋ Œ๋”๋ง ์ค‘์— window.MY_STORE_DATA์™€ ๊ฐ™์€ ๊ธ€๋กœ๋ฒŒ์„ ์„ค์ •ํ•˜๋Š” <script> ํƒœ๊ทธ๋ฅผ ์ƒ์„ฑํ•œ ๋‹ค์Œ ํด๋ผ์ด์–ธํŠธ์—์„œ getServerSnapshot์—์„œ ํ•ด๋‹น ๊ธ€๋กœ๋ฒŒ์„ ์ฝ์–ด์˜ค๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์™ธ๋ถ€ ์Šคํ† ์–ด์—์„œ ์ด๋ฅผ ์ˆ˜ํ–‰ํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•œ ์ง€์นจ์„ ์ œ๊ณตํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.


ํŠธ๋Ÿฌ๋ธ” ์ŠˆํŒ…

์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: โ€getSnapshot์˜ ๊ฒฐ๊ณผ๋ฅผ ์บ์‹œํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.โ€

์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๋ฉด getSnapshot ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋  ๋•Œ๋งˆ๋‹ค ์ƒˆ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•œ๋‹ค๋Š” ์˜๋ฏธ์ž…๋‹ˆ๋‹ค.

function getSnapshot() {
// ๐Ÿ”ด getSnapshot์—์„œ ํ•ญ์ƒ ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•˜์ง€ ๋งˆ์„ธ์š”.
return {
todos: myStore.todos
};
}

React๋Š” getSnapshot ๋ฐ˜ํ™˜ ๊ฐ’์ด ์ง€๋‚œ๋ฒˆ๊ณผ ๋‹ค๋ฅด๋ฉด ์ปดํฌ๋„ŒํŠธ๋ฅผ ๋ฆฌ๋ Œ๋”๋งํ•ฉ๋‹ˆ๋‹ค. ๊ทธ๋ ‡๊ธฐ ๋•Œ๋ฌธ์— ํ•ญ์ƒ ๋‹ค๋ฅธ ๊ฐ’์„ ๋ฐ˜ํ™˜ํ•˜๋ฉด ๋ฌดํ•œ ๋ฃจํ”„์— ๋“ค์–ด๊ฐ€์„œ ์ด ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•ฉ๋‹ˆ๋‹ค.

์‹ค์ œ๋กœ ๋ณ€๊ฒฝ๋œ ์‚ฌํ•ญ์ด ์žˆ๋Š” ๊ฒฝ์šฐ์—๋งŒ getSnapshot ๊ฐ์ฒด๊ฐ€ ๋‹ค๋ฅธ ๊ฐ์ฒด๋ฅผ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. store์— ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ํฌํ•จ๋œ ๊ฒฝ์šฐ ํ•ด๋‹น ๋ฐ์ดํ„ฐ๋ฅผ ์ง์ ‘ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.

function getSnapshot() {
// โœ… ๋ถˆ๋ณ€ ๋ฐ์ดํ„ฐ๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
return myStore.todos;
}

store ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๊ฒฝ์šฐ getSnapshot ํ•จ์ˆ˜๋Š” ํ•ด๋‹น ๋ฐ์ดํ„ฐ์˜ ๋ณ€๊ฒฝ ๋ถˆ๊ฐ€๋Šฅํ•œ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ์ฆ‰ ์ƒˆ ๊ฐ์ฒด๋ฅผ ์ƒ์„ฑํ•ด์•ผ ํ•˜์ง€๋งŒ ๋งค๋ฒˆ ํ˜ธ์ถœํ•  ๋•Œ๋งˆ๋‹ค ์ด ์ž‘์—…์„ ์ˆ˜ํ–‰ํ•ด์„œ๋Š” ์•ˆ ๋ฉ๋‹ˆ๋‹ค. ๋Œ€์‹  ๋งˆ์ง€๋ง‰์œผ๋กœ ๊ณ„์‚ฐ๋œ ์Šค๋ƒ…์ƒท์„ ์ €์žฅํ•˜๊ณ  ์ €์žฅ์†Œ์˜ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์€ ๊ฒฝ์šฐ ์ง€๋‚œ๋ฒˆ๊ณผ ๋™์ผํ•œ ์Šค๋ƒ…์ƒท์„ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค. ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ๋ฐ์ดํ„ฐ๊ฐ€ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ๋ฒ•์€ ๋ณ€๊ฒฝ ๊ฐ€๋Šฅํ•œ ์ €์žฅ์†Œ๊ฐ€ ๊ตฌํ˜„๋œ ๋ฐฉ์‹์— ๋”ฐ๋ผ ๋‹ค๋ฆ…๋‹ˆ๋‹ค.


๋ฆฌ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค subscribe ํ•จ์ˆ˜๊ฐ€ ํ˜ธ์ถœ๋ฉ๋‹ˆ๋‹ค.

subscribe ํ•จ์ˆ˜๋Š” ์ปดํฌ๋„ŒํŠธ ๋‚ด๋ถ€์— ์ •์˜๋˜๋ฏ€๋กœ ๋ฆฌ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค ๋‹ฌ๋ผ์ง‘๋‹ˆ๋‹ค.

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// ๐Ÿšฉํ•ญ์ƒ ๋‹ค๋ฅธ ํ•จ์ˆ˜๋ฅผ ์‚ฌ์šฉํ•˜๋ฏ€๋กœ React๋Š” ๋ Œ๋”๋งํ•  ๋•Œ๋งˆ๋‹ค ๋‹ค์‹œ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค.
function subscribe() {
// ...
}

// ...
}

๋ฆฌ๋ Œ๋”๋ง ์‚ฌ์ด์— ๋‹ค๋ฅธ subscribe ํ•จ์ˆ˜๋ฅผ ์ „๋‹ฌํ•˜๋ฉด React๊ฐ€ store๋ฅผ ๋‹ค์‹œ ๊ตฌ๋…ํ•ฉ๋‹ˆ๋‹ค. ์ด๋กœ ์ธํ•ด ์„ฑ๋Šฅ ๋ฌธ์ œ๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ  store ์žฌ๊ตฌ๋…์„ ํ”ผํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด subscribe ํ•จ์ˆ˜๋ฅผ ์™ธ๋ถ€๋กœ ์ด๋™ํ•˜์„ธ์š”.

function ChatIndicator() {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);
// ...
}

// โœ… ํ•ญ์ƒ ๋™์ผํ•œ ํ•จ์ˆ˜์ด๋ฏ€๋กœ React๋Š” ๋‹ค์‹œ ๊ตฌ๋…ํ•  ํ•„์š”๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.
function subscribe() {
// ...
}

๋˜๋Š” ์ผ๋ถ€ ์ธ์ˆ˜๊ฐ€ ๋ณ€๊ฒฝ๋  ๋•Œ๋งŒ ๋‹ค์‹œ ๊ตฌ๋…ํ•˜๋„๋ก subscribe์„ useCallback์œผ๋กœ ๋ž˜ํ•‘ํ•ฉ๋‹ˆ๋‹ค.

function ChatIndicator({ userId }) {
const isOnline = useSyncExternalStore(subscribe, getSnapshot);

// โœ… userId๊ฐ€ ๋ณ€๊ฒฝ๋˜์ง€ ์•Š๋Š” ํ•œ ๋™์ผํ•œ ํ•จ์ˆ˜์ž…๋‹ˆ๋‹ค.
const subscribe = useCallback(() => {
// ...
}, [userId]);

// ...
}