[Vue 빠른시작] 반응형 기초 예제 추가(Composition API.ver)

이전글

[Vue 빠른시작] 반응형 기초 예제 추가(Options API.ver)



1. 반응형 상태 설정

'reative()' 함수를 사용하여 객체 또는 배열을 반응형으로 만들 수 있다.

import { reactive } from 'vue'

const state = reactive({ count: 0 })

반응형 객체는 JavaScript Proxy이며 일반 객체처럼 작동한다. 일반 객체와 차이점은 Vue가 속성에 접근 및 반응형 객체의 변경사항을 감지할 수 있다는 것이다.

컴포넌트의 템플릿에서 반응형 상태를 사용하려면, 컴포넌트의 'setup()' 함수에서 반응형 상태를 선언하고 반환해야 한다.(Vue3의 컴포지션 API는 setup 함수 내에서 사용이 가능하다.)

setup 함수 내에 자바스크립트로 코드를 작성하고, 객체 형식으로 반환한다.


import { reactive } from 'vue'

export default {
  // `setup`은 컴포지션 API에서만 사용되는 특별한 훅입니다.
  setup() {
    const state = reactive({ count: 0 })

    // 상태를 템플릿에 노출
    return {
      state
    }
  }
}
<div>{{ state.count }}</div>

마찬가지로 반응형 상태를 변경하는 함수를 같은 범위에서 선언하고 상태와 함께 메서드로 노출 할 수 있다.

import { reactive } from 'vue'

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

    function increment() {
      state.count++
    }

    // 함수를 반환하는 것을 잊지 마세요.
    return {
      state,
      increment
    }
  }
}
<button @click="increment">
  {{ state.count }}
</button>

노출된 메서드는 일반적으로 이벤트 리스너로 사용된다.



1-1. <script setup>

'setup()' 훅을 통해 상태와 메서드를 수동으로 노출하는 것은 장황할 수 있다. 다행히 빌드 방식을 사용하지 않을 때만 이러한 방법이 필요하다. 싱글 파일 컴포넌트(*.vue) 사용 시, <script setup>과 같이 표기만 하면 되므로 복잡성을 크게 단순화 할 수 있다.

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

const state = reactive({ count: 0 })

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

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

컴포넌트의 <script setup>에서 import 또는 최상위 레벨로 선언된 변수나 함수는 해당 템플릿에서 바로 사용할 수 있다.



1-2. DOM 업데이트 타이밍

반응 상태를 변경하면 DOM이 자동으로 업데이트 된다. 하지만 DOM 업데이트는 동기적으로 적용되지 않는다는 점에 유의해야 한다. 대신 Vue는 업데이트 주기의 "다음 틱"까지 버퍼링하여 얼마나 많은 상태 변경을 수행하든 각 컴포넌트가 한 번만 업데이트되도록 한다.

상태 변경 후, DOM 업데이트가 완료될 때까지 기다리려면 nextTick() 전역 API를 사용할 수 있다.

import { nextTick } from 'vue'

function increment() {
  state.count++
  nextTick(() => {
    // 업데이트된 DOM에 접근 가능
  })
}



1-3. 깊은 반응형(Deep Reactivity)

Vue는 기본적으로 반응형 상태를 내부 깊숙이 추적하므로, 중첩된 객체나 배열을 변경할 때에도 변경 사항이 감지된다.

import { reactive } from 'vue'

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

function mutateDeeply() {
  // 변경 사항이
  obj.nested.count++
  obj.arr.push('baz')
}

루트 수준에서만 반응성을 추적하는 얕은 반응형 객체를 명시적으로 생성할 수도 있지만, 이는 일반적으로 고급 사용 사례에서만 필요한 경우이다.



1-4. 반응형 재정의 vs 원본

'reactive()'의 반환 값은 원본 객체와 같지 않고 원본 객체를 재정의한 Proxy라는 점을 유의하는 것이 중요하다.

const raw = {}
const proxy = reactive(raw)

// 반응형으로 재정의 된 것은 원본과 같지 않습니다.
console.log(proxy === raw) // false

Proxy만 반응형이다. 원본 객체를 변경해도 업데이트가 트리거되지 않는다. 따라서 객체를 Vue의 반응형 시스템으로 작업할 때 가장 좋은 방법은 상태를 재정의한 프락시만 사용하는 것이다.

프락시에 대한 일관된 접근을 보장하기 위해, 원본 객체를 'reactive()'한 프락시와 프락시를 'reactive()'한 프락시는 동일한 프락시를 반환하도록 동작한다.

// 객체를 reactive() 한 반환 값과 프락시는 동일합니다.
console.log(reactive(raw) === proxy) // true

// 프락시를 reactive()한 반환 값과 프락시는 동일합니다.
console.log(reactive(proxy) === proxy) // true

이 규칙은 중첩된 객체에도 적용된다. 내부 깊숙이까지 반응형이므로 반응형 객체 내부의 중첩된 객체도 프락시이다.

const proxy = reactive({})

const raw = {}
proxy.nested = raw

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



1-5. 'reactive()'의 제한 사항

'reactive()' API는 두 개의 제한 사항이 있다.

  1. 객체, 배열 그리고 Map이나 Set과 같은 컬렉션 유형에만 작동한다.(string, number 또는 boolean과 같은 기본 유형에 사용할 수 없다.)
  2. Vue의 반응형 변경 감지는 속성에 접근함으로써 작동하므로, 항상 반응형 객체에 대한 동일한 참조를 유지해야 한다. 즉, 첫 번째 참조에 대한 반응형 연결이 손실되기 때문에 반응형 객체를 쉽게 '교체'할 수 없음을 의미한다.
    let state = reactive({ count: 0 })
    

// 위에서 참조한 ({ count: 0 })는 더 이상 추적되지 않습니다. (반응형 연결이 끊어졌습니다.)
state = reactive({ count: 1 })

또한 반응형 객체의 속성을 로컬 변수에 할당하거나 분해 할당 또는 함수에 전달할 때 반응형 연결이 끊어짐을 의미한다.
```js
const state = reactive({ count: 0 })

// n은 state.count에서 연결이 끊긴 로컬 변수입니다.
let n = state.count
// 원본의 상태(state.count)에 영향을 미치지 않습니다.
n++

// 로컬 변수 count는 state.count로부터 연결이 끊깁니다.
let { count } = state
// 원본의 상태(state.count)에 영향을 미치지 않습니다.
count++

// 함수는 일반적인 숫자를 수신하며,
// state.count의 변경 사항을 감지할 수 없습니다.
callSomeFunction(state.count)



2. ref()를 사용한 반응형 변수

Vue는 reactive()의 제한 사항을 해결하기 위해, 어떠한 유형의 데이터라도 반응형으로 재정의할 수 있는 'ref()'함수를 제공한다.

import { ref } from 'vue'

const count = ref(0)

ref()는 받은 인자를 .value 속성을 포함하는 ref 객체에 래핑 후 반환한다.

const count = ref(0)

console.log(count) // { value: 0 }
console.log(count.value) // 0

count.value++
console.log(count.value) // 1

반응형 객체의 속성과 유사하게 ref의 .value 속성은 반응형이다. 또한 객체 유형을 가지고 있는 경우, ref는 자동으로 .value를 reactive()로 변환한다.

ref가 값으로 객체를 가지는 경우, 객체 전체를 반응형으로 대체할 수 있다.

const objectRef = ref({ count: 0 })

// 이것은 반응형으로 작동합니다
objectRef.value = { count: 1 }

또한 반응형 상태로 함수에 전달되거나 분해 할당될 수 있다.

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

// 함수가 ref를 전달받습니다.
// .value를 통해 값에 접근해야 하지만
// 반응형 연결 상태가 유지됩니다.
callSomeFunction(obj.foo)

// 분해 할당했지만, 반응형 상태가 유지됩니다.
const { foo, bar } = obj

즉, ref()를 사용하면 모든 값에 대한 "참조"를 만들어 반응성을 잃지 않고 전달할 수 있다. 이 기능은 composables 함수로 로직을 추출할 때 자주 사용되기 때문에 상당히 중요하다.



note! 분해 할당?

위 설명에서 나온 분해 할당이 뭔지 몰라 알아봄

예제

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

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

// obj 객체에서 foo와 bar 속성을 추출하여 새로운 변수 foo와 bar를 선언하는 것
const { foo, bar } = obj

console.log('obj: ', obj);
console.log('obj.foo: ', obj.foo.value);
console.log('foo: ', foo.value);
console.log('bar: ', bar.value);
console.log('obj.bar === bar: ', obj.bar === bar);

foo.value= 55;
console.log('foo 변경 후 ==========');
console.log('obj: ', obj);
console.log('obj.foo: ', obj.foo.value);
console.log('foo: ', foo.value);
console.log('obj.foo === foo: ', obj.foo === foo);
</script>

<template>
</template>

<style>
/* 필요한 스타일을 추가하세요 */
</style>

실행화면


실행화면에서 obj.foo와 foo가 같은 값을 같는 이유는, const { foo, bar } = obj에서 foo와 bar가 ref로 감싸진 변수의 값을 아니라, ref 객체 자체를 참조하고 있기 때문이다. 즉, foo와 bar는 ref로 감싸진 변수를 갖는 것이 아니라 ref 객체를 그대로 참조하고 있어서 동일한 반응형 상태를 공유하게 된다.

따라서 foo.value = 55;를 통해 foo의 값을 변경하면 obj.foo의 값도 함께 변경된다.



2-1. 템플릿에서 ref 언래핑

최상위 속성의 ref를 템플릿에서 접근하면 자동으로 언래핑되므로 '.value'를 사용할 필요가 없다.

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

const count = ref(0)

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

<template>
  <button @click="increment">
    {{ count }} <!-- .value가 필요하지 않습니다. -->
  </button>
</template>

언래핑은 참조가 템플릿 렌더링 컨텍스트에서 최상위 프로퍼티인 경우에만 적용된다. 예를 들어 foo는 최상위 프로퍼티이지만 obj.foo는 최상위 프로퍼티가 아니다.
따라서 아래와 같은 객체가 주어졌을 때

const object = { foo: ref(1) }

아래 표현식은 예상대로 작동하지 않는다

{{ object.foo + 1 }}

object.foo는 ref 객체이기 때문에 렌더링된 결과는 '[object Object]1'가 된다. foo를 최상위 속성으로 만들어 해결할 수 있다.

const { foo } = object
{{ foo + 1 }}

이제 렌더링 결과는 2가 된다.
한 가지 주목해야 할 점은 ref가 {{}} 또는 'v-text=" "'와 같은 텍스트 보간의 최종 평가 값인 경우에도 언래핑되므로 다음은 1이 렌더링 된다.

{{ object.foo }}

이것은 텍스트 보간의 편의 기능일 뿐이면 {{ object.foo.value }}와 동일하다.



2-2. 반응형 객체에서 ref 언래핑

ref가 반응형 객체의 속성으로 접근하거나 변경되면 자동으로 언래핑되어 일반 속성처럼 작동한다.

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

console.log(state.count) // 0

state.count = 1
console.log(count.value) // 1

ref가 할당된 기존 속성에 새 ref를 할당하면 ref는 대체된다.

const otherCount = ref(2)

// 기존 ref는 이제 state.count에서 참조가 끊어진다.
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1

ref 언래핑은 깊은 반응형 객체 내부에 중첩된 경우에만 발생한다. 얕은 반응형 객체의 속성으로 접근하는 경우에는 적용되지 않는다.


a. 배열 및 컬렉션에서 ref 언래핑

반응형 객체와 달리 ref를 반응형 배열의 요소로서 접근하거나 Map과 같은 기본 컬렉션 유형에서 접근할 때 언래핑이 실행되지 않는다.

const books = reactive([ref('Vue 3 Guide')])
// .value가 필요합니다
console.log(books[0].value)

const map = reactive(new Map([['count', ref(0)]]))
// .value가 필요합니다
console.log(map.get('count').value)



3. 반응형 변환 삭제됨!!




참고

Vue3 docs