본문 바로가기

BackEnd

JS OOP 시리즈 2 : 프록시를 이용한 vue3 반응형 동작 원리 살펴보며 AOP 이해하기.

반응형

해당 게시물은 여기서도 볼 수 있다.

JS OOP 시리즈 2 : 프록시를 이용한 vue3 반응형 동작 원리 살펴보며 AOP 이해하기.

Reference

이 게시물은 https://ui.toast.com/weekly-pick/ko_20210112 게시물을 학습 목적으로 요약 정리한 내용이다.

Vue.js3의 반응형

vue.js 반응형

  1. targetMap<WeakMap>은 반응형 객체가 될 target을 저장한다.
  2. depsMap<Map>은 각 반응형 객체의 값이 되며, 여기엔 targetkey가 저장된다.
  3. dep<Set>은 각 key가 변경될 때 실행될 코드를 저장하는 컬렉션이다.

참고 : weekMap

예시를 들어 설명하자면 (반응형 객체 {a:1})

  • 반응형 객체({a:1})를 키로(weekMap은 Primitive Value를 키로 허용하지 않음) 의존성 맵 가져옴.
    • targetMap = {[{a:1}] : 의존성맵(depsMap)}
  • 해당 객체의 속성을 키로 의존성 맵에서 의존성 Set을 가져옴.
    • 의존성맵(depsMap) = {a : Set(의존성들)}
  • 해당 속성에 등록된 의존성들을 전부 실행함.
    • [...Set(의존성들)].foreach(anyFn)

코드 흐름 따라가기

track: 반응형으로 실행할 코드를 저장

사용자가 반응형 속성에 접근하면 해당 함수를 실행한다.

const targetMap = new WeakMap();

function track(target, key) {
  if (!activeEffect) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
}

activeEffect는 리스너(옵저버, 섭스크라이버)의 중복 등록을 막는다.
밑부분은 설명을 위해 간략하게 되어 있으나,
모듈 내에서 effect 함수를 리터럴로 선언하는 경우 등을 대비한 처리를 하고 있을 것이다.
(마치 리액트의 useRef 같이 말이다.)
일단 흐름만 이해하고 가자.

/** 모듈 내 전역 변수 */
let activeEffect = null;

function effect(fn) {
  activeEffect = fn;
  fn();
  activeEffect = null;
}

/** 반응형 객체 생성 - 추후 설명*/
const numbers = reactive({a: 1, b: 2});
/** 반응형으로 실행할 코드 : 반응형 객체의 속성 접근*/
const sumFn = () => { sum = numbers.a + numbers.b; };
const multiplyFn = () => { multiply = numbers.a * numbers.b };

let sum = 0;
let multiply = 0;

effect(sumFn);
effect(multiplyFn);

console.log(`sum: ${sum}, multiply: ${multiply}`); // 3, 2
numbers.a = 10;
console.log(`sum: ${sum}, multiply: ${multiply}`); // 12, 20

trigger: 저장된 코드를 실행

  1. targetMap({반응형객체 : 속성의존성맵})에서 depsMap(속성의존성맵)을 찾고

  2. 트랩의 key(속성)를 통해 dep(의존성들)에서 실행할 코드를 찾고 실행한다.

    function trigger(target, key) {
    // 반응형 객체로 속성의존성맵 가져옴
    const depsMap = targetMap.get(target);
    if (!depsMap) {
     return;
    }
    // 해당 속성의 의존성들을 가져옴
    const dep = depsMap.get(key);
    // 의존성 들을 전부 실행함.
    if (dep) {
     dep.forEach(effect => effect());
    }
    }

    reactive: 반응형 객체 생성

    프록시는 track(\w get trap)과 trigger(\w set trap)를 호출한다.
    proxy는 Object.defineProperty와 달리 원본 객체를 수정하지 않으며,
    객체 내의 새로운 속성이 추가되는 등의 변화 또한 감지할 수 있다는 장점이 있다.

    function reactive(target) {
    const proxy = new Proxy(
     target,
     {
       // track(실행할 코드 저장)
       get(target, key, receiver) {
         // 호출된 this를 기억하기 위해 receiver를 넘겨준다.
         const res = Reflect.get(target, key, receiver);
         track(target, key);
    
         return res;
       },
       // trigger(저장된 코드를 실행)
       set(target, key, value, receiver) {
         const oldValue = target[key];
         const res = Reflect.set(target, key, value, receiver);
         // 값이 바뀌었으면 통보.
         if (oldValue !== res) {
           trigger(target, key, value, oldValue);
         }
         return res;
       }
     }
    )
    return proxy;
    }

    API 사용자의 관점

    이제 코드의 흐름을 다 알았으니 실행 부분을 다시 보고 가자
    우리는 effect, reactive라는 함수만 import해서 사용한다.

    const numbers = reactive({a: 1, b: 2});
    const sumFn = () => { sum = numbers.a + numbers.b; };
    const multiplyFn = () => { multiply = numbers.a * numbers.b };
    

let sum = 0;
let multiply = 0;

effect(sumFn);
effect(multiplyFn);

console.log(sum: ${sum}, multiply: ${multiply});
numbers.a = 10;
console.log(sum: ${sum}, multiply: ${multiply});

1. reactive({a: 1, b: 2})로 반응형 참조를 생성한다.
2. effect 함수에 sumFn을 전달하여 실행한다.
    - sumFn이 activeEffect에 할당된다.
    - sumFn을 실행한다.
3. reactive의 get 트랩이 호출된다 (numbers.a / numbers.b)
    - a의 처리는 다음과 같다.
      - res로 a의 값을 가져온다.
      - track으로 sumFn 함수를 a 속성의 의존성 배열에 등록한다. (activeEffect 존재)
    - 그 다음 b가 처리되는데, activeEffect의 참조(sumFn)가 동일하므로 중복 등록되지 않는다.
4.multiFlyFn의 처리는 2,3과 동일하다.
5. numbers.a가 변경되면 a에 할당된 `deps<Set>`를 찾아 들어있는 모든 함수를 실행한다.
    - reactive의 set 트랩 > trigger

# 마도구
해당 기능은 AOP 관점에서 `reactive`라는 기능을 추가했다.
즉 우리는 API 사용자 입장에서 두 가지만 제공하였다.
- {a: 1, b: 2}라는 객체.
- 해당 객체의 속성을 이용하는 함수.

해당 객체와 함수는 각각 reactive, effect에 인자로 넘겨져, 내부에서 호출 및 사용된다.
이들은 해당 함수에 대해 전혀 모른다. 그저 인자로 넘겨질 뿐이다.
(이를 의존성 주입(Dependency Injection)이라 한다.)
그런데도 마법같이 reactive라는 기능이 추가되었다.

이와 같이 프록시를 이용하면 마법처럼 로깅, 캐싱 등의 기능을 추가할 수 있다.

캐싱, 로깅과 같이 특정 로직을 각각 애플리케이션의`관점`으로 생각하고, 
그 관점을 기준으로 각각 모듈화하는 것을 AOP라 한다.

위의 reactive 모듈 또한 반응성 관점으로 모듈화된 것이다.

다음번에는 자바스크립트 데코레이터를 알아볼 것이다.


참고 :
https://engkimbs.tistory.com/746
https://ui.toast.com/weekly-pick/ko_20170313




반응형