본문 바로가기

Vue

Reactivity In Vue (한글)

이 글은 아래 원문을 번역한 글로 의역이 있을 수 있습니다. 정확한 의미를 파악하고 싶으신 분은 원문을 참고해주시기 바랍니다.

 

원문: https://www.smashingmagazine.com/2021/03/reactivity-in-vue/

저작권 정보: https://www.smashingmagazine.com/


요약

반응성은 변수(array, string, number, object 등)의 값이나 참조하는 다른 변수가 선언 이후에 변경이 일어났을 때, 변수를 업데이트하는 기능입니다.


이 글에서, 우리는 Vue의 반응성(reactivity)에 대해, 그것이 어떻게 동작하는지, 그리고 새로운 메서드와 함수를 이용해서 어떻게 반응성 있는 변수를 만들 수 있는지 알아보겠습니다. 이 글은 2 버전대 Vue가 어떻게 동작하는지 잘 알고 있고, Vue 3를 알아보고 싶은 개발자를 대상으로 합니다.

 

우리는 이 주제를 잘 이해하기 위해 간단한 응용 프로그램을 만들 것입니다. 이 앱의 코드는 GitHub에서 볼 수 있습니다.

 

기본적으로, JavaScript는 반응형이 아닙니다. 이는 우리가 `boy`라는 변수를 만들고 애플리케이션의 A 파트에서 이를 참조하면, B 파트에서 `boy`를 수정해도 A 파트는 새로운 `boy`의 새로운 값으로 업데이트되지 않음을 의미합니다.

let framework = 'Vue';
let sentence = `${framework} is awesome`;
console.log(sentence);
 // logs "Vue is awesome"
framework = 'React';
console.log(sentence);
//should log "React is awesome" if 'sentence' is reactive.

위 예제는 JavaScript의 non-reactive 한 특징을 보여주는 완벽한 예제입니다. 그러면, 수정이 `sentance` 변수에 반영되지 않는 이유는 무엇일까요?

 

Vue 2 버전대에서는 `props`, `computed`, 그리고 `data()` 모두 기본적으로 컴포넌트가 생성될 때 데이터에 존재하지 않던 프로퍼티들을 제외하면 반응형입니다. 즉, 컴포넌트가 DOM에 주입될 때 데이터 객체에 존재하던 프로퍼티만 해당 프로퍼티가 변경됐을 때 컴포넌트를 업데이트한다는 것입니다.

 

내부적으로 Vue 3는 Proxy 객체(ECMA 6 기능)를 사용하여 이러한 프로퍼티들의 반응성을 보장합니다. 하지만 여전히 Internet Explorer 지원을 위해 Vue 2의`Object.defineProperty`를 사용할 수 있는 선택지를 제공합니다. 이 메서드는 객체에 새 프로퍼티를 정의하거나, 객체에 존재하는 프로퍼티를 수정하고 객체를 반환합니다.

 

우리 대부분은 이미 Vue에서 반응성이 새로운 것이 아니라는 것을 알고 있기 때문에, 언뜻 보면 이러한 특성들을 사용하는 것이 불필요해 보일 수 있지만, Options API는 프로그램의 여러 부분에서 재사용 가능한 함수가 있는 대규모 응용 프로그램을 다룰 때 제한이 있습니다. 이런 코드 베이스를 읽고 유지하기 쉽게 로직을 추상화하는 것을 돕기 위해 새롭게 Composition API가 소개되었습니다. 또한, 우리는 이제 새로운 프로퍼티와 메서드들을 사용하여 데이터 타입에 관계없이 모든 변수를 쉽게 반응형으로 만들 수 있습니다.

 

Composition API의 진입점 역할을 하는 `setup` 함수를 사용할 때, `setup`이 실행될 때는 아직 컴포넌트 인스턴스가 만들어지지 않았기 때문에 `data `객체, `computed` 프로퍼티, 그리고 메서드에 접근이 불가능합니다. 따라서 `setup` 내부에서는 이러한 내장 반응성을 활용할 수 없습니다. 이 튜토리얼에서는, `setup` 내부에서 반응성을 사용할 수 있는 모든 방법을 알아보겠습니다.

Reactive 메서드

문서에 따르면, `reactive` 메서드(Vue 2.6의 `Vue.observable()`과 동일)는 프로퍼티들이 모두 반응형인 객체(Options API의 `data` 객체와 같은)를 만들고자 할 때 유용합니다. 내부적으로 Options API의 `data` 객체는 모든 프로퍼티를 반응형으로 만들기 위해 이 메서드를 사용합니다.

 

이렇게 우리만의 반응형 객체를 만들 수 있습니다:

import { reactive } from 'vue'

// reactive state
const user = reactive({
  id: 1,
  name: 'Leanne Graham',
  username: 'Bret',
  email: 'Sincere@april.biz',
  address: {
    street: 'Kulas Light',
    suite: 'Apt. 556',
    city: 'Gwenborough',
    zipcode: '92998-3874',
    geo: {
      lat: '-37.3159',
      lng: '81.1496'
    }
  },
  phone: '1-770-736-8031 x56442',
  website: 'hildegard.org',
  company: {
    name: 'Romaguera-Crona',
    catchPhrase: 'Multi-layered client-server neural-net',
    bs: 'harness real-time e-markets'
  },
  cars: {
    number: 0
  }
});

Vue에서 `reactive` 메서드를 import 하고, 그 값들을 함수의 매개변수로 넘기면서 `user`라는 변수를 정의했습니다. 이렇게 하면, 우리는 `user`를 반응형으로 만든 것입니다. 만약 우리가 `user`를 템플릿에 사용하고, 이 객체 혹은 프로퍼티가 변경되면, 이 값은 템플릿에서 자동으로 업데이트됩니다.

`ref`

객체를 반응형으로 만드는 방법이 있듯이, 독립된 원시 값(strings, booleans, undefined values, numbers 등.)과 배열을 반응형으로 만드는 방법이 있습니다. 개발 중에는 이런 다른 데이터 타입들도 반응형으로 만들어야 할 때가 있습니다. 첫 번째 방법은 `reactive`를 사용하는 방법을 생각해 볼 수 있습니다. 반응형으로 만들고 싶은 변수의 값을 넘기는 것입니다.

import { reactive } from 'vue'

const state = reactive({
  users: [],
});

`reactive`는 깊은 부분까지(deep) 반응형으로 만들기 때문에, 우리가 원하던 대로 `user`는 반응형이 됩니다. 즉, 템플릿에서 `user`는 사용된 모든 위치는 항상 업데이트됩니다. `ref` 프로퍼티를 사용하면, 우리는 어떤 데이터 타입을 갖는 변수이건 간에 값을 넘겨서 반응형으로 만들 수 있습니다. 이 메서드는 객체에서도 동작하지만, `reactive`를 사용할 때보다 1 단계가 더 중첩됩니다.

let property = {
  rooms: '4 rooms',
  garage: true,
  swimmingPool: false
};
let reactiveProperty = ref(property);
console.log(reactiveProperty);
// prints {
// value: {rooms: "4 rooms", garage: true, swimmingPool: false}
// }

내부적으로, `ref`는 전달받은 매개변수를 가지고 `value`라는 프로퍼티를 가진 object를 만듭니다. 즉, 우리는 우리의 변수를 `variable.value`로 접근할 수 있습니다. 그리고 우리는 이 값을 동일한 방법으로 접근해서 수정할 수 있습니다.

import {ref} from 'vue'
let age = ref(1);

console.log(age.value);
//prints 1
age.value++;
console.log(age.value);
//prints 2

이렇게 우리는 `ref`를 컴포넌트로 import 하고 반응형 변수를 만들 수 있습니다.

<template>
  <div class="home">
    <form @click.prevent="">
      <table>
        <tr>
          <th>Name</th>
          <th>Username</th>
          <th>email</th>
          <th>Edit Cars</th>
          <th>Cars</th>
        </tr>
        <tr v-for="user in users" :key="user.id">
          <td>{{ user.name }}</td>
          <td>{{ user.username }}</td>
          <td>{{ user.email }}</td>
          <td>
            <input
              type="number"
              style="width: 20px;"
              name="cars"
              id="cars"
              v-model.number="user.cars.number"
            />
          </td>
          <td>
            <cars-number :cars="user.cars" />
          </td>
        </tr>
      </table>
      <p>Total number of cars: {{ getTotalCars }}</p>
    </form>
  </div>
</template>
<script>
  // @ is an alias to /src
  import carsNumber from "@/components/cars-number.vue";
  import axios from "axios";
  import { ref } from "vue";
  export default {
    name: "Home",
    data() {
      return {};
    },
    setup() {
      let users = ref([]);
      const getUsers = async () => {
        let { data } = await axios({
          url: "data.json",
        });
        users.value = data;
      };
      return {
        users,
        getUsers,
      };
    },
    components: {
      carsNumber,
    },
    created() {
      this.getUsers();
    },
    computed: {
      getTotalCars() {
        let users = this.users;
        let totalCars = users.reduce(function(sum, elem) {
          return sum + elem.cars.number;
        }, 0);
        return totalCars;
    },
  };
</script>

컴포넌트에 반응하는 `users` 변수를 만들기 위해 `ref`를 import 했습니다. 그런 다음 `public` 폴더에 있는 JSON 파일을 불러오기 위해 axios를 import 하고, 나중에 만들 예정인 `carsNumber` 컴포넌트를 import 했습니다. 다음으로 `ref` 메서드를 이용하여 `users` 변수를 반응형으로 만들어서 JSON 파일이 바뀔 때마다 `users` 가 업데이트되도록 했습니다.

 

또한, axios를 사용해서 JSON 파일에서 `users` 배열을 가져오는 `getUser` 함수도 만들고, 가져온 값을 `users` 변수에 할당했습니다. 마지막으로, 템플릿에서 수정한 대로 사용자들이 보유한 총 자동차 수를 계산하는 `computed` 속성을 만들었습니다.

 

템플릿 안 혹은 `setup()` 밖에서는 `ref` 프로퍼티가 자동으로 포장이 벗겨진다는 것을 유의해야 합니다. 객체인 `refs`는 여전히 `. value`로 접근해야 하지만, `users`가 배열이기 때문에 `getTotalCars`에서는 `users.value`가 아닌 `users`로 접근합니다.

 

템플릿에 `<cars-number />` 컴포넌트와 함께 각 사용자의 정보를 표시하는 테이블이 있습니다. 이 컴포넌트는 각 사용자의 행에 표시된 자동차의 개수인 `cars` props를 받습니다. 이 값은 사용자 객체의 `cars` 값이 바뀔 때마다 업데이트됩니다. 이는 정확히 Options API의 `data` 객체나 `computed` 속성이 동작하는 방법과 같습니다.

`toRefs`

Composition API를 사용할 때, `setup` 함수는 `props`와 `context` 2개의 매개변수를 받습니다. `props`는 컴포넌트에서 `setup()`으로 전달되며, 새로운 API 내부에서 컴포넌트의 props에 접근할 수 있습니다. 이 방법은 객체의 반응성을 잃지 않고 구조 분해 할당할 수 있기 때문에 특히 유용합니다.

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      console.log(props);
   // prints {gender: "female", cars: Proxy}
    },
  };
</script>
<style></style>

Composition API에서 prop 객체의 반응성을 유지하면서 값을 사용하기 위해 `toRefs`를 사용합니다. 이 메서드는 반응형 객체를 받아 객체의 각 프로퍼티들이 `ref`인 일반 객체로 바꿉니다. 즉, `cars` props가...

cars: {
  number: 0
}

이렇게 됩니다.

{
  value: {
    cars: {
      number: 0
    }
  }
}

이렇게 하면 setup API 내부 어디에서든 반응성을 유지하면서 `cars`를 사용할 수 있습니다.

setup(props) {
  const {cars} = toRefs(props);
  console.log(cars.value);
  // prints {number: 0}
},

우리는 이 새로운 변수를 Composition API의 `watch`를 이용하여 관찰하면서 변화가 있을 때 우리가 원하는 대로 반응할 수 있습니다.

setup(props) {
  const {cars} = toRefs(props);
  watch(
    () => cars,
    (cars, prevCars) => {
      console.log('deep ', cars.value, prevCars.value);
    },
    {deep: true}
  );
},

`toRef`

우리가 만날 수 있는 또 다른 경우는 객체가 아닌 `ref`와 동작하는 다른 데이터 타입(array, number, string, boolean 등.)을 넘기는 것입니다. `toRef`를 사용하면, 반응형 객체로부터 반응형 프로퍼티(즉, `ref`)를 만들어낼 수 있습니다. 이렇게 하면 프로퍼티는 반응형임이 보장되고, 상위 소스가 변경될 때 업데이트됩니다.

const cars = reactive({
  Toyota: 1,
  Honda: 0
})

const NumberOfHondas = toRef(cars, 'Honda')

NumberOfHondas.value++
console.log(cars.Honda) // 1

cars.Honda++
console.log(NumberOfHondas.value) // 2

`reactive` 메서드를 사용하여 `Toyota`와 `Honda` 프로퍼티를 갖는 반응형 객체를 만들었습니다. Honda를 반응형 프로퍼티로 만들기 위해 `toRef`를 사용했습니다. 위의 예제에서 우리가 반응형 객체 `cars`나 `NumberOfHondas`를 이용하여 `Honda`를 업데이트하면, 두 경우 모두 값이 업데이트되는 것을 볼 수 있습니다.

 

이 메서드는 `toRefs`와 비슷하지만, 소스와의 연결이 유지되고, string, array, number에 사용할 수 있다는 점에서 매우 다릅니다. `toRefs`와 달리, 만약 프로퍼티가 존재하지 않으면 `ref`를 생성하고 null을 반환하기 때문에, 프로퍼티가 생성 시점에 존재하는지 걱정할 필요가 없습니다. 이것은 여전히 관찰자(watcher)와 함께 유효한 프로퍼티로 저장되어 있고, 값이 변경되면 `toRef`로 만들어진 `ref`도 업데이트됩니다.

 

이 메서드를 사용하면 `props`로부터 반응형 프로퍼티를 만들 수도 있습니다.

<template>
  <p>{{ cars.number }}</p>
</template>
<script>
  import { watch, toRefs, toRef } from "vue";
  export default {
    props: {
      cars: {
        type: Object,
        required: true,
      },
      gender: {
        type: String,
        required: true,
      },
    },
    setup(props) {
      let { cars } = toRefs(props);
      let gender = toRef(props, "gender");
      console.log(gender.value);
      watch(
        () => cars,
        (cars, prevCars) => {
          console.log("deep ", cars.value, prevCars.value);
        },
        { deep: true }
      );
    },
  };
</script>

이렇게 `props`로부터 `gender` 프로퍼티를 받아 `ref`를 만들었습니다. 이는 특정 컴포넌트의 prop에서 추가 작업을 할 때 유용합니다.

결론

이 글에서는 Vue 3에서 새롭게 도입된 함수와 메서드를 이용해서 Vue의 반응성이 어떻게 동작하는지 알아보았습니다. 반응성이 무엇인지 그리고 Vue가 이를 만들어내기 위해 뒤에서 `Proxy` 객체를 어떻게 사용하고 있는지 살펴보았습니다. 또한, `reactive`를 사용해서 반응형 객체를 만드는 방법과 `ref`를 이용하여 반응형 프로퍼티를 만드는 방법을 알아보았습니다.

 

마지막으로, 우리는 반응형 객체를 각 프로퍼티가 해당 원본 객체의 프로퍼티의 `ref`인 일반 객체로 만드는 방법, 그리고 반응형 객체의 프로퍼티의 `ref`를 만드는 방법을 알아보았습니다.

참고자료