(준)공식 문서/Vue.js

[ Vue.js 3 공식 문서 ] 2. Essentials - Reactivity Fundamentals ( 반응성 기본 원칙 )

Je-chan 2022. 2. 18. 17:12

 

[ Reference ] 

https://vuejs.org/guide/essentials/reactivity-fundamentals.html

 

Reactivity Fundamentals | Vue.js

 

vuejs.org

 

  공식 문서에서는 예시 코드를 Options API, Composition API 둘 다 지원하고 있다. 나는 개인적으로 Composition API 를 더 선호하고 있기 때문에 Composition API 로 예시 코드를 가져오도록 하겠다.

 

1. Declaring Reactive State

  ** reactive( ) ** 함수를 사용하게 되면 반응성이 있는 객체나 배열을 만들 수 있다. 

 

import { reactive } from 'vue'

const state = reactive({ count: 0 })

  반응성이 있는 객체는 javascript Proxy 개념을 기반으로 하며 기본 동작은 일반 객체와 같다. 차이점이 있다고 하면 Vue 는 반응성 있는 객체에 접근하는 속성 값들이나 변화를 추적할 수 있다는 점이다. 

 

  컴포넌트의 Template 에서 반응성 있는 상태를 사용하기 위해서는 먼저 ** setup ** 이라는 함수를 리턴해야 한다. 

 

// script
import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    return {
      state
    }
  }
}
// template
<div>{{ state.count }}</div>

  

  이와 같은 방식으로 같은 스코프 내에서 반응성 있는 상태를 변형함수를 선언하고 상태와 함께 return 할 수 있다.

import { reactive } from 'vue'

export default {
  setup() {
    const state = reactive({ count: 0 })

    function increment() {
      state.count++
    }

    // 이렇게 리턴을 해줘야만 template 에서 사용 가능하다
    return {
      state,
      increment
    }
  }
}
<button @click="increment">
  {{ state.count }}
</button>

 

  그러나! 이제는 달라졌다. 이전에는 위와 같이 setup( ) 을 통해서 수동으로 template 에서 사용할 상태나 함수들을 내보낼 수 있었지만, 지금은 빌드를 사용하지 않을 때만 이렇게 사용하면 된다. SFC Component 를 사용한다면 ** <script setup> ** 만 가지고도 따로 return 을 해줄 필요가 없이 template 에서 자유롭게 사용할 수 있다. (* 이것이 이번에 vue3 가 정식 버전이 되고 개편되면서부터 사용할 수 있게 된 것) 

 

<script setup>
import { reactive } from 'vue'

const state = reactive({ count: 0 })

function increment() {
  state.count++
}
</script>

<template>
  <button @click="increment">
    {{ state.count }}
  </button>
</template>

 

  이렇게 setup 을 사용하는 게 많은 Vue 개발자들이 가장 많이 사용하는 방법이라고 한다.

 

DOM Update Timing

  반응성 있는 상태에 변화를 줄 때 DOM 은 자동으로 update 가 된다. 하지만, 그건 DOM 이 업데이트 되는 방식은 동기적으로 이뤄지는 것이 아니다. 다만, 업데이트 된 후에는 ** nextTick( ) ** 을 통해서 업데이트 된 내용에 접근을 할 수 있게 된다. 그렇게 되면 사용자가 변경한 상태 개수에는 상관 없이 각 컴포넌트는 한 번만 업데이트 하면 된다. (*  nextTick 과 관련된 건 나중에 Global API 를 정리할 때 살펴보도록 하겠다)

 

Deep Reactivity

  Vue 에서 상태는 기본적으로 반응성이 있다. 이건 중첩된 객체나 배열에 변화를 줄 때에도 반응성이 작용한다는 것을 의미한다. 

 

import { reactive } from 'vue'

const obj = reactive({
  nested: { count: 0 },
  arr: ['foo', 'bar']
})

function mutateDeeply() {
  // these will work as expected.
  obj.nested.count++
  obj.arr.push('baz')
}

 

  ** shallowReactive( ) ** 를 이용해서 반응성이 root-level 에서만 작용할 수 있도록 할 수는 있다. (* 이와 관련된 것도 나중에 Reactivity Advanced 에서 자세하게 다뤄 볼 예정)

 

Reactive Proxy VS Original

  중요하게 봐야할 것은 ** reactive( ) ** 를 통해서 반응성 있게 만든 값은 Proxy라는 점이다. 그리고 이건 원본 객체와 동일한 값이 아니다. Proxy 는 정말 반응성을 위해 존재하는 것이다. 즉, original 객체에 변화를 준다 하더라도 업데이트 되지 않다는 말이다. 그렇기 때문에 Vue 의 반응성을 최대한 잘 활용하기 위해서는 상태의 Proxy 버전을 단독으로 사용하는 것이다. 그래서 Proxy 에 대한 일관된 접근을 보장하기 위해 동일한 객체에서 ** reactive( ) ** 를 호출하면 항상 동일한 Proxy 가 반환되고 그 Proxy 에서 reactive( ) 를 호출해도 동일한 Proxy 가 반환된다. 이건 중첩된 객체에서도 적용된다. 깊은 반응성 때문에 반응성 있는 중첩된 객체 안에 존재하는 객체 또한 Proxy 다. 

 

const proxy = reactive({})

const raw = {}
proxy.nested = raw

console.log(proxy.nested === raw) // false

   

Limitations of reactive( )

  reactive( ) 는 두 가지의 한계가 존재한다

 

  첫 번째는 Object 타입에서만 작용한다는 점이다. 객체, 배열, Map, Set 등에서만 사용할 수 있다. 그렇기에 원시 자료형인 String, Number, Boolean 값은 사용할 수 없다

 

  두 번째는 Vue 가 반응성을 추적할 때 속성에 접근하므로, 우리는 반드시 반응성 객체에 대한 동일한 참조를 유지해야 한다는 점이다. 다시 말해, 반응형 객체를 다른 것으로 쉽게 대체할 수 없다는 의미다. 

 

let state = reactive({ count: 0 })

// 밑의 것은 동작하지 않는다
// reactive 내부의 값이 동일한 참조가 아니기 때문
state = reactive({ count: 1 })

  

  이 문제점은 우리가 반응성 있는 객체의 속성을 로컬 변수에 할당한다거나 없앨 때, 혹은 그 속성을 함수에 전달하거나 업애고 싶을 때 반응성을 끊어버려야 한다는 것을 의미한다. 

 

const state = reactive({ count: 0 })

// n 은 지역 변수
// state 의 값을 받아오지만 반응성은 없음

let n = state.count

// n에 변화를 주더라도 state 에 영향 주진 않음
n++

// 문제는 참조가 끊겨 반응성이 사라졌다.
let { count } = state

// 마찬가지로 state 에 영향을 줄 수 없다
count++

// 이 함수는 인자로 그냥 Number 를 받음
// state.count 의 변화를 추적하지는 못한다
callSomeFunction(state.count)

 


2. Reactive Variables with ref( ) 

  ** reactive( ) ** 에 대한 한계는 분명히 존재하기에 Vue 는 ** ref( ) ** 함수를 제공한다. ** ref( ) ** 는 모든 값의 유형을 저장하고 반응성이 있는 Ref 를 만들어 낸다. (* 이 블로그에서는 Ref 를 ref( ) 를 통해서 만들어낸 값을 의미한다)인자값으로 받은 것을 Ref 객체 내의 ** .value ** 라는 속성으로 감싸서 return 한다. 

 

  반응형 객체와 유사하게 Ref 의 ** .value ** 속성 또한 반응성을 지닌다. 더불어 Ref 가 객체 타입을 보유하고 있을 때는 자동적으로 .value 의 값을 ** reactive( ) ** 를 이용해서 반응성으로 만들어 준다.  또 reactive( ) 가 지니고 있던 문제점들을 보완해서 함수의 인자로 전달하거나 속성 값에 변화를 줄 때에도 반응성을 잃지 않는다.

 

const obj = {
  foo: ref(1),
  bar: ref(2)
}

// the function receives a ref
// it needs to access the value via .value but it
// will retain the reactivity connection
callSomeFunction(obj.foo)

// still reactive
const { foo, bar } = obj

 

  다른 말로 하자면 ** ref ( ) ** 는 어떤 값이든 Ref 를 만들어 내고 그 Ref 는 반응성을 잃지 않을 수 있도록 해준다. 이 기능으로 Composable Functions 를 만들기 때문에 매우 중요하다. 

 

Ref Unwrapping in Templates

  Ref 는 template 에서 최상위 속성으로 접근하는 경우 자동적으로 wrapping 이 해제가 된다. (* wrapping 해제를 한 꺼풀 벗기는 거라고 생각하면 좋을 것 같다. 예를 들어 obj.a 로 돼 있으면 a 가 현재 obj 에 의해 wrapping 된 거고, 여기서 wrapping 해제를 하면 a 만으로 쓸 수 있다는 것을 의미한다) 한 마디로 template 구문 안에서는 .value 를 사용하지 않아도 된다는 말이다.

 

<script setup>
import { ref } from 'vue'

const count = ref(0)

function increment() {
  count.value++
}
</script>

<template>
  <button @click="increment">
    {{ count }} <!-- no .value needed -->
  </button>
</template>

 

Ref Unwrapping Reactive Objects

  반응성이 있는 객체의 속성으로서 Ref  가 접근되거나 값이 바뀐다면, 그것 또한 자동으로 wrapping 이 해제되어 일반 속성처럼 동작할 수 있다. 

 

const count = ref(0)
const state = reactive({
  count
})

// 이거는 객체의 속성으로 접근된 것
console.log(state.count) // 0

// 이거는 ref 의 값이 바뀐 것
state.count = 1
console.log(count.value) // 1

 

  만약 기존 Ref 로 연결된 속성 중에서 새로운 Ref 가 할당이 된다면 이전 참조가 대체된다. 즉 반응성이 끊긴다는 것. 위의 코드와 연결해서 밑의 코드를 보면 된다. 

 

const otherCount = ref(2)

state.count = otherCount
console.log(state.count) // 2
// original ref is now disconnected from state.count
console.log(count.value) // 1

  

  이런 Ref 의 wrapping 해제는 깊은 반응성에서만 작용하는 것이고 ** shallowReactive( ) ** 를 이용해서 만든 Ref 에는 unwrapping 이 되지 않는다.

 

Ref Unwrapping in Arrays and Collections

  만약 반응성이 있는 것이 객체가 아니라 배열, Set, Map 이라면 unwrapping 이 되지 않는다. 

 

const books = reactive([ref('Vue 3 Guide')])
// need .value here
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// need .value here
console.log(map.get('count').value)