반응성에 대하여

Vue의 핵심 기능 중 하나는 반응성 시스템입니다.

컴포넌트의 state는 반응형 자바스크립트 객체입니다.

state를 수정하면 화면이 갱신됩니다.


이러한 시스템은 state 관리를 직관적으로 만들지만, 몇 가지 문제를 피하기 위해 작동 방식을 이해하고 사용하는 것이 중요합니다.


반응성이란 무엇인가?


이 용어는 최근 프로그래밍 주제에서 꽤 많이 등장하지만 어떤 것을 의미할까요?

반응성이란, 데이터의 흐름과 변경사항의 전파에 중점을 둔 선언적 프로그래밍 패러다임입니다.


일반적으로 보여주는 좋은 예시는 Excel 스프레드시트입니다.

excel gif

excel

A2 셀은 = A0 + A1 이라는 수식이 작성되어 있으므로 스프레드시트는 3을 보여주고 있습니다.

사진처럼 A0을 업데이트 하거나, A1을 업데이트하면 A2 역시 자동으로 업데이트 됩니다.


하지만, 자바스크립트는 기본적으로 이렇게 작동하지 않습니다.

최대한 위 예시와 비슷하게 작성한다면:

let A0 = 1;
let A1 = 2;
let A2 = A0 + A1;

console.log(A2); // 3

A0 = 2;
console.log(A2); // 여전히 3

A0을 변경해도 A2는 자동으로 변경되지 않습니다.


그러면 자바스크립트에서는 어떻게 해야 할까요?

먼저, A2를 업데이트 하는 코드를 재실행 할 수 있도록 함수로 래핑해보겠습니다.

let A2;

function update() {
  A2 = A0 + A1;
}

그 다음 앞으로 사용할 몇 가지 용어 정리를 합시다.

  • update() 함수는 프로그램의 상태를 수정하기 때문에 side effect(여기서는 줄여서 effect라고 합니다.)를 만들어냅니다.
  • A0A1은 자신의 값이 effect에 활용되기 때문에 effect의 종속성이 됩니다. effect는 이 종속성에 대한 구독자라고 합니다.


우리에게 필요한 것은 A0 이나 A1(종속성)이 수정될 때 update() (effect)를 호출해주는 마법의 함수입니다.

whenDepsChange(update);

whenDepsChange() 함수에는 다음과 같은 task가 있습니다.

  1. 변수를 읽는 순간을 추적합니다. 예를 들자면 A0 + A1 수식을 계산(eval)할 때 A0, A1 변수 모두 읽는 순간을 감지합니다.
  2. effect를 실행하는 중에 변수를 읽으면 해당 effect를 해당 변수의 구독자로 만듭니다. 예를 들어, A0A1update()가 실행될 때 읽혀지기 때문에 update()는 첫번째 호출 후에 A0A1의 구독자가 됩니다.
  3. 변수가 변경될 때마다 감지합니다. 예를 들어, A0에 새 값이 할당되면 모든 구독자에게 재실행하도록 알립니다.


반응성이 vue에서 작동하는 방식


실제로 바닐라 자바스크립트에서 위 예제들과 같이 지역 변수의 읽기 쓰기를 추적하는 매커니즘은 없습니다.

하지만 객체의 프로퍼티를 읽거나 쓰는 것을 가로채는 것은 가능합니다.


자바스크립트에서 프로퍼티 접근을 가로채는 방법에는 getter/setterproxy 두 가지가 있습니다.

Vue 2는 브라우저 지원 제한으로 인해 getter/setter 만을 사용해왔지만, Vue 3에서 proxy는 반응 객체에 사용되고 getter/setter는 refs에 사용됩니다.


다음은 작동 방식을 보여주는 의사 코드입니다.

function reactive(obj) {
  return new Proxy(obj, {
    get(target, key) {
      **track(target, key)**
      return target[key]
    },
    set(target, key, value) {
      **trigger(target, key)**
      target[key] = value
    }
  })
}

function ref(value) {
  const refObject = {
    get value() {
      **track(refObject, 'value')**
      return value
    },
    set value(newValue) {
      **trigger(refObject, 'value')**
      value = newValue
    }
  }
  return refObject
}

이 코드로 인해서 fundamentals 섹션에서 설명한 반응 객체의 몇 가지 제한사항이 설명됩니다.

  • 반응성 객체를 구조 분해 하거나 프로퍼티를 로컬 변수에 할당하면 더 이상 proxy 객체의 get / set 트랩을 트리거 하지 않기 때문에 반응성이 “연결 해제” 됩니다.
  • reactive() 에서 반환된 proxy 객체는 원본과 동일하게 작동하지만 === 연산자를 사용하여 원본과 비교하면 다른 참조를 갖습니다.


이 부분 때문에 컴포저블 로직을 작성할 때 반응성이 해제되어 의도와 다르게 작동한 경험이 있어 proxy 객체 복사에 관한 코드 스니펫을 첨부합니다.

  • proxy 객체 복사 코드 스니펫

    let numbers = [0, 1, 2];
    
    numbers = new Proxy(numbers, {
      get(target, prop) {
        console.log("call get proxy trap !!");
    
        return target[prop];
      },
    });
    
    const cloneNumbers = { ...numbers };
    
    console.log(numbers[0]); // 0, call get proxy trap !!
    
    console.log(cloneNumbers[0]); // 0
    
    // proxy 객체는 getter/setter 접근자를 가로채는 점 때문에
    // Object.assgin()이나 구조분해 할당으로 복사가 되지 않습니다.
    // 또한 일반 객체와 다르게 특수한 객체이기에
    // Object.getOwnPropertyDescriptor(), Object.defineProperty()를
    // 활용한 복사도 적용되지 않았습니다.
    


이제 track() 함수 내부를 살펴보면, 현재 실행중인 effect가 있는 지 확인합니다.

존재하는 경우, 추적중인 프로퍼티에 대한 구독자를 조회하고 Set에 추가합니다.

// effect가 실행되기 전 설정됩니다. 이후에 다룹니다.
let activeEffect;

function track(target, key) {
  if (activeEffect) {
    const effects = getSubscribersForProperty(target, key);
    effects.add(activeEffect);
  }
}

effect 구독은 전역 WeakMap<target, Map<key, Set<effect>>> 자료구조에 저장됩니다.

프로퍼티에 대한 구독 effect Set이 발견되지 않은 경우 (처음으로 추적된 경우) 생성됩니다.

이것은 getSubscribersForProperty() 함수가 하는 일이며, 간단히 하기 위해 자세한 내용은 건너뜁니다.


trigger() 내부에서 프로퍼티에 대한 구독자를 다시 조회합니다.

그러나 이번에는 호출합니다.

function trigger(target, key) {
  const effects = getSubscribersForProperty(target, key);
  effects.forEach((effect) => effect());
}


마지막으로 whenDepsChange() 함수로 되돌아가봅시다.

function whenDepsChange(update) {
  const effect = () => {
    activeEffect = effect;
    update();
    activeEffect = null;
  };
  effect();
}

실제 업데이트를 실행하기 전에 자기 자신을 activeEffect로 설정하는 함수를 래핑합니다.

이것은 업데이트 중에 활성화된 effect를 찾아 track()이 호출되도록 합니다.


이 시점에서 종속성을 자동으로 추적하고 종속성이 변경될 때마다 다시 실행하는 effect를 만들었습니다. 이것을 Reactive Effect라고 부릅니다.


이해가 조금 어렵다면 track()trigger()whenDepsChange() 의 구체적인 작동방식이라고 보시면 될 것 같습니다. 구독자를 저장하는 곳이 전역 WeakMap이라는 점을 상기하면 이해에 도움이 될 것 같습니다.


원문: https://vuejs.org/guide/extras/reactivity-in-depth.html


© 2021. All rights reserved.