Skip to content

Latest commit

 

History

History
186 lines (153 loc) · 7.97 KB

2주차_성지현_개인.md

File metadata and controls

186 lines (153 loc) · 7.97 KB

external store

External Store

  • External의 의미 == React의 외부
  • External Store란 React 외부의 저장소를 의미
    • 즉, React가 관리하지 않는 Store
    • e.g. 상태 관리 라이브러리, 브라우저 스토리지(IndexedDb, localStorage, …), 웹소켓 등등
      • useState, useReducer를 제외한 모든 것이라고 보면 됨

useSyncExternalStore

등장 배경

  • Tearing 이슈를 해결하기 위함
    • Tearing이란 하나의 state가 서로 다른 값으로 렌더링되는 현상
  • Tearing 이슈는 왜 발생하는가
    • React가 concurrent 렌더링을 지원하면서 생긴 문제
    • 기존에는 synchronous 렌더링 방식을 사용했기 때문에, 한 번 시작한 렌더링은 중지되지 않음
      • 즉, 렌더링 중간에 상태가 변경되더라도, 변경 이전의 상태를 가지고 렌더링을 마친 다음 새로운 상태를 바탕으로 리렌더링 (업데이트 순서 보장) Untitled
    • 하지만 concurrent 렌더링 방식을 사용하면서, 한 번 시작한 렌더링이 중단(interrupt받을 수 있는 상황)될 수 있는 상황이 생겨버림
      • e.g. 높은 우선순위를 가지는 유저 인터랙션이 발생하여 상태가 변경된 경우
      • 즉, 렌더링 중간에 상태가 변경되면 그 시점을 기준으로 렌더링에 쓰인 상태 값이 다를 수 있음 Untitled
  • 이처럼 같은 데이터임에도 다른 값을 표시하는 현상을 Tearing 이라 함
    • 이때, Tearing 이슈는 외부 저장소의 상태를 사용하는 경우에만 발생
      • useState 같은 React 내부에서 관리하는 상태는 tearing 이슈가 발생하지 않도록 처리되어 있음
  • 정리하자면, useSyncExternalStore는 concurrent 렌더링 환경에서의 Tearing 이슈를 보완하고자 등장한 훅
    • React 내부의 것이 아니지만 싱크를 맞춰보겠다는 의지가 담긴…

레퍼런스

useSyncExternalStore(
	subsribe: (callback) => Unsubscribe,
	getSnapshot: () => State,
	getServerSnapshot: () => State,
) => State
  • parameters
    • subscribe
      • 콜백 함수를 받아 스토어에 등록하는 용도
      • 스토어에 있는 값이 변경되면 이 콜백이 호출되고, useSyncExternalStore 훅을 호출한 컴포넌트를 리렌더링함
    • getSnapshot
      • 컴포넌트에 필요한 현재 스토어의 데이터를 반환하는 함수
      • 스토어에 있는 값이 변경되지 않았다면 매번 함수를 호출할 때마다 동일한 값을 반환해야함
        • 스토어의 값에 변경이 있는지 확인하기 위해 Object.is로 비교
    • getServerSnapshot
      • 옵셔널 값으로, 서버 사이드 렌더링 시에 내부 리액트를 하이드레이션 하는 도중에만 사용
      • 서버 사이드에서 렌더링되는 훅이라면 반드시 이 값을 넘겨줘야 하며, 클라이언트 값과 불일치가 발생하면 오류가 발생
  • returns
    • 스토어에 저장된 상태값

subscribe, getSnaphot 등의 기능을 제공하고 있는 외부 스토어여야 이 훅을 사용할 수 있음 (일종의 규격)

실사용 예시 - zustand

들어가기 전…

zustand로 스토어를 만드는 코드는 다음과 같음 (create 함수 호출)

import { create } from 'zustand'

// store 생성
const useBearStore = create((set) => ({
  bears: 0, // 스토어에 저장할 상태
  increasePopulation: () => set((state) => ({ bears: state.bears + 1 })), // 상태를 다룰 메서드 1
  removeAllBears: () => set({ bears: 0 }), // 상태를 다룰 메서드 2
  ...
}))
// 스토어를 구독하는 컴포넌트
function BearCounter() {
  const bears = useBearStore((state) => state.bears); // bears라는 상태를 사용
  return <h1>{bears} around here ...</h1>;
}

// 스토어의 상태 조작 메서드를 사용하는 컴포넌트
function Controls() {
  const increasePopulation = useBearStore((state) => state.increasePopulation); // increasePopulation 함수 사용
  return <button onClick={increasePopulation}>one up</button>;
}

zustand 코드 까보기

// zustand의 Store는 setState/getState/getInitialState/subscribe를 가짐
export interface StoreApi<T> {
  setState: SetStateInternal<T>;
  getState: () => T;
  getInitialState: () => T;
  subscribe: (listener: (state: T, prevState: T) => void) => () => void;
}

const createStoreImpl: CreateStoreImpl = (createState) => {
  type TState = ReturnType<typeof createState>;
  type Listener = (state: TState, prevState: TState) => void;
  let state: TState; // zustand 스토어가 관리할 상태
  const listeners: Set<Listener> = new Set(); // 상태를 TODO

  const setState: StoreApi<TState>['setState'] = (partial, replace) => {
    const nextState =
      typeof partial === 'function' // setter 함수를 받으면
        ? (partial as (state: TState) => TState)(state) // 함수 실행한 결과를 nextState로
        : partial; // 함수가 아니면 그 값을 그대로 활용
    if (!Object.is(nextState, state)) {
      // 현재 상태와 nextState 비교
      // nextState와 state가 다르다면
      const previousState = state; // 현재 상태는 이전 상태로 취급
      state = // 현재 상태는
        // replace가 true라면 nextState를 덮어쓰고
        // replace가 false이면서 nextState가 객체가 아니라면 nextState를 덮어쓰고
        // replace가 false이면서 nextState가 객체라면 state와 nextState를 병합
        replace ?? (typeof nextState !== 'object' || nextState === null)
          ? (nextState as TState)
          : Object.assign({}, state, nextState);
      // 스토어를 구독하는 함수들 모두 호출
      listeners.forEach((listener) => listener(state, previousState));
    }
  };

  // 현재 상태 반환
  const getState: StoreApi<TState>['getState'] = () => state;

  // 이니셜 상태 반환
  const getInitialState: StoreApi<TState>['getInitialState'] = () => initialState;

  // 상태가 바뀌면 호출할 함수들을 등록하거나 지움
  const subscribe: StoreApi<TState>['subscribe'] = (listener) => {
    listeners.add(listener);
    // Unsubscribe
    return () => listeners.delete(listener);
  };

  const api = { setState, getState, getInitialState, subscribe };
  const initialState = (state = createState(setState, getState, api));
  return api as any;
};

export const createStore = ((createState) =>
  createState ? createStoreImpl(createState) : createStoreImpl) as CreateStore;
// react.ts
// react에서 zustand를 사용하는 경우
import React from 'react';
import { createStore } from './vanilla.ts';
import type { Mutate, StateCreator, StoreApi, StoreMutatorIdentifier } from './vanilla.ts';

export function useStore<TState, StateSlice>(
  api: ReadonlyStoreApi<TState>,
  selector: (state: TState) => StateSlice = identity as any
) {
  const slice = React.useSyncExternalStore(
    api.subscribe, // 스토어(에 저장된 상태)를 구독하게 해주는 함수
    () => selector(api.getState()), // 스토어에 저장된 현재 상태를 반환하는 함수 전달
    () => selector(api.getInitialState()) // 스토어에 저장된 이니셜 상태를 반환하는 함수 전달
  );
  React.useDebugValue(slice); // React DevTools에서 커스텀훅(=useStore)에 label을 추가할 수 있는 훅
  return slice;
}

const createImpl = <T,>(createState: StateCreator<T, [], []>) => {
  const api = createStore(createState); // { setState, getState, getInitialState, subscribe }

  const useBoundStore: any = (selector?: any) => useStore(api, selector);

  Object.assign(useBoundStore, api);

  return useBoundStore;
};

export const create = (<T,>(createState: StateCreator<T, [], []> | undefined) =>
  createState ? createImpl(createState) : createImpl) as Create;
  • React+Zustand 환경에서 create로 스토어를 만드는 것은 결국
    • zustand 내부적으로 구현된 스토어의 형태를
    • useSyncExternalStore로 래핑(=즉, 쉽게 말하면 티어링 방지 처리가 된..?)하는 것