이 글에서는 Vue2의 반응성을 직접 구현해 보며 동작원리를 살펴볼 것이다. Vue3의 반응성을 바로 살펴보지 않고 Vue2 먼저 하는 이유는 Vue2의 반응성을 알면 Vue3도 쉽게 이해할 수 있기 때문이다. 미리 스포를 하자면 Vue2와 Vue3의 반응성의 가장 큰 차이점은 Vue2는 Object.defineProperty
를 사용하여 반응성이 구현하고, Vue3는 Proxy
를 사용하여 반응성을 구현한다는 것이다.
Vue2 예제 코드
간단한 Vue2 코드이다. 이 글에서 구현해볼 샘플 코드이기도 하다. data
안에 price
와 count
값이 있다. Vue2는 Vue 인스턴스에 data
옵션으로 전달되는 객체의 모든 속성을 순회하며 Object.defineProperty
의 getter, setter를 사용하여 반응형을 구현한다.
data
안에 있는 price
와 count
값이 변하면 total
과 totalSaled
값이 자동으로 계산되는 코드다. price
와 count
에 반응성이 주입되어 있어서 total
과 totalSaled
값이 자동으로 계산되는 것이다. 그리고 total
과 totalSaled
도 반응성이 간접적으로 주입되게 된다.
data, total 선언
아래와 같이 data
와 total
정의되어있다.
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
함수를 호출해 주면 된다.
지금까지 짠 코드이다. totalFn
와 totalSaledFn
은 vue에서 computed 기능과 유사하다. 하지만 위 코드에는 매우 불편한 점 두 가지가 있다. 아마 눈치가 좋은 사람이라면 눈치챘을 것이다.
첫 번째는 계산하는 함수, 여기서는 totalFn
와 totalSaledFn
를 최초에 직접 한 번씩 수행해줘야 한다는 것이다. 만약 최초에 직접 수행하지 않는다면 최초의 total
과 totalSaled
값은 모두 0일 것이다. 두 번째는 dep에 계산 로직도 직접 push 해야 한다는 점이다.
어떻게 하면 이 두 가지도 자동으로 수행되게 할 수 있을까?
watcher
watcher
라는 함수를 만들었다. Composition API와 사용방법을 비슷하게 만들었다. watcher
함수는 수행되어야 할 함수를 인자로 받는다. 그리고 인자로 받은 함수를 한번 수행한 후에 dep 배열에 추가한다. 두 가지 역할을 watcher
함수에 위임함으로써 중복로직을 제거할 수 있다.
하지만 이렇게 했을 경우 함수에는 치명적인 단점이 있다. 만약 data
에 name
이라는 키가 있다면 어떻게 될까?
data.name
값을 변경하면 Object.defineProperty
의 set 함수가 실행되어 그 안에 있는 trigger 함수가 호출될 것이다. 즉, name
을 변경하면 total
과 totalSaled
값도 다시 계산된다. total
과 totalSaled
은 name
에 영향을 받지 않는 값이기 때문에 다시 계산될 필요가 없다. 그럼 어떻게 하면 이 단점을 해결할 수 있을까?
방법은 이외로 간단하다. dep
변수를 Object.defineProperty
에서 사용되는 value
값처럼 Object.keys(data).forEach
안에 선언하면 된다. dep
을 해당 클로저로 이동하면 dep
을 사용하는 track
과 trigger
함수도 같은 범위 안으로 이동되어야 한다.
이제 남은 건 watcher
안에서 track
함수를 처리하는 일이다. track
함수는 dep
과 같은 범위로 이동되었기 때문에 watcher
에서 실행될 수 없다. 아래처럼 바꿔보자
activeEffect
라는 전역변수를 하나 만들었다. 그리고 get
에서 track
을 실행하게 해줬다. 이렇게 하면 어떤 일이 일어날까.
watcher 함수를 호출하면 일어나는 일은 아래와 같다.
watcher
를 함수를 호출한다.activeEffect
변수에() => (total = data.price * data.count)
함수가 저장된다.() => (total = data.price * data.count)
함수가 호출된다.- data.price에 접근하면서 defineProperty의 get 함수가 호출된다.
- activeEffect에 값이 존재함으로 track 함수가 호출되면서
data.price
클로저에 있는dep
에total
계산 함수가 저장되고data.price
값이 반환된다. - data.count 접근하면서 get 함수가 호출된다.
5)
단계가 반복된다.
이제 data.name
값을 변경해도 total
과 totalSaled
이 다시 계산되지 않는다.
정리
이제 Vue2 공식 홈페이지에 있는 그림이 이해가 될것이다. Vue2의 반응성은 Object.defineProperty
를 사용해서 구현되었다. 위의 코드는 실제 Vue2 코드를 참고해서 핵심 부분만 구현해 본 코드이며 실제 코드는 훨씬 복잡하게 되어있다. 다음 글에서는 Vue3의 반응성에 대해서 알아보겠다.
이전글 : https://donggov.tistory.com/238
'개발' 카테고리의 다른 글
자바스크립트 every 함수 구현 (feat. ECMA-262) (1) | 2024.04.15 |
---|---|
자바스크립트 엔진과 런타임 차이 (20) | 2024.04.06 |
Vue 반응성 분석 (1) : 인트로 (Intro) (2) | 2024.02.21 |
jackson objeckMapper List 타입 역직렬화(deserialize) 분석 (0) | 2023.07.16 |
Sentry onpromise 설치 및 vue 애플리케이션 설정 (0) | 2023.06.18 |