Vue 3.0/02.Essentials

[vue 3] 03. 반응성(Reactivity)

  • -

반응성이란 데이터가 변경되었을 때 이를 감지하고 이에 반응하여 부가적인 동작을 수행하기 위한 성질이다.

예를 들어 위와 같이 excel 수식을 작성한 후 A를 20으로 변경하면 이에 반응하여 Sum이 40으로 변경된다. 즉 데이터를 변경하면 화면이 변경되는 것이다. Vue는 이런 반응성 구현을 위해 Proxy API를 사용한다.

https://goodteacher.tistory.com/524

 

[es2015]Proxy

이번 포스트에서는 Vue.js등 프론트엔드 프레임워크 들에서 반응성을 위해 사용하는 Proxy에 대해서 살펴보자. 반응성이란? 반응성(Reactivity) 반응성이란 선언적인 방식으로 어떤 값에 대한 변경에

goodteacher.tistory.com

이번 포스트에서는 반응성을 위한 ref와 reactive에 대해서 살펴보자.

 

reactive()를 통한 반응성 제공

 

reactive()

Vue의 reactive()는 원본 객체에 대한 Proxy를 제공해서 객체에 대한 반응성을 제공한다. 

function reactive<T extends object>(target: T): UnwrapNestedRefs<T>

reactive의 반응성은 하위의 중첩 속성까지 계속해서 전파되는 깊은 반응성이다. 만약 이런 깊은 반응성이 싫다면 shallowReactive()를 사용하면 된다.

반응성은 객체를 proxy를 통해서 접근할 때 이뤄진다. 따라서 객체의 특정 속성만 뚝 떼어내서 변수에 할당하거나 다른 함수의 파라미터로 넘겨주면 당연히 그 속성은 반응성을 상실한다.

const state = reactive({ count: 0 }) // 반응형 객체 생성

const n = state.count                // n은 반응성을 갖지 않음
callSomeFunction(state.count)        // 어떤 함수에 전달된 state.count는 state와 관계가 끊김

 

반응성 확인

간단한 예제를 통해 일반 객체와 반응형 객체의 차이점에 대해 살펴보자.

<script src="https://unpkg.com/vue@3"></script>
<script>
    const { createApp, reactive } = Vue;

    createApp({
        setup() {
            const nState = { count: 10, user: { age: 10 } };
            const rState = reactive(nState);
            
            console.log(nState, rState);

            return { nState, rState};
        },
    }).mount("#app");
</script>

 

스크립트를 먼저 살펴보면 createApp과 함께 Vue가 제공하는 reactive를 사용하고 있다. setup에서 생성한 객체는 rState와 nState가 있는데 rState는 nState를 reactive로 처리한 객체이다. 그결과  콘솔에 출력된 내용을 보면 rState는 Proxy임을 알 수 있다.

화면에서는 버튼이 클릭될 때 각각 rState와 nState가 가진 값을 변경시켜 보자. 그리고 3번째 버튼은 깊은 반응성을 점검하기 위해서 rState의 user의 age를 변경해본다.

<div id="app">
    <button @click="nState.count++">일반형, 화면 갱신 X: {{nState}}</button><br />
    <button @click="rState.count++">반응형: 화면 갱신 O: {{rState}}</button><br />
    <button @click="rState.user.age++">깊은 반응성</button><br />
</div>

먼저 일반형 버튼을 여러번 클릭해보면 화면은 전혀 변화가 없다. 하지만 개발자 도구를 통해서 확인해보면(Force Refresh를 클릭해야 한다.) 값은 변경된 것을 확인할 수 있다.

즉 반응성이 없는 것이다. 하지만 반응형 버튼을 클릭해보면 변경된 값이 바로 바로 표현된다. 그리고 이때 일반형의 값도 동시에 반영되는 것을 확인할 수 있다.(원본 객체는 하나이기 때문에 일반형이나 반응형 모두 값은 같다.)

또한 깊은 반응성 버튼을 클릭했을 때에도 user의 age가 변경되고 화면 반영도 잘 되는 것을 알 수 있다.

 

reactive()의 한계

이렇게 아주 쉽게 반응성 객체를 만들 수 있는 reactive()는 치명적인 단점이 있었으니 바로 객체 타입(objects, arrays, Map, Set...)에서만 사용이 가능하다는 점이다. 즉 기본형인 string, number, boolean 등에 대해서는 사용할 수 없다. reactive는 proxy를 생성하는데 proxy는 객체를 대상으로만 만들 수 있기 때문이다.

그럼 반응형 데이터를 사용하기 위해서는 언제나 객체형으로 만들어서 써야 하느냐? 당연히 그렇지 않고 기본형을 사용할 때는 ref()를 이용한다.

 

ref()를 이용한 반응성 제공

 

ref()

ref는 reactive와 마찬가지로 반응성을 가지는 데이터를 구성할 때 사용된다.

function ref<T>(value: T): Ref<UnwrapRef<T>

ref()가 reactive()와 가장 큰 차이점은 값으로 객체형은 물론 기본형 까지 모든 타입의 가질 수 있다는 점이다. 따라서 가급적 ref()를 이용해서 반응형 데이터를 만드는 것을 권장한다.

ref를 사용하는 방법은 조금 귀찮긴하다.  ref는 기본형이든 참조형이든 단일 값을 파라미터로 갖는데 내부적으로는 value라는 키값에 파라미터를 매핑하는 객체이다. 따라서 실제 값에 접근하기 위해서는 .value로 한단계 더 들어가야 한다.

다음은 객체형과 기본형으로 ref를 만든 후 실제 값을 출력하는 예이다. 특히 연산을 처리할 때 .value로 실제 값에 접근하고 있음을 주목하자.

const refObject = ref({ count: 0 });
console.log(refObject);
console.log(`기존 count +1 : ${++refObject.value.count}`);

const refPrimitive = ref(0);
console.log(refPrimitive);
console.log(`기존 값 + 1: ${++refPrimitive.value}`);

log에 출력된 실제 값을 확인하면 두 경우 모두 RefImpl 타입이고 _value에 값이 저장된 것을 볼 수 있다.

일단 refObject는 value는 Proxy이다.

객체에 대한 ref의 결과 value는 Proxy이다.!

refPrimitive는 value가 그냥 값이다. 하지만 getter와 setter가 vue에 의해서 재정의([[FunctionLocation]] 참조) 되었고 그 내부에서 다시 상황에 따라 reactive가 호출된다.

기본형에 대한 ref의 결과 value는 단순 값이다. 하지만 setter에 들어가보면..

set value(newValue) {
    const oldValue = this._rawValue;
    const useDirectValue = this["__v_isShallow"] || isShallow(newValue) || isReadonly(newValue);
    newValue = useDirectValue ? newValue : toRaw(newValue);
    if (hasChanged(newValue, oldValue)) {
        this._rawValue = newValue;
        this._value = useDirectValue ? newValue : toReactive(newValue);
        {this.dep.trigger({target: this, type: "set",key: "value", newValue, oldValue });}
    }
}

 

정리하면 Vue3에서는 반응성을 위해 Proxy를 사용한다. Proxy를 직접 만들어서 사용하기 위해서는 상당한 코딩이 필요한데 이를 자동으로 해주는 함수가 reactive()이다. 하지만 reactive는 객체를 Proxy로 만들 뿐 기본형을 Proxy로 만들지는 못한다. ref는 내부적으로 value라는 키로 데이터를 value로 하는 객체를 구성하고 이것을 Proxy로 만들어준다.

따라서 객체를 사용할 때는 reactive()를 사용하고 단순 값을 사용할 때는 ref()를 사용하면 된다. ref를 사용하면 스크립트 단에서 value 속성을 사용해야 하는 번거로움이 있지만 일관성을 위해 ref 를 사용하는 것이 유리하기도 하다.

 

template에서의 사용

javaScript 영역에서 ref의 값에 접근할 때는 .value가 필요했지만 template 영역에서 ref 값을 사용할 때는 .value를 사용하지 않는다

<div id="app">
    <ul>
        <!--top level의 ref의 값을 참조할 때에는 .value 불 필요-->
        <li>ref(기본형): {{refPrimitive}}</li>
        <li>ref(객체형): {{refObject.count}}</li>
    </ul>
    <button @click="refPrimitive++">refPrimitive 증가</button>
    <button @click="refObjectIncrease">refObject count 증가</button>
</div>

위의 경우처럼 단순히 값을 출력할 때는 물론 이벤트를 통해서 값을 변경할 때도 value를 사용하지 않는다. 하지만 동일한 동작을 하더라도 javascript 영역에서는 value 가 필수이다.

const refObjectIncrease = () => {
    refObject.value.count++;
};

 

ref() 요소를 속성으로 갖는 객체의 활용

model 데이터를 구성할 때 ref() 요소를 속성으로 갖는 객체를 구성할 수 있다. 이 경우 당연히 속성 하나 하나가 반응성이다.

createApp({
  setup() {
    // obj의 모든 property들은 자체로 반응형
    const nested = { foo: ref(1), bar: ref(2), };
    const increment = () => nested.foo.value++;
    return { nested, increment, };
  },
}).mount("#app");

 

이런 nested 객체를 template에서 사용할 때 주의 사항이 있다. 연산 과정에서 앞 단락에서 top level의 ref를 사용할 때에는 value 속성을 생략할 수 있었지만 nested 객체의 내부에 있는 ref를 사용할 때에는 value 속성을 추가해야 한다.

<div id="app">
  <ul>
    <!-- top level이 아닌 경우에는 value를 사용하거나 top level로 만들어서 사용-->
    <li>nested: {{nested}}</li>
    <li>nested.foo: {{nested.foo}}, {{nested.foo +1}}, {{nested.foo.value+1}}</li>
    <li>nested.var: {{nested.bar}}, {{nested.bar +1}}, {{nested.bar.value+1}}</li>
  </ul>
  <button @click="increment">+</button>
  <button @click="nested.bar.value++">+</button>
</div>

 

위 코드의 출력 결과를 보면 두 번째 항목 {{nested.foo + 1}} 의 결과가 [object Object]1로 나온 것을 볼 수 있다. 이런 경우는 3번째 항목처럼 nested.foo.value 와 같이 .value를 추가해야 한다.

 

기타 유용한 API들

 

toRefs()

toRefs()는 일반 객체의 속성을 하나씩 분리해서 반응성 있는 ref 들로 만들려고 할 때 반응성 객체의 속성을 하나씩 분리하면서도 반응성을 유지하려고 할때 사용된다.

function toRefs<T extends object>(object: T): {
  [K in keyof T]: ToRef<T[K]>
}

실제 활용 예를 살펴보자.

createApp({
  setup() {
    const user = reactive({name: "hong gil dong", age: 30,});
    // 그냥 분해하면 name과 age는 반응성을 상실하게 됨
    const { name: nname, age: nage } = user;
    console.log(nname, nage);

    // toRefs를 이용하면 각 요소를 ref로 만들어주며 반응성을 유지함
    const { name: rname, age: rage } = toRefs(user);
    console.log(rname, rage);
    console.log(rname.value, rage.value);
    
    return {user,nname,nage,rname,rage,};
  },
}).mount("#app");

콘솔 출력 결과를 살펴보면 rname과 rage는 반응성 객체임을 알 수 있다. 따라서 .value를 붙여서 값을 조회해야 한다.

 

readonly()

readonly()는 이름 그대로 반응형 객체를 전달받은 곳에서 값의 변경을 하지 못하게 하고 반응성만 유지하려는 경우에 사용할 수 있다.

function readonly<T extends object>(target: T):DeepReadonly<UnwrapNestedRefs<T>>

 

아래 예는 count를 속성으로 갖는 객체에 대한 반응형 proxy로 reactiveState를 만들고 이를 다시 readonlyState로 작성한 예이다.

const { createApp, reactive, readonly } = Vue;
createApp({
  setup() {
    const reactiveState = reactive({ count: 0 });
    const readonlyState = readonly(reactiveState);
    
    return {reactiveState, readonlyState,};
  },
}).mount("#app");

이 두 값을 template에서 출력하고 각각 수정하는 시도를 해보자.

<div id="app">
  <div>{{reactiveState.count}}</div>
  <div>{{readonlyState.count}}</div>
  <button @click="reactiveState.count++">reactiveState.count</button>
  <button @click="readonlyState.count++">readonlyState.count</button>
</div>

출력은 당연히 문제가 없고 버튼을 클릭할 때 reactiveState를 이용하면 두 div의 내용이 잘 수정된다. readonlyState도 반응성이기 때문이다. 하지만 readonlyState를 이용하면 아래와 같은 Warn이 발생하며 값이 수정되지 않는다.

'Vue 3.0 > 02.Essentials' 카테고리의 다른 글

[vue 3] 06.v-for  (0) 2022.06.25
[vue 3] 05.조건문을 위한 v-if, v-show  (0) 2022.06.24
[vue 3] 04. v-bind를 통한 속성 처리  (0) 2022.06.23
[vue 3] 02.Text Interpolation  (0) 2022.06.21
[vue 3] 01. Vue Application 생성  (0) 2022.06.20
Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.