치춘짱베리굿나이스

Vue를 먹자 3 - 반응형 API 본문

ClientSide/Vue

Vue를 먹자 3 - 반응형 API

치춘 2023. 10. 24. 16:54

Vue를 먹자

Vue는 공식 문서가 참 달다 …

굳이 다른 블로그 안 봐도 공식문서만으로 왠만한 게 다 해결되는듯 …

반응형 API

컴포지션 API

https://ko.vuejs.org/guide/extras/composition-api-faq.html

뷰 공식문서를 보면 Options | Composition 으로 나누어져 있는 것을 볼 수 있다

Options 는 Vue 2에서 사용하던 기존 체계이고, Composition API는 Vue 3에서 공식적으로 지원하기 시작한 API이다

컴포지션 API 체계로 바뀌면서 로직의 재사용성이 높아졌고, 코드 구성을 조금 더 유연하게 할 수 있게 되었다

타입스크립트와도 잘 맞아서 타입 추론도 더 잘 된다고 한다

setup()

https://ko.vuejs.org/api/composition-api-setup.html

<script>
export default {
    setup() {
        const state = ref(0);

        function sayHello() {
            console.log("hello world!");
        }

        onMounted(() => { // 생명주기 메서드: 마운트되었을 때
            console.log(state);
        });

        return { state, sayHello };
    },
}
</script>

컴포지션 API의 진입점 역할을 한다

setup() 함수에 컴포넌트에서 활용할 변수와 함수 등을 선언해서 훗날 사용할 수 있다

데이터가 한 곳에 모여있기 때문에 데이터의 흐름을 파악하기 쉽고, 재사용이 용이하다는 장점이 있다

script setup

https://ko.vuejs.org/api/sfc-script-setup.html

<script setup lang="ts">
...
</script>

https://vuejs.org/api/sfc-script-setup.html

컴포지션 API를 더 쉽게 읽고 사용할 수 있게 해 주는 컴파일 타임 문법적 설탕이다

일반 script 문법보다 더 많은 이점을 제공한다 (가독성, 런타임 성능, 타입스크립트 …)

컴파일하면 script setup 내의 내용이 위의 setup() 내용으로 컴파일되는데, 따라서 컴포넌트 인스턴스가 생성될 때마다 script setup이 실행된다고 한다

컴포저블

function useComposableTest() {
    const a = ref('');
    const b = ref('');

    return { a, b };
}

리액트에서의 훅과 비슷하게, 상태 저장 로직을 모듈화해서 사용하는 것을 뜻한다

컴포저블 함수끼리 중첩해서 사용할 수도 있고, 조합해서 새로운 컴포저블을 만드는 것도 가능하다

리액트 훅과 매우 비슷한데, 실제로 컴포저블이 리액트 훅에 많은 영감을 얻었다고 한다

반응형 상태

반응형? 반응성?

Reactivity

데이터가 변경되었을 때, 이를 감지 (반응) 하고 DOM을 업데이트하는 것을 반응성이라고 한다

Vue는 반응형 데이터 (상태) 가 바뀔 때마다 감지해서 DOM을 자동으로 업데이트해 주기 때문에, 우리가 해야 할 것은 적절한 반응성을 위해 상태를 잘 설정해주는 것이다

리액트에서 useState() 를 이용해서 상태값을 선언하고 조작해 줬다면, Vue에는 ref(), reactive(), … 등등이 그 역할을 대신한다

Vue에서 상태값 변경을 추적하는 방법

// pseudo-code
const state = {
    _value: 0,
    get value() {
        track(); // 추적
        return this._value;
    },
    set value(newValue) {
        this._value = newValue;
        trigger(); // 트리거 수행
    }
}

Vue는 상태값에 프록시를 걸어서 (프록시로 재정의해서) 변화를 지속적으로 추적한다

프록시를 건다는 것은 객체의 get, set 메서드를 Proxy 객체를 이용해서 재정의하고,

  • get 메서드가 호출될 때마다 상태값을 추적한다
  • set 메서드가 호출될 때마다 DOM을 업데이트할 수 있도록 설정한다 (트리거를 수행한다)

상태값이 변경될 때마다 DOM 업데이트가 즉각 이루어지는 것은 아니고, 내부적으로 매 틱마다 업데이트가 이루어진다고 한다

ref

import { ref } from 'vue';

const count = ref(0); // 초기값

useState 같은 거다

전달받은 기존 변수를 반응형 객체로 변경해서 (프록시를 부착해서) 반환한다

 

<template>
    <span>{{ count }}</span>
</template>

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

const count = ref(0); // 초기값
console.log(count.value); // value 로 값 호출
</script>

템플릿에서는 value 프로퍼티가 아니어도 내부 값에 접근할 수 있지만, 스크립트에서는 value를 이용해서 접근해야 한다

 

count.value = 10;

ref 내부 값을 변경하고 싶으면 value 를 통해 접근하면 된다

setState를 쓸 필요가 없다니

 

const obj = ref({ name: 'chichoon' });

obj.value.name = 'abc';

반응성은 깊이 적용되어 있기 때문에 객체의 내부 값을 바꾸어도 반응성이 적용된다

reactive

import { reactive } from 'vue';

const obj = reactive({ name: 'chichoon' });

ref와 동일한 동작을 하지만, 객체만을 인자로 받는다

객체 상태값에만 적용시킬 수 있다는 뜻… 따라서 사실상 원시값까지 받아올 수 있는 ref 만 사용해도 전혀 지장은 없다

ref 가 내부적으로 reactive 로 구현되어 있다고 한다

 

import { reactive } from 'vue';

const obj = reactive({ name: 'chichoon' });
console.log(obj);

ref 와 다르게 value 없이도 값에 접근이 가능하다

computed

https://ko.vuejs.org/guide/essentials/computed.html

const obj = reactive({ name: 'chichoon' });
const computedName = computed(() => obj.name.split('').reverse().join(''));

상태값을 사용해서 복잡한 연산을 수행해야 할 경우, computed 를 활용해서 로직을 분리하고 reactive하게 관리할 수 있다

computedgetter 를 인자로 받으며, getter 함수 내의 모든 상태값을 추적하고 값을 업데이트한다

위 예시에서 computedNamecomputedgetter 안에 있는 obj 의 상태를 추적하고 변화가 있을 때마다 업데이트된다는 것

일반 메서드로 구현해도 되는 것 아닌가? 싶은데, computed는 의존하고 있는 반응형 값에 따라 캐싱이 이루어지므로 캐싱을 원치 않을 경우엔 일반 메서드로 구현해도 된다 (근데 이럴 일이 많을까 싶긴 하다)

 

const now = computed(() => Date.now());

공식 문서에 있는 예제이다

computed 내의 getter 에선 반응형 값이 아닌 값에 의존하지 않으므로, 위의 경우에는 처음의 값이 캐싱된 채로 값의 업데이트가 되지 않는다

 

const computedName = computed({
    get() { ... },
    set() { ... }
});

computed 는 기본적으로 함수 인자 (getter) 단 하나만 받으며 반환값은 readonly라 변경이 불가능하다

하지만 computedsetter를 넘겨주면 수정 가능한 computed 값을 얻을 수 있다

watch

https://ko.vuejs.org/guide/essentials/watchers.html

watch(objRef, () => {
    ...
});

의존성이 추가된 useEffect처럼, 상태값에 의존하여 로직 (사이드 이펙트) 을 수행하고 싶을 때가 있다

vue에서는 watch 함수가 그 동작을 수행한다

watch 함수의 첫 번째 인자로 넘겨준 상태값 (반응형 값) 이 변경될 때마다 이를 감지하고 두 번째 인자로 들어온 콜백을 실행한다

반응형 값에 대응하여 비동기 로직이나 시간이 많이 소요되는 로직을 수행하고 싶을 때 적합하다

 

watch(route.query.tag, () => console.log(route.query.tag));

반응형 값이 아닌 일반 값 (예시: 반응형 객체의 내부 속성 등) 은 추적이 되지 않는다

 

watch(() => route.query.tag, () => console.log(route.query.tag));

이럴 땐 첫 번째 인자를 getter 로 만들어서 해결할 수 있다

 

watch(() => route.query.tag, (tag) => console.log(tag));

‘두 번째 인자로 들어오는 콜백’의 첫 번째 인자는 첫 번째 인자 getter의 반환값이다

 

watch(() => route.query.tag, (currTag, prevTag) => console.log(currTag, prevTag));

‘두 번째 인자로 들어오는 콜백’ 의 두 번째 인자는 getter의 이전 반환값이다

 

watch([() => route.query.tag, () => route.query.tab], ([tag, tab]) => console.log(tag, tab));

배열을 이용해서 2개 이상의 값을 받아올 수도 있다

 

const reactiveObj = reactive({ innerObj: { number: 0 } });

watch(reactiveObj, () => { ... }); // 깊은 감시자
watch(() => reactiveObj.innerObj, () => { ... }); // 얕은 감시자
watch(() => reactiveObj.innerObj, () => { ... }, { deep: true }); // 깊은 감시자
  • 반응형 객체를 직접 부착하는 경우 자동으로 깊은 감시가 수행된다
  • 반응형 객체가 아닌 getter 를 부착하는 경우 얕은 감시가 수행된다
  • 위의 경우, watch의 3번째 인자로 { deep: true } 를 추가하는 것으로 깊은 감시를 수행할 수 있다

watchEffect

watchEffect(() => { ... });

watch 는 의존성 값이 바뀔 때만 실행된다면, watchEffect는 최초에 콜백을 즉시 한번 실행한다

  • watch와 달리, 이전 값은 신경쓰지 않는다
  • watch와 달리, (의존하는 값들을 인자로 받지 않고) 첫 번째 인자로 콜백을 받으며, 콜백 내부의 반응형 값에 의존하여 업데이트된다
  • 서술했듯 최초에 한 번 반드시 실행된다

isRef

const isThisRef = isRef(obj);

객체가 반응형인지 아닌지 체크하는 함수이다

프록시가 붙었는지 안 붙었는지 체크한다고 보면 될 듯

toRef

const obj = reactive({ name: 'chichoon' });
const name = toRef(obj, 'name');

name.value = 'abc';
console.log(obj.name); // abc

obj.name = 'chichoon2';
console.log(name.value); // chichoon2

원본 reactive 객체의 속성을 가져와 새로운 ref를 만들 수 있고, 원본 객체와의 연결은 유지된다

예시에서 nameobj와의 연결은 유지하되 새로운 ref로서 제 소임을 다할 수 있다

  • name을 일반 객체에 복사할 경우, obj 호출에 의해 obj.name 의 값이 변경되어도 name은 변경되지 않는다
  • nametoRef을 통해 복사할 경우, obj.name의 값이 변경되면 name도 연결되어 있기 때문에 값이 같이 변하며, 따라서 반응성이 유지된다

컴포저블 API에서 많이 활용할 수 있다

shallowRef

const obj = shallowRef({ name: 'chichoon' });

obj.value = { name: 'asdf' }; // 상태값 변경으로 인식됨
obj.value.name = 'asdf'; // 상태값 변경 인식 못함

얕은 버전 상태값이라고 생각하면 된다

객체 내부의 속성 하나하나의 변화는 감지하지 않지만 객체 자체가 변경되는 것은 감지한다


자주… 쓰나..?

toRefs

const obj = reactive({ name: 'chichoon', age: 'secret' });
const objToRefs = toRefs(obj);

console.log(objToRefs.name.value); // chichoon
console.log(objToRefs.age.value); // secret

const { name, age } = objToRefs;
// 보통 Destructuring 하여 사용

reactive 객체를 일반 객체로 변환하여 반환하되, 그 내부의 속성 각각이 새로운 reactive 객체가 되어 반환된다

예시에서 objToRefs 자체는 일반 객체지만, 내부 속성인 name, age는 reactive하게 변한 것이다

unref

const unreferenced = unref(count);

반응성을 해제하고 일반 값을 반환한다

내부적으로 value 값을 그냥 반환해주는 함수이다

toValue

const unreferenced = toValue(count);

unref와 똑같이 반응성을 해제하고 일반 값을 반환하지만, unref와 다르게 getter 도 인자로 받을 수 있다

customRef

customRef((track, trigger) => ({
    get() {
        track();
        return value;
    },
    set(newValue) {
        value = newValue;
        trigger();
    } 
}));

게터와 세터를 직접 설정하고 싶을 때 (trigger, track 말고도 추가적인 로직을 넣고 싶을 때) customRef 을 이용하여 프록시를 직접 정의할 수 있다


참고 자료

https://v2.ko.vuejs.org/v2/guide/reactivity.html

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

https://analogcode.tistory.com/41

https://stackoverflow.com/questions/66585688/what-is-the-difference-between-ref-toref-and-torefs

https://ko.vuejs.org/guide/essentials/watchers.html

https://ko.vuejs.org/api/sfc-script-setup.html

https://ko.vuejs.org/guide/essentials/computed.html

https://ko.vuejs.org/api/composition-api-setup.html

'ClientSide > Vue' 카테고리의 다른 글

Vue 에서 Form input 관리하기  (0) 2023.10.26
Vue를 먹자 2 - template  (0) 2023.10.20
Vue를 먹자 1  (0) 2023.10.20
Comments