치춘짱베리굿나이스
Vue를 먹자 3 - 반응형 API 본문
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하게 관리할 수 있다
computed
는 getter
를 인자로 받으며, getter
함수 내의 모든 상태값을 추적하고 값을 업데이트한다
위 예시에서 computedName
은 computed
의 getter
안에 있는 obj 의 상태를 추적하고 변화가 있을 때마다 업데이트된다는 것
일반 메서드로 구현해도 되는 것 아닌가? 싶은데, computed
는 의존하고 있는 반응형 값에 따라 캐싱이 이루어지므로 캐싱을 원치 않을 경우엔 일반 메서드로 구현해도 된다 (근데 이럴 일이 많을까 싶긴 하다)
const now = computed(() => Date.now());
공식 문서에 있는 예제이다
computed
내의 getter
에선 반응형 값이 아닌 값에 의존하지 않으므로, 위의 경우에는 처음의 값이 캐싱된 채로 값의 업데이트가 되지 않는다
const computedName = computed({
get() { ... },
set() { ... }
});
computed
는 기본적으로 함수 인자 (getter
) 단 하나만 받으며 반환값은 readonly라 변경이 불가능하다
하지만 computed
에 setter
를 넘겨주면 수정 가능한 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
를 만들 수 있고, 원본 객체와의 연결은 유지된다
예시에서 name
은 obj
와의 연결은 유지하되 새로운 ref
로서 제 소임을 다할 수 있다
name
을 일반 객체에 복사할 경우,obj
호출에 의해obj.name
의 값이 변경되어도name
은 변경되지 않는다name
을toRef
을 통해 복사할 경우,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
'ClientSide > Vue' 카테고리의 다른 글
Vue 에서 Form input 관리하기 (0) | 2023.10.26 |
---|---|
Vue를 먹자 2 - template (0) | 2023.10.20 |
Vue를 먹자 1 (0) | 2023.10.20 |