개발

Vue 반응성 분석 (2) : Vue2 반응성 구현

동고킴 2024. 2. 25. 15:59
반응형

이 글에서는 Vue2의 반응성을 직접 구현해 보며 동작원리를 살펴볼 것이다. Vue3의 반응성을 바로 살펴보지 않고 Vue2 먼저 하는 이유는 Vue2의 반응성을 알면 Vue3도 쉽게 이해할 수 있기 때문이다. 미리 스포를 하자면 Vue2와 Vue3의 반응성의 가장 큰 차이점은 Vue2는 Object.defineProperty를 사용하여 반응성이 구현하고, Vue3는 Proxy를 사용하여 반응성을 구현한다는 것이다.

 

Vue2 예제 코드

간단한 Vue2 코드이다. 이 글에서 구현해볼 샘플 코드이기도 하다. data 안에 pricecount 값이 있다. Vue2는 Vue 인스턴스에 data 옵션으로 전달되는 객체의 모든 속성을 순회하며 Object.defineProperty의 getter, setter를 사용하여 반응형을 구현한다.

data 안에 있는 pricecount 값이 변하면 totaltotalSaled 값이 자동으로 계산되는 코드다. pricecount에 반응성이 주입되어 있어서 totaltotalSaled 값이 자동으로 계산되는 것이다. 그리고 totaltotalSaled도 반응성이 간접적으로 주입되게 된다.

 

data, total 선언

아래와 같이 datatotal 정의되어있다.

price가 변경되었을 때 total이 자동으로 계산되길 원한다면 어떻게 해야 할까?

방법은 간단하다. price가 변경될 때마다 total을 다시 계산해 주면 된다. total을 계산하는 totalFn 함수를 만들어서 실행해 주자.

 

하지만 이 방법이 문제점은 price가 변할 때마다 매번 total 값을 다시 계산해줘야 한다는 것이다. 이 문제점을 해결하려면 price가 변할때마다 total 값을 다시 계산해 주도록 하면 된다. 어떻게 할 수 있을까? Vue2에서는 Object.defineProperty를 사용해서 이 문제점을 해결하였다.

 

Object.defineProperty 사용

Object.defineProperty 함수를 사용하면 객체의 속성을 세밀하게 추가하거나 수정할 수 있다. 이 함수에 대한 자세한 설명은 MDN 문서를 참고하기 바라며, 이 글에서는 코드를 작성하기 위해 알아야 하는 내용들만 간단히 다루겠다.

Object.defineProperty의 구문은 아래와 같다.

 

obj는 객체, prop은 새로 정의하거나 수정하려는 속성의 이름 또는 Symbol, 마지막 파라미터는 descriptor로 속성 서술자라고 부른다. 속성 서술자(descriptor)는 데이터 서술자(data descriptors)와 접근자 서술자(accessor descriptors) 두 가지 형식을 취할 수 있다.

 

예를 들어 위에서 obj.price는 definePropery에서 value로 지정한 10이며, writable가 false 이기 때문에 obj.price = 20 이렇게 값을 변경할 수 없다. value 데이터 서술자는 실제로 Vue2의 computed에서 사용된다.

 

getter, setter를 설정한 예제이다. 참고로 value와 함께 사용할 수 없다. 이렇게 설정하면 obj.price에 값을 설정하면 set이 호출되고, obj.price 값을 조회하면 get이 호출된다. Vue2는 이 접근자 서술자를 활용해서 반응성을 구현하였다.

 

set 함수에 totalFn 함수 호출 로직을 추가했다. definePropery의 get/set 안에서 data.price를 직접 호출하면 재귀호출로 인해 Maximum call stack size 에러가 발생하기 때문에 별도 변수 value를 선언했다. 그리고 price의 set이 호출될 때마다 totalFn 함수를 호출해 주었다.

price가 변경될 때뿐만 아니라 count가 변경될 때도 total 값을 다시 계산해줘야 한다. 동일한 기능을 캡슐화를 시켜보자.

 

이제 count가 변할 때에도 total이 자동으로 계산된다.

만약 total 외에 할인된 가격을 계산해 주는 totalSaled 값 계산 로직을 넣어보자. 그리고 이런 로직들이 여러 개 있다고 가정해보자. 그럼 코드가 아래처럼 set에 계속 추가될 것이다.

 

이 문제를 해결하는 방법은 간단하다. 수행해야 하는 함수들을 저장하는 변수를 선언한 후, 그 변수에 함수들을 저장해 놓는다. 그리고 set 안에서는 그 변수에 저장된 함수들을 모두 수행하기만 하면 된다.

 

dep 변수는 자동으로 수행되어야 할 함수들을 저장하는 변수이다.
track 함수는 dep 변수에 함수를 저장하는 함수이다.
trigger 함수는 dep에 저장된 함수들을 모두 수행하는 함수이다. 이제 Object.defineProperty의 set 안에서 trigger 함수를 호출해 주면 된다.

 

지금까지 짠 코드이다. totalFntotalSaledFn은 vue에서 computed 기능과 유사하다. 하지만 위 코드에는 매우 불편한 점 두 가지가 있다. 아마 눈치가 좋은 사람이라면 눈치챘을 것이다.

첫 번째는 계산하는 함수, 여기서는 totalFntotalSaledFn를 최초에 직접 한 번씩 수행해줘야 한다는 것이다. 만약 최초에 직접 수행하지 않는다면 최초의 totaltotalSaled 값은 모두 0일 것이다. 두 번째는 dep에 계산 로직도 직접 push 해야 한다는 점이다.

어떻게 하면 이 두 가지도 자동으로 수행되게 할 수 있을까?

 

watcher

watcher라는 함수를 만들었다. Composition API와 사용방법을 비슷하게 만들었다. watcher 함수는 수행되어야 할 함수를 인자로 받는다. 그리고 인자로 받은 함수를 한번 수행한 후에 dep 배열에 추가한다. 두 가지 역할을 watcher 함수에 위임함으로써 중복로직을 제거할 수 있다.

하지만 이렇게 했을 경우 함수에는 치명적인 단점이 있다. 만약 dataname이라는 키가 있다면 어떻게 될까?

 

data.name 값을 변경하면 Object.defineProperty의 set 함수가 실행되어 그 안에 있는 trigger 함수가 호출될 것이다. 즉, name을 변경하면 totaltotalSaled 값도 다시 계산된다. totaltotalSaledname에 영향을 받지 않는 값이기 때문에 다시 계산될 필요가 없다. 그럼 어떻게 하면 이 단점을 해결할 수 있을까?

 

방법은 이외로 간단하다. dep 변수를 Object.defineProperty에서 사용되는 value 값처럼 Object.keys(data).forEach 안에 선언하면 된다. dep을 해당 클로저로 이동하면 dep을 사용하는 tracktrigger 함수도 같은 범위 안으로 이동되어야 한다.

 

이제 남은 건 watcher 안에서 track 함수를 처리하는 일이다. track 함수는 dep과 같은 범위로 이동되었기 때문에 watcher에서 실행될 수 없다. 아래처럼 바꿔보자

 

activeEffect라는 전역변수를 하나 만들었다. 그리고 get에서 track을 실행하게 해줬다. 이렇게 하면 어떤 일이 일어날까.

watcher 함수를 호출하면 일어나는 일은 아래와 같다.

  1. watcher를 함수를 호출한다.
  2. activeEffect 변수에 () => (total = data.price * data.count) 함수가 저장된다.
  3. () => (total = data.price * data.count) 함수가 호출된다.
  4. data.price에 접근하면서 defineProperty의 get 함수가 호출된다.
  5. activeEffect에 값이 존재함으로 track 함수가 호출되면서 data.price 클로저에 있는 deptotal 계산 함수가 저장되고 data.price 값이 반환된다.
  6. data.count 접근하면서 get 함수가 호출된다.
  7. 5) 단계가 반복된다.

이제 data.name 값을 변경해도 totaltotalSaled이 다시 계산되지 않는다.

 

정리

이제 Vue2 공식 홈페이지에 있는 그림이 이해가 될것이다. Vue2의 반응성은 Object.defineProperty를 사용해서 구현되었다. 위의 코드는 실제 Vue2 코드를 참고해서 핵심 부분만 구현해 본 코드이며 실제 코드는 훨씬 복잡하게 되어있다. 다음 글에서는 Vue3의 반응성에 대해서 알아보겠다.

 

이전글 : https://donggov.tistory.com/238

 

반응형