3. useSyncExternalStore

useSyncExternalStore๋ž€?

  • useSyncExternalStore๋Š” ์™ธ๋ถ€ ์Šคํ† ์–ด๋ฅผ ๊ตฌ๋…ํ•  ์ˆ˜ ์žˆ๋Š” Reactํ›…์ด๋‹ค.

  • ์ „์—ญ์ƒํƒœ ๊ด€๋ฆฌ๋ฅผ ํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ, ๊ฐ ๊ฐœ๋ณ„์ ์ธ ์ƒํƒœ์— ๋Œ€ํ•œ ์ €์žฅ, ์—…๋ฐ์ดํŠธ๊ฐ€ ๊ฐ€๋Šฅํ•˜๋‹ค.

์‚ฌ์šฉํ•˜๊ธฐ

/src/store/BaseStore.ts

export type Listener = () => void;

class BaseStore<Snapshot> {
  protected listeners = new Set<Listener>();

  snapshot = {} as Snapshot;

  addListener(listener: Listener): void {
    this.listeners.add(listener);
  }

  removeListener(listener: Listener): void {
    this.listeners.delete(listener);
  }

  publish() {
    this.listeners.forEach((listener) => listener());
  }

  getSnapshot() {
    return this.snapshot;
  }

  getListener() {
    return this.listeners;
  }
}

export default BaseStore;


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

/src/store/BaseStore.test.ts

import BaseStore, { Listener } from './BaseStore';

describe('BaseStore', () => {
  type AnySnapshot = {
    obj: {
      id: number,
      name: string,
    },
  };

  let baseStore: BaseStore<AnySnapshot>;
  let listener1: Listener;
  let listener2: Listener;

  beforeEach(() => {
    baseStore = new BaseStore();
    listener1 = jest.fn();
    listener2 = jest.fn();
  });

  it('should add and remove listeners correctly', () => {
    baseStore.addListener(listener1);
    baseStore.addListener(listener2);

    expect(baseStore.getListener().size).toBe(2);

    baseStore.removeListener(listener1);

    expect(baseStore.getListener().size).toBe(1);
    expect(baseStore.getListener().has(listener1)).toBe(false);
    expect(baseStore.getListener().has(listener2)).toBe(true);
  });
});

/src/store/CartStore.ts

import { singleton } from 'tsyringe';

import BaseStore from './BaseStore';

import CartModel from '../models/CartModel';
import CartItemModel from '../models/CartItemModel';
import RestaurantModel from '../models/RestaurantModel';
import MenuItemModel from '../models/MenuItemModel';

export type CartStoreSnapshot = {
  cartItems: CartItemModel[];
};

@singleton()
class CartStore extends BaseStore<CartStoreSnapshot> {
  private cart = new CartModel();

  constructor() {
    super();
    this.takeSnapshot();
  }

  takeSnapshot() {
    this.snapshot = {
      cartItems: this.cart.cartItems,
    };
  }

  private update() {
    this.takeSnapshot();
    this.publish();
  }

  setOrderType(orderType: string) {
    this.cart = this.cart.setOrderType(orderType);
  }

  addItem({
    restaurant,
    menuItem,
    quantity,
  }: {
    restaurant: RestaurantModel;
    menuItem: MenuItemModel;
    quantity: number;
  }) {
    this.cart = this.cart.upsertItem({ restaurant, menuItem, quantity });
    this.update();
  }

  removeItem({ menuId }: { menuId: number }) {
    this.cart = this.cart.deleteItem({ menuId });
    this.update();
  }

  clear() {
    this.cart = this.cart.clearItems();
    this.update();
  }

  // Properties
  totalItemNum(): number {
    return this.cart.totalItemNum();
  }

  totalPrice(): number {
    return this.cart.totalPrice();
  }

  formattedTotalPrice(): string {
    return this.cart.formattedTotalPrice();
  }

  getCart() {
    return this.cart;
  }
}

export default CartStore;

/src/hooks/useCartStore.ts

import { useSyncExternalStore } from 'react';

import CartStore, { CartStoreSnapshot } from '../stores/CartStore';

const cartStore = new CartStore();

function useCartStore(): [CartStoreSnapshot, CartStore] {
  const snapshot: CartStoreSnapshot = useSyncExternalStore(
    (onStoreChange) => {
      cartStore.addListener(onStoreChange);
      return () => cartStore.removeListener(onStoreChange);
    },
    () => cartStore.getSnapshot(),
  );

  return [snapshot, cartStore];
}

export default useCartStore;

์„ ํ˜ธ์ด์œ 

  • ๋ฆฌ์—‘ํŠธ์—์„œ ์ œ๊ณตํ•˜๋Š” ํ›…์„ ์‚ฌ์šฉํ•œ๋‹ค.

  • ๋ฆฌํ„ด ํƒ€์ž…์„ ๋ช…์‹œ์ ์œผ๋กœ ์ง€์ •ํ•  ์ˆ˜ ์žˆ๋‹ค.

  • ํ…Œ์ŠคํŠธ์ฝ”๋“œ ์ž‘์„ฑ์— ์šฉ์ดํ•˜๋‹ค.

Last updated