본문 바로가기
front/vue

순수자바스크립트로 뷰의 반응형 구현하기(ref, watchEffect, computed)

by juniKang 2022. 7. 12.

뷰의 가장 두드러지는 특징중 하나는 적당한 반응형 시스템이다. 뷰의 컴포넌트는 반응형 자바스크립트 객체여서, 상태가 수정되면, 화면이 업데이트된다. 반응형이 상태 관리를 직관적이고 간단하게 만들어주지만, 어떻게 동작하는지 모르면 흔한 함정에 빠질 수 있다. 이 장에서는 뷰의 반응형 시스템이 어떻게 동작하는지 조금 더 깊게 다룰 것이다.

 

반응형이란 무엇일까?

"반응형"은 프로그래밍에서 흔하게 등장하는데 어떤의미일까? 여기서 반응형이란, 우리가 반응형으로 선언한 어떤 변수를 사용하는 모든 곳에서, 변수의 값이 변함에 따라, 그 변수를 사용하는 모든 값들이 다시 계산되고, 화면에 나타난 변수도 재렌더링 되는 것이다. 흔한 예로, 엑셀 스프레드 시트를 들 수 있다.

  A B C
0 1    
1 2    
2 3 (A0+A1)    

A0과 A1에 넣은 값을 변경하면, A2는 자동으로 A0 + A1을 다시 계산한다.

 

그러나 자바스크립트에는 이런 기능이 없다. 위 예시를 코드로 표현한 아래 예제를 보자.

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

console.log(A2) // 3

A0 = 2
console.log(A2) // Still 3

A0을 2로 바꾸더라도, A2는 처음 할당값인 3에서 변하지 않는다.

 

그럼 자바스크립트에서는 어떻게 A2가 반응형으로 동작하도록 만들 수 있을까? 기본 타입 number, string 에는 변경을 추적하는 기능이 없다. 변경을 추적할 수 있도록, A0과 A1을 객체로 만들어준다.

const A0 = ref(1)
const A1 = ref(2)
let A2 = A0.value + A1.value

console.log(A2) // 3

A0.value = 2 // 안녕?

console.log(A2) // Still 3

function ref(value) {
  const obj = {
    get value() {
      return value
    },
    set value(newValue) {
      console.log("안녕?")
      value = newValue;
    },
  };
  return obj;
}

이제 A0과 A1은 value 프로퍼티를 가지는 객체가 되었다.  value 프로퍼티에 접근할 때와 값을 바꿀 때, 우리가 원하는 작업을 추가할 수 있다. 위 코드에서는 A0.value에 값을 할당 할 때, setter에 적어준 "안녕?"이 콘솔에 찍히는걸 볼 수 있다.

console.log("안녕?")을 입력한 저 공간에 반응형을 심어주는 코드를 넣어서 반응형을 구현할 수 있다.

 

(getter, setter 문법이 어렵다면 아래 링크를 읽어보자. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Working_with_Objects#defining_getters_and_setters)

 

반응형 정보(변수와 변수를 사용하는 함수들)를 넣어줄 배열을 선언한다. WeakMap은 키값이 Object인 배열이다. 키값으로 변수 ref 오브젝트를 넣고, 값으로 함수들을 저장할 것이다.

const effectSetWeakMap = new WeakMap();

// effectSetWeakMap에 저장될 정보 =======
[
  A0객체 : [A0을 사용하는 함수들...],
  A1객체 : [A1을 사용하는 함수들...],
  ...
]
// 이 배열을 보고 A0 객체가 사용되면, 함수들을 실행함

get value()가 실행될 때, 전역 배열(effectSetWeakMap) 에 함수를 등록해주는 track함수를 구현해보자.

const effectSetWeakMap = new WeakMap();
let activeEffect; // 현재 실행중인 함수를 기억하는 변수

function ref(value) {
  const obj = {
    get value() {
      track(obj); // 이 변수를 사용중인 함수를 추적해서, 전역 배열에 해당 함수를 등록한다.
      return value
    },
    set value(newValue) {
      value = newValue;
      trigger(obj); // 전역 배열에 등록된 이 변수를 사용하는 함수들을 모두 실행한다.
    },
  };
  return obj;
}

function track(target) {
  // 현재 실행중인 함수가 있는지 확인한다.
  // 있으면,
  if(activeEffect) { 
    // effectSetWeakMap에서 obj를 키값으로 해당 element를 찾아온다.
    const effectSet = getEffectSet(target); 
    // 현재 실행중인 함수를 effectSetWeakMap에 등록한다.
    effectSet.add(activeEffect);
  }
}

function getEffectSet(target) {
  // 전역배열에서 변수객체를 키값으로 찾는다.
  cosnt findSet = effectSetWeakMap.get(target);
  // 찾으면 리턴하고,
  if (findSet) {
    return findSet;
  // 못찾으면 새로 만든다.
  } else {
    const newSet = new Set();
    effectSetWeakMap.set(target, newSet);
    return;
  }
}

이제 activeEffect에 함수가 들어있는 상태로 getter가 실행되면, 전역 배열에 키값으로 객체, 값으로 함수가 저장된다.

trigger를 구현해서, setter가 실행되면 저장된 함수를 모두 실행할 수 있다.

function trigger(target) {
  const effectSet = getEffectSet(target);
  effectSet.forEach((effect) => effect());
}

이것으로 ref에 대한 구현이 모두 끝났다. getter와 setter를 재정의해서, getter가 실행될 때, 함수를 전역배열에 등록하고, setter가 실행될 때 전역배열에 등록된 함수를 모두 실행한다. 이제 함수를 등록해보자. 함수를 등록하기 위해 vue의 watchEffect를 사용해보자.

const A0 = ref(1);
const A1 = ref(2);
const A2 = ref();

watchEffect(() => {
  A2.value = A0.value + A1.value;
});

watchEffect를 구현해보자.

let activeEffect;
...

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

watchEffect는 다음 순서로 실행된다.

1. activeEffect에 effect가 할당된다.

2. update() : ()=> {A2.value = A0.value + A1.value} 가 검사된다.

3. A0.value 의 getter가 실행되면서 activeEffect에 담긴 effect가 전역배열에 등록된다.
  => [A0객체 : [effect]]

4. A1.value 의 getter가 실행되면서  activeEffect에 담긴 effect가 전역배열에 등록된다.
  => [A0객체 : [effect], A1객체:[effect]]

5. A2.value의 setter가 실행되면서 전역배열에 등록된 A2객체의 이벤트가 실행되나, 등록된게 없어서 아무일도 일어나지 않는다.

6. activeEffect가 null이 된다.

 

이제 watchEffect에 담긴 함수는 A0, A1에 확실하게 등록이 되었다. 그래서 A0의 값을 바꾸면 A2의 값도 변하게 된다. 반응형 구현을 성공한 것이다. 처음 예제를 다시 시도해보면 반응형이 동작하는 것을 확인할 수 있다.

const A0 = ref(1)
const A1 = ref(2)
const A2 = ref()

watchEffect(()=>{
  A2.value = A0.value + A1.value
})

console.log(A2.value) // 3

A0.value = 2

console.log(A2.value) // now 4

 

 


다음과 같이 computed를 사용하는게 더 간결해보인다.

const A0 = ref(1);
const A1 = ref(2);
const A2 = computed(() => A0.value + A1.value);

computed는 다음과 같이 구현할 수 있다.

function computed(callback) {
  let innerValue;

  const effect = () => {
    activeEffect = effect;
    innerValue = callback();
    console.log("computed계산됨");
    activeEffect = null;
  };

  effect();

  return {
    get value() {
      return innerValue;
    },
  };
}

'front > vue' 카테고리의 다른 글

[vue3] checkbox specification  (2) 2022.07.25
[eslint] vue3 eslint 설정  (0) 2022.07.23
TS with Composition API  (0) 2022.05.09
[vue3 공식문서 번역]Reusability.3.Plugins  (0) 2022.05.05
[vue3 공식문서 번역]Reusability.2.Custom Directives  (0) 2022.05.05

댓글