해당 게시물은 여기서도 볼 수 있다.
JS OOP 시리즈 2 : 프록시를 이용한 vue3 반응형 동작 원리 살펴보며 AOP 이해하기.
Reference
이 게시물은 https://ui.toast.com/weekly-pick/ko_20210112 게시물을 학습 목적으로 요약 정리한 내용이다.
Vue.js3의 반응형
targetMap<WeakMap>
은 반응형 객체가 될target
을 저장한다.depsMap<Map>
은 각 반응형 객체의 값이 되며, 여기엔target
의key
가 저장된다.dep<Set>
은 각key
가 변경될 때 실행될 코드를 저장하는컬렉션
이다.
참고 : weekMap
예시를 들어 설명하자면 (반응형 객체 {a:1})
- 반응형 객체({a:1})를 키로(weekMap은 Primitive Value를 키로 허용하지 않음) 의존성 맵 가져옴.
- targetMap =
{[{a:1}] : 의존성맵(depsMap)}
- targetMap =
- 해당 객체의 속성을 키로 의존성 맵에서 의존성 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: 저장된 코드를 실행
targetMap({반응형객체 : 속성의존성맵})에서 depsMap(속성의존성맵)을 찾고
트랩의 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
'BackEnd' 카테고리의 다른 글
IOC(Inversion of control : 제어의 역전) 컨테이너와 DI(Dependency Injection : 의존성 주입) 패턴. Service Locator 패턴. (0) | 2022.02.12 |
---|---|
JS OOP 시리즈 3 : 자바스크립트 데코레이터 이해하기 (0) | 2022.01.17 |
JS OOP 시리즈 1 : 메타 프로그래밍과 Proxy, Reflect 간단하게 알아보기 (0) | 2022.01.17 |
[우아한테크세미나] 190620 우아한객체지향 by 우아한형제들 개발실장 조영호님 요약 (0) | 2021.12.28 |
[KSUG Seminar] Growing Application - 2nd. 애플리케이션 아키텍처와 객체지향 (0) | 2021.12.27 |