728x90

들어가며

Proxy에서 처음 접하게 된 것은 제로초님의 쇼츠였습니다.  객체를 가공해서 사용할 수 있게 하는 일종어가며

Proxy에서 처음 접하게 된 것은 제로초님의 쇼츠였습니다. 객체를 가공해서 사용할 수 있게 하는  미들웨어 느낌의 객체입니다. 해당 쇼츠에서는 객체가 에러를 뱉도록 가공하는 예시를 보여줬었습니다.

 

리액트 훅을 활용한 마이크로 상태관리에서 Valtio가  Proxy 기반으로 상태관리를 한다는 사실을 접하게 되고, proxy란 무엇이고 Valtio에서는 어떻게 사용하는지 자세히 알아볼겸 포스팅하게 되었습니다.

 

 

프록시란?

mdn에서 나타낸 프록시의 정의는 아래와 같습니다.

Proxy 객체를 사용하면 원래 Object 대신 사용할 수 있는 객체를 만들지만, 이 객체의 속성 가져오기, 설정 및 정의와 같은 기본 객체 작업을 재정의할 수 있습니다. 프록시 객체는 일반적으로 속성 액세스를 기록하고, 입력의 유효성을 검사하고, 형식을 지정하거나, 삭제하는 데 사용됩니다.

 

let proxy = new Proxy(target, handler)
  • target – 감싸게 될 객체로, 함수를 포함한 모든 객체가 가능함
  • handler – 동작을 가로채는 메서드인 '트랩(trap)'이 담긴 객체로, 여기서 프락시를 설정함

그리고 이때 handler에 사용할 수 있는 메서드는 아래와 같습니다.

https://ko.javascript.info/proxy#ref-1033

 

아래는 javainfo에서 보여준 get메서드를 활용한 proxy 사용 예시입니다. 

let numbers = [0, 1, 2];

numbers = new Proxy(numbers, {
  get(target, prop) {
    if (prop in target) {
      return target[prop];
    } else {
      return 0; 
    }
  }
});

console.log( numbers[1] ); // 1
console.log( numbers[123] ); // 0

이와 같이 객체를 가공하여 추가적인 기능을 삽입할 때 사용할 수 있는 툭수한 객체라고 생각하면 됩니다.

 

이제 Valtio에 대해서 알아보고  Valtio에서는 Proxy를 어떻게 사용하는지 알아보려고 합니다.

Valtio란?

리액트 훅을 활용한 마이크로 상태관리의 저자이자 Valtio를 만든 다이시 카토는 아래와 같이 Valtio를 설명합니다.

갱신 모델의 상태 관리 라이브러리이자 상태 사용 추적 기법 기반으로 렌더링 최적화를 하는 라이브러리

 

proxy는 상태를 감지하는 변경가능한 상태를 만들고, snapshot을 통해서 변경 불가능한 객체(Object.freeze 기반)를 만든다.

import { proxy, useSnapshot } from 'valtio';

const state = proxy({
  count1: 0,
  count2: 0,
});

const Counter1 = () => {
  const snap = useSnapshot(state);
  const inc = () => ++state.count1;
  return (
    <>
      {snap.count1} <button onClick={inc}>+1</button>
    </>
  );
};

const Counter2 = () => {
  const snap = useSnapshot(state);
  const inc = () => ++state.count2;
  return (
    <>
      {snap.count2} <button onClick={inc}>+1</button>
    </>
  );
};

const ValtioCounter = () => (
  <>
    <div>
      <Counter1 />
    </div>
    <div>
      <Counter2 />
    </div>
  </>
);

export default ValtioCounter;

 

Counter 1 2는 각각 상태 count 12 를 사용하고 있다. zustand나 redux처럼 별도의 selector를 사용하지 않기에, 리렌더링 최적화가 안될거 같지만 앞서 언급한 상태 사용 추적기법에 의해서 리렌더링이 최적화가 되어있다.

1을 눌렀을 때 2가 리렌더링 되지 않음

 

useSnapshot 훅에서  나온  스냅샷을 컴퍼넌트가 사용하게 되면 해당 컴퍼넌트가 특정 상태를 사용하는 것을 기억한다. 이후 상태가 변경하였으면 proxy에서 특정 상태의 변경을 알리고 이를 통해  useSnapshot훅이 컴퍼넌트가 사용하기 있는 객체가 변경 되었으면 리렌더링을 하는 것이다.

 

Valtio내부 코드 확인

state (proxy함수 리턴값) 와 snap을 각각 콘솔로 찍어보면 Proxy 객체임을 확인할 수 있습니다. 그리고  각각의 Handler를 보면 state에는 deleteProperty와 set이 존재하고  snap에는 get, getOwnPropertyDescription ,has , ownKeys 프로퍼티가 존재하는 것을 확인할 수 있습니다.

 

이제 각각의 코드를 확인하려고 합니다. 분량이 많아서 전체코드는 다루지 않겠습니다.

각각의 코드를 확인하기전에 proxy, useSnapshot 모두 Proxy를 다룰 때,  proxy-compare 라이브러리에 의존하고 있고, 해당 부분은 관심사의 밖이기에 간단하게 어떤 역할을 하는지만 언급하려고 합니다. 자세한 내용은 상단 링크의 레포에서 확인할 수 있습니다.

 

proxy

proxy의 경우에는  레포지토리의 src\vanilla.ts 경로에 존재합니다.  

vanilla.ts 에서는 크게 3부분으로 나누어져 있습니다. 모든 코드를 보여드리기엔 많기에 간단한 예시 정도만 보여드리려고 하고,  어떻게 상태 사용 추적이 되는지에 포커스를 두려고 합니다.

 

1. 타입선언 부분 및 기본 상수값으로 쓰는 함수들

예시)

type Primitive = string | number | boolean | null | undefined | symbol | bigint
type Op =
  | [op: 'set', path: Path, value: unknown, prevValue: unknown]
  | [op: 'delete', path: Path, prevValue: unknown]
type Listener = (op: Op, nextVersion: number) => void

const canProxyDefault = (x: unknown): boolean =>
  isObject(x) &&
  !refSet.has(x) &&
  (Array.isArray(x) || !(Symbol.iterator in x)) &&
  !(x instanceof WeakMap) &&
  !(x instanceof WeakSet) &&
  !(x instanceof Error) &&
  !(x instanceof Number) &&
  !(x instanceof Date) &&
  !(x instanceof String) &&
  !(x instanceof RegExp) &&
  !(x instanceof ArrayBuffer) &&
  !(x instanceof Promise)

 

2. 내부 함수 및 내부 변수
예시) 

// internal states
const proxyStateMap: WeakMap<ProxyObject, ProxyState> = new WeakMap()
const refSet: WeakSet<object> = new WeakSet()
const snapCache: WeakMap<object, [version: number, snap: unknown]> =
  new WeakMap()
const versionHolder = [1, 1] as [number, number]
const proxyCache: WeakMap<object, ProxyObject> = new WeakMap()

// internal functions
let objectIs: (a: unknown, b: unknown) => boolean = Object.is
let newProxy = <T extends object>(target: T, handler: ProxyHandler<T>): T =>
  new Proxy(target, handler)
let canProxy: typeof canProxyDefault = canProxyDefault
let createSnapshot: typeof createSnapshotDefault = createSnapshotDefault
let createHandler: typeof createHandlerDefault = createHandlerDefault

3. 모듈 외부로 export할 함수들

예시) proxy, snapshot 

 

 

이제 다시 proxy에 조금 더 집중해보려고 합니다. proxy 함수의 전체 코드는 아래와 같습니다.

export function proxy<T extends object>(baseObject: T = {} as T): T {
  if (!isObject(baseObject)) {
    throw new Error('object required')
  }
  const found = proxyCache.get(baseObject) as T | undefined
  if (found) {
    return found
  }
  let version = versionHolder[0]
  const listeners = new Set<Listener>()
  const notifyUpdate = (op: Op, nextVersion = ++versionHolder[0]) => {
    if (version !== nextVersion) {
      version = nextVersion
      listeners.forEach((listener) => listener(op, nextVersion))
    }
  }
  let checkVersion = versionHolder[1]
  const ensureVersion = (nextCheckVersion = ++versionHolder[1]) => {
    if (checkVersion !== nextCheckVersion && !listeners.size) {
      checkVersion = nextCheckVersion
      propProxyStates.forEach(([propProxyState]) => {
        const propVersion = propProxyState[1](nextCheckVersion)
        if (propVersion > version) {
          version = propVersion
        }
      })
    }
    return version
  }
  const createPropListener =
    (prop: string | symbol): Listener =>
    (op, nextVersion) => {
      const newOp: Op = [...op]
      newOp[1] = [prop, ...(newOp[1] as Path)]
      notifyUpdate(newOp, nextVersion)
    }
  const propProxyStates = new Map<
    string | symbol,
    readonly [ProxyState, RemoveListener?]
  >()
  const addPropListener = (prop: string | symbol, propValue: unknown) => {
    const propProxyState =
      !refSet.has(propValue as object) && proxyStateMap.get(propValue as object)
    if (propProxyState) {
      if (import.meta.env?.MODE !== 'production' && propProxyStates.has(prop)) {
        throw new Error('prop listener already exists')
      }
      if (listeners.size) {
        const remove = propProxyState[2](createPropListener(prop))
        propProxyStates.set(prop, [propProxyState, remove])
      } else {
        propProxyStates.set(prop, [propProxyState])
      }
    }
  }
  const removePropListener = (prop: string | symbol) => {
    const entry = propProxyStates.get(prop)
    if (entry) {
      propProxyStates.delete(prop)
      entry[1]?.()
    }
  }
  const addListener = (listener: Listener) => {
    listeners.add(listener)
    if (listeners.size === 1) {
      propProxyStates.forEach(([propProxyState, prevRemove], prop) => {
        if (import.meta.env?.MODE !== 'production' && prevRemove) {
          throw new Error('remove already exists')
        }
        const remove = propProxyState[2](createPropListener(prop))
        propProxyStates.set(prop, [propProxyState, remove])
      })
    }
    const removeListener = () => {
      listeners.delete(listener)
      if (listeners.size === 0) {
        propProxyStates.forEach(([propProxyState, remove], prop) => {
          if (remove) {
            remove()
            propProxyStates.set(prop, [propProxyState])
          }
        })
      }
    }
    return removeListener
  }
  let initializing = true
  const handler = createHandler<T>(
    () => initializing,
    addPropListener,
    removePropListener,
    notifyUpdate,
  )
  const proxyObject = newProxy(baseObject, handler)
  proxyCache.set(baseObject, proxyObject)
  const proxyState: ProxyState = [baseObject, ensureVersion, addListener]
  proxyStateMap.set(proxyObject, proxyState)
  Reflect.ownKeys(baseObject).forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(
      baseObject,
      key,
    ) as PropertyDescriptor
    if ('value' in desc && desc.writable) {
      proxyObject[key as keyof T] = baseObject[key as keyof T]
    }
  })
  initializing = false
  return proxyObject
}

 

 

1. 유효성 검사 및 캐시 여부 확인

proxy의 시작부분에서 우선 proxy의 매개변수로 들어오는 값이 객체인줄 확인하고, 캐쉬 여부를 합니다. 동일한 객체에 대해서 캐시를 한 경우에는   캐시 된 값을 얼리리턴합니다.

  if (!isObject(baseObject)) {
    throw new Error('object required')
  }
  const found = proxyCache.get(baseObject) as T | undefined
  if (found) {
    return found
  }

 

2. 버전 관리 및 리스너 등록을 위한 변수 선언

  let version = versionHolder[0]
  const listeners = new Set<Listener>()
  let checkVersion = versionHolder[1]

version은 객체의 현재 버전을 저장하는 변수로, 상태가 업데이트 될 경우 버전이 증가합니다. 이후 checkversion 변수를 활용하여 상태 변경 여부를 확인하고 필요한 경우에만 동기화합니다.

 

listeners는 해당 상태 업데이트시 등록된 리스너에게 알리기 위해서 사용하는 변수입니다.

 

3. 프록시 생성 

let initializing = true
  const handler = createHandler<T>(
    () => initializing,
    addPropListener,
    removePropListener,
    notifyUpdate,
  )
  const proxyObject = newProxy(baseObject, handler)
  proxyCache.set(baseObject, proxyObject)
  const proxyState: ProxyState = [baseObject, ensureVersion, addListener]
  proxyStateMap.set(proxyObject, proxyState)
  Reflect.ownKeys(baseObject).forEach((key) => {
    const desc = Object.getOwnPropertyDescriptor(
      baseObject,
      key,
    ) as PropertyDescriptor
    if ('value' in desc && desc.writable) {
      proxyObject[key as keyof T] = baseObject[key as keyof T]
    }
  })
  initializing = false
  return proxyObject
}

 

이후 새 프록시 객체를  생성하고,  캐쉬 및 상태관리를 합니다. proxy내부 하위 객체에 대해서도 Reflect객체를 활용하여 proxy로 만들고  최종적으로는 프록시 객체를 반환합니다.

 

결국 프록시를 생성하는 것은 알겠고, 캐시를 통해 효율적 상태관리를 하고, 하위 객체에 대한 등록도 하는 것은 알겠는데

proxy를 만들면서 어떤식으로 객체를 핸들링하는지 파악하려면, handler에 사용된 함수들에 대해서 알 필요가 있습니다.

 

4. creatHandler함수

아래는 creatHandler 에 등록 creatHandlerDefault 함수입니다.

const createHandlerDefault = <T extends object>(
  isInitializing: () => boolean,
  addPropListener: (prop: string | symbol, propValue: unknown) => void,
  removePropListener: (prop: string | symbol) => void,
  notifyUpdate: (op: Op) => void,
): ProxyHandler<T> => ({
  deleteProperty(target: T, prop: string | symbol) {
    const prevValue = Reflect.get(target, prop)
    removePropListener(prop)
    const deleted = Reflect.deleteProperty(target, prop)
    if (deleted) {
      notifyUpdate(['delete', [prop], prevValue])
    }
    return deleted
  },
  set(target: T, prop: string | symbol, value: any, receiver: object) {
    const hasPrevValue = !isInitializing() && Reflect.has(target, prop)
    const prevValue = Reflect.get(target, prop, receiver)
    if (
      hasPrevValue &&
      (objectIs(prevValue, value) ||
        (proxyCache.has(value) && objectIs(prevValue, proxyCache.get(value))))
    ) {
      return true
    }
    removePropListener(prop)
    if (isObject(value)) {
      value = getUntracked(value) || value
    }
    const nextValue =
      !proxyStateMap.has(value) && canProxy(value) ? proxy(value) : value
    addPropListener(prop, nextValue)
    Reflect.set(target, prop, nextValue, receiver)
    notifyUpdate(['set', [prop], value, prevValue])
    return true
  },
})

매개변수로는 4가지를 받습니다

 

  • isInitializing
    객체가 초기화 상태인지 확인하는 함수
  • addPropListener
    특정 속성에 리스너를 추가하는 함수입니다.속성의 변경을 감지하고, 구독자에게 알림을 보내기 위해 사용
  • removePropListener
    속성에 연결된 리스너를 제거하는 함수
  • notifyUpdate
    속성 값이 변경되었을 때 변경 사항을 알리는 함수로 변경된 속성의 이름과 값, 이전 값 등을 구독자에게 전달함

 

 

또한 앞서 console.log에서 확인했듯이 creatHandler는  set 와 deleteProperty  두가지 핸들러 메서드를 반환하는 것을 확인할 수 있습니다.

이제 각각의 핸들러 메서드에서 어떤 작업이 진행되는지 알아봅시다.

  • deleteProperty
    1. 삭제하려는 속성의 기존 값을 Reflect.get으로 가져옴
    2. 그 다음, 해당 속성에 연결된 리스너를 제거함
    3. Reflect.deleteProperty를 통해 속성을 삭제
    4. 속성이 성공적으로 삭제되면, notifyUpdate를 호출하여 삭제 작업을 listener에게 알림
    5.삭제가 여부를 반환합니다.
  •  set
    1. 속성의 이전 값이 존재하는지 확인하고 초기화 중이 아니라면, Reflect.has로 해당 속성이 이미 있는지 확인하고, 이전 값을 가져옵니다.
    2.만약 이전 값과 새 값이 동일하거나, 새 값이 이미 캐시된 프록시 값과 동일하다면 true를 반환하고 설정 작업을 종료
    3. 새로운 값이 설정될 속성에 대해 기존 리스너를 제거함
    4.  새 값이 객체일 경우, getUntracked를 호출해 프록시되지 않은 원본 객체를 가져오거나, 새 값이 프록시 가능한 경       우 proxy()를 사용해 새로운 프록시로 변환.
    5. addPropListener를 통해 새 값에 대한 리스너를 추가
    6. Reflect.set을 호출하여 속성에 새로운 값을 설정
    7.  notifyUpdate를 호출해 속성 값이 변경되었음을 알림

결국 proxy는 캐시를 활용한 최적화에 대한 작업과 쓰기 등록 시 구독자에게 알려준다는 사실을 알았습니다. 하지만 

컴퍼넌트가 해당 객체의 어떤 속성을 사용하는지 어떻게 알고 리렌더링 시키는지에 대해서 알기 위해선 useSnapshot훅을 분석할 필요성이 있습니다.

 

useSnapshot

import {
  useCallback,
  useDebugValue,
  useEffect,
  useLayoutEffect,
  useMemo,
  useRef,
  useSyncExternalStore,
} from 'react'
import {
  affectedToPathList,
  createProxy as createProxyToCompare,
  isChanged,
} from 'proxy-compare'
import { snapshot, subscribe } from './vanilla.ts'
import type { Snapshot } from './vanilla.ts'


export function useSnapshot<T extends object>(
  proxyObject: T,
  options?: Options,
): Snapshot<T> {
  const notifyInSync = options?.sync
  // per-proxy & per-hook affected, it's not ideal but memo compatible
  const affected = useMemo(
    () => proxyObject && new WeakMap<object, unknown>(),
    [proxyObject],
  )
  const lastSnapshot = useRef<Snapshot<T>>()
  let inRender = true
  const currSnapshot = useSyncExternalStore(
    useCallback(
      (callback) => {
        const unsub = subscribe(proxyObject, callback, notifyInSync)
        callback() // Note: do we really need this?
        return unsub
      },
      [proxyObject, notifyInSync],
    ),
    () => {
      const nextSnapshot = snapshot(proxyObject)
      try {
        if (
          !inRender &&
          lastSnapshot.current &&
          !isChanged(
            lastSnapshot.current,
            nextSnapshot,
            affected,
            new WeakMap(),
          )
        ) {
          // not changed
          return lastSnapshot.current
        }
      } catch {
        // ignore if a promise or something is thrown
      }
      return nextSnapshot
    },
    () => snapshot(proxyObject),
  )
  inRender = false
  useLayoutEffect(() => {
    lastSnapshot.current = currSnapshot
  })
  if (import.meta.env?.MODE !== 'production') {
    condUseAffectedDebugValue(currSnapshot as object, affected)
  }
  const proxyCache = useMemo(() => new WeakMap(), []) // per-hook proxyCache
  return createProxyToCompare(currSnapshot, affected, proxyCache, targetCache)
}

 

proxy를 캐시하는 내용과 유효성 검사등의 내용에 대해서는 생략하겠습니다.

내부에서 어떻게 proxy의 변경을 감지하고 useSnapshot 훅의 리턴값을 사용만 하면 특정 상태에 대한 구독이 되는지에 대해서 알아보려고 합니다.

우선 proxy의 핸들러 메서드를 통해서 쓰기 및 삭제 시 listener들에게 알림이 간다는 사실을 확인했습니다.

이때 리스너가 알림을 받았을 때, 리렌더링을 하게 하는 훅이 useSyncExternalStore 훅입니다. 

 

useSysncstore의 훅은 3가지의 매개변수를 받을 수 있습니다.

const snapshot = useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot?)
  • subscribe: 하나의 callback 인수를 받아 store에 구독하는 함수입니다. store가 변경될 때, 제공된 callback이 호출되어 React가 getSnapshot을 다시 호출하고 (필요한 경우) 컴포넌트를 다시 렌더링하도록 해야 합니다. subscribe 함수는 구독을 정리하는 함수를 반환해야 합니다.
  • getSnapshot: 컴포넌트에 필요한 store 데이터의 스냅샷을 반환하는 함수입니다. store가 변경되지 않은 상태에서 getSnapshot을 반복적으로 호출하면 동일한 값을 반환해야 합니다. 저장소가 변경되어 반환된 값이 다르면 (Object.is와 비교하여) React는 컴포넌트를 리렌더링합니다.
  • optional getServerSnapshot: store에 있는 데이터의 초기 스냅샷을 반환하는 함수입니다. 서버 렌더링 도중과 클라이언트에서 서버 렌더링 된 콘텐츠의 하이드레이션 중에만 사용됩니다. 서버 스냅샷은 클라이언트와 서버 간에 동일해야 하며 일반적으로 직렬화되어 서버에서 클라이언트로 전달됩니다. 이 함수가 제공되지 않으면 서버에서 컴포넌트를 렌더링할 때 오류가 발생합니다.

이제 다시 useSnapshot 훅을 보면 1. useCallback으로 감싸진 subscribe부분, 2. 화살표 함수로된 getSnapshot, 
3. ()=>snapshot(proxyObject)인 getServerSnapshot 을 확인할 수 있습니다.

 const currSnapshot = useSyncExternalStore(
    useCallback(
      (callback) => {
        const unsub = subscribe(proxyObject, callback, notifyInSync)
        callback() // Note: do we really need this?
        return unsub
      },
      [proxyObject, notifyInSync],
    ),
    () => {
      const nextSnapshot = snapshot(proxyObject)
      try {
        if (
          !inRender &&
          lastSnapshot.current &&
          !isChanged(
            lastSnapshot.current,
            nextSnapshot,
            affected,
            new WeakMap(),
          )
        ) {
          // not changed
          return lastSnapshot.current
        }
      } catch {
        // ignore if a promise or something is thrown
      }
      return nextSnapshot
    },
    () => snapshot(proxyObject),
  )

 

 

1. subscribe 함수에서 전달된 callback에서는 proxyObject의 상태 변화를 감지하고 상태가 변경될 때마다 이 callback이 호출되어 새로운 스냅샷을 가져오고 컴포넌트를 리렌더링합니다.
2. getSnapshot 함수에 전달된 화살표 함수 내부에 있는 isChanged 함수는 상태가 변경되었는지 확인하는 역할을 합니다. 만약 상태가 변하지 않았으면 리렌더링을 방지합니다.

 

이제 proxy 객체가 변화가 되었을 때, 왜 컴퍼넌트가 리렌더링하는지는 알게 되었습니다. 하지만 아직 어떻게 특정 컴퍼넌트가 객체를 사용하고 있는지에 대해서는 언급이 없었습니다.

 

해당 내용은 createProxyToCompare 이란 별칭으로 사용된  proxy-compare 라이브러리의 createProxy 함수에 있습니다.

공식 문서에서 createProxy 함수에 대한 설명은 아래와 같습니다.

createProxy  함수는 최상위에서 프록시를 생성하고, 접근할 때 중첩된 객체들도 프록시로 만들어, get/has 프록시 핸들러를 통해 어떤 프로퍼티가 접근되었는지를 추적합니다.


해당 함수에 관한 내용은 방대하기에 소스코드 링크만 남겨두고,  어떻게 proxy 핸들러에서 접근을 감지하는지에 대한 부분만 간단하게 추츨하는지 알아보기 위해  proxy의 핸들러의 내용만 추출하면 아래와 같습니다.

 

 const handler: ProxyHandler<T> = {
    get(target, key) {
      if (key === GET_ORIGINAL_SYMBOL) {
        return origObj;
      }
      recordUsage(KEYS_PROPERTY, key);
      return createProxy(
        Reflect.get(target, key),
        state[AFFECTED_PROPERTY] as Affected,
        state[PROXY_CACHE_PROPERTY],
        state[TARGET_CACHE_PROPERTY],
      );
    },
    has(target, key) {
      if (key === TRACK_MEMO_SYMBOL) {
        recordObjectAsUsed();
        return true;
      }
      recordUsage(HAS_KEY_PROPERTY, key);
      return Reflect.has(target, key);
    },
    getOwnPropertyDescriptor(target, key) {
      recordUsage(HAS_OWN_KEY_PROPERTY, key);
      return Reflect.getOwnPropertyDescriptor(target, key);
    },
    ownKeys(target) {
      recordUsage(ALL_OWN_KEYS_PROPERTY);
      return Reflect.ownKeys(target);
    },
  };
  if (isTargetCopied) {
    handler.set = handler.deleteProperty = () => false;
  }
  return [handler, state] as const;
};

 

이떄 get이 proxy를 읽을 때 핸들링하는 내용입니다.  key에 접근할 때, recordUsage를 호출해 KEYS_PROPERTY와 함께 사용 기록을 남기고, 다시 createProxy를 호출하여 그 값을 프록시로 감쌉니다. 따라서 우리가 컴퍼넌트에서 useSnapshot훅의 반환 proxy를 사용하게 되면 get 핸들러 메서드에 의해 기록되어서 추후  상태가 변경되었을 때  관련 상태를 사용한 컴퍼넌트만 리렌더링이 되게 됩니다.

 

요약

리액트 훅을 활용한 마이크로 상태관리훅을 통해서 proxy와 Valtio의 상태 사용 추적이 어떻게 일어나는지에 대해서 알아봤습니다.

proxy를 통해서 원본 객체에 핸들링 메서드를 적용할 수 있는데, Valtio에서는 이를 통해서 proxy의 상태 변화를 감지하고 이를 기반으로 불변의 snapshot을 찍습니다. 이때 상태관리하는 프록시에서는 proxy의 핸들러 메서드 중 set과 deleteProperty를 통해 변경을 listener들에게 알립니다.

 

그리고 리엑트 컴퍼넌트에서는  useSnapshot 훅 내부의 useSyncExternalStore와 createProxy를 통해서 2차 가공된 proxy 객체를 얻습니다. 해당 객체는 원본 proxy가 상태 변화를 할 때마다 새로운 proxy가 형성되는 불변 객체이며 컴퍼넌트에서 가공된 proxy객체(추후 스냅샷이라 부름)를 사용할 경우  createProxy훅 내부에 있는 get 메서드에 의해서 사용이 감지되고 등록이 됩니다. 이후 proxy객체가 변경이되면 useSyncExternalStore훅에 의해서 이전 상태와 차이점이 있는지를 비교하고 있을 경우 리렌더링이 됩니다.

 

 

참고문헌 및 출처

리액트 훅을 활용한 마이크로 상태관리 (다이시 카토 지음)

https://github.com/pmndrs/valtio

 

GitHub - pmndrs/valtio: 🧙 Valtio makes proxy-state simple for React and Vanilla

🧙 Valtio makes proxy-state simple for React and Vanilla - pmndrs/valtio

github.com

 

https://ko.javascript.info/proxy#ref-1036

 

Proxy와 Reflect

 

ko.javascript.info

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Reflect

 

Reflect - JavaScript | MDN

Reflect 는 중간에서 가로챌 수 있는 JavaScript 작업에 대한 메서드를 제공하는 내장 객체입니다. 메서드의 종류는 프록시 처리기와 동일합니다. Reflect는 함수 객체가 아니므로 생성자로 사용할 수

developer.mozilla.org

https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Global_Objects/Proxy

 

Proxy - JavaScript | MDN

Proxy 객체를 사용하면 한 객체에 대한 기본 작업을 가로채고 재정의하는 프록시를 만들 수 있습니다.

developer.mozilla.org

https://www.npmjs.com/package/proxy-compare

 

proxy-compare

Compare two objects using accessed properties with Proxy. Latest version: 3.0.0, last published: 5 months ago. Start using proxy-compare in your project by running `npm i proxy-compare`. There are 39 other projects in the npm registry using proxy-compare.

www.npmjs.com

https://ko.react.dev/reference/react/useSyncExternalStore

 

useSyncExternalStore – React

The library for web and native user interfaces

ko.react.dev

 

+ Recent posts