본문 바로가기

FrontEnd

Valtio의 프록시 상태관리가 어떻게 동작할까? (Vanila Part)

반응형

원문 : https://blog.axlight.com/posts/how-valtio-proxy-state-works-vanilla-part/

 

How Valtio Proxy State Works (Vanilla Part)

Introduction Valtio is a library for global state primarily for React. It’s originally modeled to match with useMutableSource API. However, it turns out it’s a novel API to add immutability to mutable state. What is immutable state? JavaScript doesn’

blog.axlight.com

Introduction

Valtio는 React의 전역 상태 라이브러리입니다.
원래 useMutableSource API와 일치하도록 모델링되었습니다.
그러나 변경 가능한 상태에 불변성을 추가하는 새로운 API임이 밝혀졌습니다.
불변 상태란? JavaScript는 언어로 불변성을 지원하지 않으므로 코딩 계약(컨트랙트, 스타일)일 뿐입니다.
const immutableState1 = { count: 0, text: 'hello' };

// update the state
const immutableState2 = { ...immutableState1, count: immutableState1.count + 1 };

// update it again
const immutableState3 = { ...immutableState2, count: immutableState2.count + 1 };

어떤 사람들은 이 패턴에 익숙할 수도 있고 다른 사람들에게는 낯설 수도 있습니다.
우리는 항상 기존 개체를 수정하지 않고 새 개체를 만듭니다. 이를 통해 상태 개체를 비교할 수 있습니다.
immutableState1 === immutableState2 // is false
immutableState2 === immutableState3 // is false

// decrement count
const immutableState4 = { ...immutableState3, count: immutableState3.count - 1 };

console.log(immutableState4); // shows "{ count: 1, text: 'hello' }"
console.log(immutableState2); // shows "{ count: 1, text: 'hello' }"

// however their references are different
immutableState2 === immutableState4 // is false

불변 상태의 이점은 상태 개체를 === 비교하여 내부에 변경된 항목이 있는지 알 수 있다는 것입니다.
변경할 수 없는 상태와 달리 변경 가능한 상태는 업데이트에 대한 코딩 계약이 없는 JavaScript 객체입니다.
const mutableState = { count: 0, text: 'hello' };

// update the state
mutableState.count += 1;

// update it again
mutableState.count += 1;

불변 상태와 달리 우리는 상태를 변경하고 동일한 객체를 유지합니다.
JavaScript 객체가 본질적으로 변경 가능한 방식이기 때문에 변경 가능한 상태는 처리하기가 더 쉽습니다.
 
변경 가능한 상태의 문제는 변경할 수 없는 상태의 이점의 이면입니다.
두 개의 변경 가능한 상태 개체가 있는 경우 모든 속성을 비교하여 내용이 동일한지 확인해야 합니다.
const mutableState1 = { count: 0, text: 'hello' };
const mutableState2 = { count: 0, text: 'hello' };

const isSame = Object.keys(mutableState1).every(
  (key) => mutableState1[key] === mutableState2[key]
);​

중첩된 개체에는 충분하지 않으며, 키 수가 다를 수 있습니다.
두 개의 변경 가능한 객체를 비교하려면 소위 deepEqual이 필요합니다.
deepEqual은 큰 개체에 대해 그다지 효율적이지 않습니다.
비교가 객체의 크기나 깊이에 의존하지 않기 때문에 불변 객체가 빛을 발합니다.
따라서 우리의 목표는 변경 가능한 상태와 변경 불가능한 상태 사이를 연결하는 것입니다.
보다 정확하게는 변경 가능한 상태에서 변경 불가능한 상태를 자동으로 생성하고자 합니다.

변형 감지 (Detecting mutation)

프록시는 개체 작업을 트랩하는 방법입니다. 우리는 변형을 감지하기 위해 set 핸들러를 사용합니다.
const p = new Proxy({}, {
  set(target, prop, value) {
    console.log('setting', prop, value);
    target[prop] = value;
  },
});

p.a = 1; // shows "setting a 1"

객체가 변형되었는지 추적해야 하므로 버전 번호가 있습니다.
let version = 0;
const p = new Proxy({}, {
  set(target, prop, value) {
    ++version;
    target[prop] = value;
  },
});

p.a = 10;
console.log(version); // ---> 1
++p.a;
console.log(version); // ---> 2

이 버전 번호는 객체 자체에 대한 것이며 어떤 속성이 변경되었는지는 상관하지 않습니다.
// continued
++p.a;
console.log(version); // ---> 3
p.b = 20;
console.log(version); // ---> 4

이제 돌연변이를 추적할 수 있으므로 다음은 불변 상태를 만드는 것입니다.

스냅샷 만들기(Creating snapshot)

변경 가능한 상태의 변경 불가능한 상태를 스냅샷이라고 합니다.
변형이 감지되면, 즉 버전 번호가 변경되는 경우 새 스냅샷을 생성합니다.
 
스냅샷 생성은 기본적으로 개체를 복사하는 것입니다.
단순화를 위해 객체가 중첩되지 않는다고 가정해 보겠습니다.
let version = 0;
let lastVersion;
let lastSnapshot;
const p = new Proxy({}, {
  set(target, prop, value) {
    ++version;
    target[prop] = value;
  },
});
const snapshot = () => {
  if (lastVersion !== version) {
    lastVersion = version;
    lastSnapshot = { ...p };
  }
  return lastSnapshot;
};

p.a = 10;
console.log(snapshot()); // ---> { a: 10 }
p.b = 20;
console.log(snapshot()); // ---> { a: 10, b: 20 }
++p.a;
++p.b;
console.log(snapshot()); // ---> { a: 11, b: 21 }

 
snapshot은 스냅샷 객체를 생성하는 기능입니다.
스냅샷 객체는 스냅샷이 호출될 때만 생성된다는 점에 유의하는 것이 중요합니다.
그때까지 우리는 원하는 만큼 변형을 수행할 수 있으며, 이는 버전만 증가시킵니다.

구독 (Subscribing)

이 시점에서 우리는 변형이 언제 발생할지 모릅니다.

상태가 변경되면 무언가를 하고 싶은 경우가 종종 있습니다. 이를 위해 구독이 있습니다.

let version = 0;
const listeners = new Set();
const p = new Proxy({}, {
  set(target, prop, value) {
    ++version;
    target[prop] = value;
    // 리스너에 알림
    listeners.forEach((listener) => listener());
  },
});

// 리스너에 등록
const subscribe = (callback) => {
  listeners.add(callback);
  const unsubscribe = () => listeners.delete(callback);
  return unsubscribe;
};

subscribe(() => {
  console.log('mutated!');
});

p.a = 10; // shows "mutated!"
++p.a; // shows "mutated!"
p.b = 20; // shows "mutated!"
스냅샷과 구독을 결합하면 가변 상태를 React에 연결할 수 있습니다.
다음 포스트에서 valtio가 React와 어떻게 작동하는지 소개할 것입니다.

중첩 객체 다루기 (Handling nested objects)

지금까지 우리의 예제는 속성이 primitive인 단순한 객체를 사용했습니다.
실제로 우리는 중첩된 객체를 사용하기를 원하며 이는 불변 상태의 이점입니다. 중첩 객체는 다음과 같습니다.
const obj = {
  a: { b: 1 },
  c: { d: { e: 2 } },
};
우리는 또한 배열을 사용하고 싶습니다.
Valtio는 중첩 객체와 배열을 지원합니다.
구현 방법에 관심이 있다면 소스 코드를 확인하십시오.
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 - GitHub - pmndrs/valtio: 💊 Valtio makes proxy-state simple for React and Vanilla

github.com

주 : Reflect.set을 이용해 객체일 경우 재귀처리를 함
원문 소스 링크는 저기지만 실제론 https://github.com/pmndrs/valtio/blob/main/src/vanilla.ts 에서 봐야 함

맺음말

이 블로그 게시물에서는 예제에서 간단한 코드를 사용합니다.
구현은 다양한 경우를 처리하기 위해 더 많은 작업을 수행합니다. 
실제 API는 예제 코드에 매우 가깝습니다. TypeScript의 대략적인 타입 정의는 다음과 같습니다.
function proxy<T>(initialObject: T): T;

function snapshot<T>(proxyObject: T): T;

function subscribe<T>(proxyObject: T, callback: () => void): () => void;
이 게시물에서 우리는 valtio의 바닐라 부분에 대해 논의했습니다. 조만간 리액트 파트에 대해 쓸 수 있기를 바랍니다.

https://itchallenger.tistory.com/632

 

Valtio의 프록시 상태관리가 어떻게 동작할까? (React Part)

원문 : https://blog.axlight.com/posts/how-valtio-proxy-state-works-react-part/ How Valtio Proxy State Works (React Part) Introduction In the previous article, we explained how Valtio proxy state wor..

itchallenger.tistory.com

 

반응형