본문 바로가기
vue

Vue - reactivity

by 초특급하품 2021. 5. 16.

Vue에서 데이터 바인딩을 사용하다 보면 편리하면서도 이게 어떻게 가능한가 싶은 생각이 든다. text라는 변수를 여러 곳에서 사용하더라도 text를 변경하면 그 모든 곳들이 자동으로 변경되니 말이다.

 

보통 상식에는 text가 변경되어도 다른 값들은 이미 계산된 결과를 들고 있기 때문에 영향을 받지 않는다. 결국 어디선가 다시 계산을 유도시켜야 하는데 이를 reactivity(반응형)이라고 한다.

 

다행히 javascript에는 이를 깔끔히 해결해주는 Object.defineProperty라는 문법이 존재한다.

defineProperty에서 특정 object의 getter와 setter를 override 시킬 수 있다.  그러면 관찰하고자 하는 변수와(x), 그 변수를 사용하는 다른 변수(y)들의 getter와 setter에 개입해서 그 연관관계를 관리할 수 있다.

 

이를 사용하면 다음과 같은 방법으로 reactivity를 구현할 수 있다.

  1. y = ax + b와 같이 의존성 있는 식을 정의한다.
  2. 식에 x가 사용되기 때문에 x의 getter에서 위 식을 저장한다.
  3. x의 setter에서 x가 변경되었을 때 저장해 놓은 함수를 실행시킨다.

 

 

그럼 간단한 reactivity부터 일반화해 나가면서 하나씩 코드로 알아보자.

let data = { x: 10 }

Object.defineProperty(data, 'x', {
  get() {
    return data.x
  },
  set(value) {
    data.x = value
  }
})

단순히 getter, setter를 재정의 만 하고 동작은 바꾸질 않았다. 

예시로 든 코드지만 사실 함정이 있다. getter에서 data.x로 다시 getter를 호출하기 때문에 무한히 호출한다. 실행해보면 이런 에러가 발생한다.

Uncaught RangeError: Maximum call stack size exceeded

 

따라서 data.x를 임시 변수에 담도록 변경한다.

let data = { x: 10 }
let tmpValue = data.x

Object.defineProperty(data, 'x', {
  get() {
    return tmpValue
  },
  set(value) {
    tmpValue = value
  }
})

 

여기에 data.y 변수를 추가해서 x에 의존적인 값을 넣어본다.

func = (x) => 2 * x + 3
let data = { x: 10, y: func(10) }
let tmpValue = data.x

Object.defineProperty(data, 'x', {
  get() {
    return tmpValue
  },
  set(value) {
    tmpValue = value
    data.y = func(tmpValue)
  }
})

x의 setter로 인해 y가 자동으로 갱신된다.

 

 

지금은 관찰할 변수가 x, y 뿐이고 그 식도 간단하기 때문에 위와 같은 방법으로 해도 문제 될 건 없다. 하지만 관찰할 변수와 관련 식을 일반화시키고 더 분리시켜보자.

let data = { x: 10 }
let reactivityFunc

Object.keys(data).forEach(key => {
  let tmpValue = data[key]
  let dependencies = []
  Object.defineProperty(data, key, {
    get() {
      reactivityFunc && dependencies.push(reactivityFunc)
      return tmpValue
    },
    set(value) {
      tmpValue = value
      dependencies.forEach(f => f())
    }
  })
})

function addReactivity(f) {
  reactivityFunc = f
  f()
  reactivityFunc = null
}

addReactivity(() => data.y = 2 * data.x + 3)

 

data의 모든 값에 대해 getter, setter를 일반화시켜서 재정의했다. 또한 의존성 있는 reactivity 함수를 따로 빼서 그 함수에서 getter를 통해 denedencies 배열에 등록시켰다.

 

물론 아직도 코드에는 몇몇 문제점들이 있지만 Vue를 포함한 다른 라이브러리들이 어떻게 reactivity를 구현했는지 느낌을 받아갈 수 있을 것 같다.

'vue' 카테고리의 다른 글

Vue - 양방향 바인딩  (0) 2021.05.15

댓글