목차
· 감시자(Watch)
· 템플릿 참조
· 목록 렌더링
· 조건부 렌더링
감시자(Watch)
종종 반응형 상태가 변경 되었을 때를 감지하여 다른 작업 (데이터 패칭 등)을 수행해야하는 경우가 있습니다. 예를 들어 어떠한 상태가 변경되었을 때 DOM을 변경하거나, 비동기 작업을 하여 다른 상태를 변경하는 것입니다.
Composition API의 watch 함수를 사용하여 반응형 상태가 될 때마다 특정 작업을 수행할 수 있습니다.
const message = ref(`Hello World`);
/**
* 첫 번째 매개변수에는 데이터 변경을 감지하고자 하는 인자가 들어갑니다.
* 이때, 첫번째 매개변수는 ref, reactive, computed, getter, array 타입과 같은 다양한 타입이 들어갈 수 있습니다.
*/
watch(message, (newValue, oldValue) => {
console.log(`newValue: ${newValue}`);
console.log(`oldValue: ${oldValue}`);
});
Watch Source Type
watch(/* Source Type */, (newValue, oldValue) => {});
watch의 첫 번째 매개변수는 다양한 타입이 될 수 있습니다. ref, reactive, computed, getter 함수, array 타입이 될 수 있습니다.
const x = ref(0);
const y = ref(0);
/**
* [single ref]
* x의 단일 값 변화를 감지하여 사용자에게 관련 내용을 출력해줄 수 있습니다.
*/
watch(x, (newX) => {
console.log(`x is ${newX}`);
});
/**
* [getter]
* x와 y의 합 변화를 감지하여 사용자에게 출력해줄 수 있습니다.
*/
watch(() => x.value + y.value, (sum) => {
console.log(`sum of x + y is: ${sum}`);
})
/** array of multiple sources */
watch([x, () => y.value], ([newX, newY], oldValue) => {
console.log(`x is ${newX} and y is ${newY}`);
console.log(`This is Old Value : ${oldValue}`
});
* array of multiple sources result
다음과 같이 반응형 객체의 속성은 볼 수 없습니다.
const obj = reactive({ count: 0 });
/** 숫자를 전달하기 때문에 작동하지 않습니다 */
watch(obj.count, (newValue) => {
console.log(`newValue: ${newValue}`);
});
대신 getter을 사용할 수 있습니다.
const obj = reactive({ count: 0 });
/** 해당 부분에서 변경된 점은 getter 함수를 사용하여 접근을 했다는 점입니다 */
watch(() => obj.count, (newValue) => {
console.log(`newValue: ${newValue}`);
});
Deep Option
반응형 객체를 직접 watch() 하면 암시적으로 깊은 감시자가 생성됩니다. 즉, 속성 뿐만아니라 모든 중첩된 속성에도 트리거가 됩니다.
const person = reactive({
name: "홍길동",
age: 30,
hobby: "운동",
obj: {
count: 0,
},
});
watch(person, (newValue) => {
console.log(`newValue: ${newValue}`);
});
getter 함수를 사용하여 객체를 넘길 경우에는 객체의 값이 바뀔 경우에만 트리거가 됩니다.
즉, 중첩된 속성은 트리거되지 않습니다.
watch(() => person.obj, (newValue) => {
/** person.obj, 객체의 값이 바뀔 경우에만 트리거가 됩니다. */
...
});
하지만, 여기서도 깊은 감시자로 강제하고 싶을 수 있습니다. 이럴 경우에는 deep 옵션을 사용하여 깊은 감시자로 강제할 수 있습니다.
watch(() => person.obj, (newValue) => {
/** person.obj, 중첩된 값들까지 깊은 감시자로 강제할 수 있습니다. */
...
}, {deep: true});
deep 옵션은 큰 데이터 구조에서 사용할 경우, 비용이 많이 들 수 있습니다.
필요한 경우에만 사용을 하고 성능 영향에 주의를 기울여야합니다.
Immediate 즉시 실행
immediate 옵션을 사용하여 최초에 즉시 실행을 할 수 있습니다.
const message = ref('Hello World!');
const reverseMessage ref('');
watch(message, (newValue) => {
reverseMessage.value = newValue.split('').reverse().join('');
}, { immediate: true });
또는 함수를 외부에 선언하여 즉시 실행을 할 수 있습니다. 이는 WatchEffect를 사용하면 더 단순화도 가능합니다.
const message = ref('Hello World!');
const reverseMessage ref('');
const reverseFn = () => {
reverseMessage.value = message.value.split('').reverse().join('');
});
watch(message, reverseFn);
/** watch 구문을 선언해놓고, 옵션을 사용하지 않고 외부함수의 의해 즉시 실행을 하는 방식으로도 가능하다 */
reverseFn();
Computed vs Watch
computed와 watch 반응형 데이터에 반응하여 동작한다는 점에서 둘 다 비슷한 역할을 하고 있습니다.
computed
const reverseMessage = computed(() => {
message.value.split('').reverse().join('');
});
watch
watch(message, (newValue) => {
reverseMessage.value = newValue.split('').reverse().join('');
});
어떻게 사용할 수 있을까?
computed
- Vue 인스턴스 상태(ref, reactive)의 종속 관계를 자동으로 세팅하고자 할 때는 computed로 구현하는 것이 좋습니다.
- 위 예시처럼 reverseMessage는 message 값에 따라 결정되어지는 종속 관계에 있다. 이 종속 관계 코드가 복잡해지면 watch로 구현할 경우 더 복잡해지거나 중복 계산 또는 오류를 발생시킬 수 있습니다.
watch
- Vue 인스턴스 상태(ref, reactive)의 변경 시점에 특정 액션(라우트, 데이터 패칭 등)을 취하고자 할 때 적합합니다.
- 대게 경우 computed로 구현 가능한 것이라면 watch가 아니라 computed로 구현하는게 대부분 옳습니다.
WatchEffect
WatchEffect는 콜백 함수 안의 반응성 데이터에 변화가 감지되면 자동으로 반응하여 실행합니다. 그리고 WatchEffect의 코드는 컴포넌트가 생성될 때 즉시 실행이 되어집니다.
watchEffect(async () => {
const { data } = await axios.get(`API URL`);
items.value = data.data;
})
Watch vs WatchEffect
watch와 watchEffect 둘 다 관련 작업(라우터, 데이터 패칭 등)을 반응적으로 수행할 수 있게 해줍니다.
하지만 주요한 차이점은 관련된 반응형 데이터를 추적하는 방식입니다.
- watch는 명시적으로 관찰된 소스만 추적합니다. 콜백 내에서 액세스한 항목은 추적하지 않습니다. 또한 콜백은 소스가 실제로 변경된 경우에만 트리거 되며, watch 종속성 추적을 부작용과 분리하여 콜백이 실행되어야하는 시기를 보다 정확하게 제어할 수 있습니다.
- watchEffect는 반면 종속성 추적과 부작용을 한 단계로 결합합니다. 동기 실행 중 액세스되는 모든 반응 속성을 자동으로 추적합니다. 이것은 더 편리하고 일반적으로 더 간결한 코드를 생성하지만 반응형 종속성을 덜 명시적으로 만듭니다.
여기서 사이드 이펙트란, 결함에 의한 내용인 부작용을 뜻하는 것이 아닌 컴포넌트가 화면에 렌더링 된 이후에 비동기로 처리되어야하는 부수적인 효과들을 사이드 이펙트라고 명칭합니다.
템플릿 참조
Vue의 선언적 렌더링 모델은 대부분의 직접적인 DOM의 작업을 대신 수행합니다.
하지만 때론 기본 DOM 요소에 직접 접근해야하는 경우가 있을 수 있습니다.
이때 ref 특수 속성을 사용하여 쉽게 접근을 할 수 있습니다.
<input type="text" ref="input" />
ref는 특수 속성입니다. 이 ref 특수 속성을 통하여 마운트된 DOM 요소 또는 자식 컴포넌트에 대한 참조를 얻을 수 있습니다.
Refs 접근하기
Composition API로 참조를 얻으려면 동일한 이름의 참조를 선언해야 합니다.
- 컴포넌트가 마운트된 후 접근할 수 있습니다.
- <template> 안에서 <input>으로 Refs 참조에 접근하려는 경우, 렌더링 되기 전에는 참조가 null 일 수 있습니다.
- <template> 안에서 $refs 내장 객체로 Refs 참조에 접근할 수 있습니다.
<template>
<input ref="input" type="text"/>
<div> {{ input }} </div>
<div> {{ $refs.input }} </div>
<div> {{ input === $refs.input }} </div>
</template>
<script>
import { onMounted, ref } from 'vue';
export default {
conponents: {},
setup() {
const input = ref(null);
onMounted(() => {
input.value.value = 'Hello World';
input.value.focus();
});
return {
input
};
},
};
</script>
v-for 내부 참조
v-for 내부에서 ref가 사용될 때 ref는 마운트 후 요소 배열로 채워집니다.
<template>
<ul>
<li v-for="item in list" ref="itemRefs">
{{ item }}
</li>
</ul>
</template>
<script>
import { ref, onMounted } from 'vue';
export default {
setup() {
const list = ref([1, 2, 3]);
const itemRefs = ref([]);
onMounted(() => console.log(itemRefs.value));
return {
list,
itemRefs
}
}
}
</script>
Function Refs
ref 속성에 문자열 키 대신 함수를 바인딩할 수도 있습니다.
<input :ref="(ele) => { ... }"/>
컴포넌트 Refs
ref를 자식 컴포넌트에도 사용을 할 수 있습니다.
ref로 자식 컴포넌트에 참조 값을 얻게 되면 자식 컴포넌트의 모든 속성과 메소드에 대한 전체를 접근할 수 있습니다.
이러한 경우 부모 및 자식 컴포넌트간 의존도가 생기기 때문에 이러한 방법은 반드시 필요한 경우에만 사용을 해야합니다.
ref 보다 표준 props를 사용하여 부모 및 자식간 상호작용을 구해야합니다.
/** Child Component */
<template>
<div> Child Component </div>
</template>
<script>
import { ref } from 'vue';
export default {
setup() {
const message = ref('Hello Child');
const sayHello = () => {
alert(message.value);
};
return {
message,
sayHello
};
},
};
</script>
부모 컴포넌트에서 자식 컴포넌트의 상태나 메서드에 접근을 할 수 있습니다.
/** Parent Component */
<template>
<button @click="child.sayHello()"> child.sayHello() </button>
<Child ref="child"></Child>
</template>
<script>
import { onMounted, ref } from 'vue';
import Child from '../Child.vue';
export default {
components: {
Child,
}
setup() {
const child = ref(null);
const message = ref("Welcome, This is parent component");
onMounted(() => {
console.log(child.value.message);
/** output : Hello Child */
});
return {
child,
message
};
},
};
</script>
$parent
자식 컴포넌트에서 상위 컴포넌트를 참조하기 위해서는 $parent 내장 객체를 사용할 수 있습니다.
<template>
<div> Child Component </div>
<div> {{ $parent.message }} </div>
/** output : Welcome, This is parent component */
</template>
Composition API에서 defineExpose
<script setup>을 사용하는 컴포넌트는 기본적으로 닫혀있습니다. 즉, 템플릿 참조 또는 $parent 체인을 통해 검색되는 컴포넌트의 공개 인스턴스는 <script setup> 내부에서 선언된 바인딩을 노출하지 않습니다. 이를 위해 명시적으로 노출하기 위해서는 defineExpose 컴파일러 매크로를 사용해야합니다.
<script setup>
import { ref } from 'vue';
const a = 1;
const b= ref(2);
defineExpose({
/** a와 b가 명시적으로 노출되어집니다 */
a, b
});
</script>
목록 렌더링
v-for
v-for 디렉티브를 사용하여 배열인 목록을 렌더링을 할 수 있습니다.
const items = reactive([
{ id: 1, message: 'Java' },
{ id: 2, message: 'HTML' },
{ id: 3, message: 'CSS' },
{ id: 4, message: 'JavaScript },
]);
<template>
<li v-for-"(item, index) in items" :key="index">
{{ item.message }}
</li>
</template>
key를 사용하는 이유
vue 또한, react와 마찬가지로 v-dom을 사용합니다.이때 key를 사용하여 기존 트리와 이후 트리의 자식들이 일치하는지 캐치할 수 있으며 이때 효율적인 렌더링이 가능합니다.정리하자면, key는 엘리먼트에 안정적인 고유성을 보유하기 위해 배열 내부의 엘리먼트에 지정하는 것을 의미합니다.
- v-for="item in items" 문법을 사용해서 배열에서 항목을 순차적으로 할당합니다.
- v-for="(item, index) in items" 문법을 사용해서 배열 인덱스를 가져올 수 있습니다.
- 항목을 나열할 때 각 :key 속성에는 고유한 값을 지정해야합니다.
v-for 객체
v-for을 사용하여 객체의 속성을 반복할 수도 있습니다.
const myObj = reactive({
title: '제목입니다',
author: '홍길동',
publishedAt: '2024-01-01'
});
<template>
<li v-for"(value, key, index) in myObj" :key="key">
{{ key }} - {{ value }} - {{ index }}
</li>
</template>
조건부 렌더링
v-if
v-if 디렉티브는 조건부로 블록을 렌더링 할 때 사용됩니다.
<h1 v-if="visible"> Hello Vue3! </h1>
v-else
v-else 디렉티브는 v-if는 거짓일 때 렌더링하는 블록입니다.
<h1 v-if="visible"> Hello Vue3! </h1>
<h1 v-else> Good Bye! </h1>
v-else-if
v-if에 대한 else-if 블록입니다. 여러 조건을 연결할 수 있습니다.
<h1 v-if="type === `A`"> A </h1>
<h1 v-else-if="type === `B`"> B </h1>
<h1 v-else-if="type === `C`"> C </h1>
<h1 v-else> Not A/B/C </h1>
<template v-if="">
여러 개의 HTML 요소를 v-if 디렉티브로 연결하고 싶다면, <template>을 사용할 수 있습니다.
<template v-if="visible">
<h1>Title</h1>
<p>First Paragraph</p>
<p>Twice Paragraph</p>
</template>
v-show
요소를 조건부로 표시하는 또 다른 옵션은 v-show 디렉티브입니다.
<h1 v-show="show">Title</h1>
<button @click="show = !show">Toggle Show</button>
v-if vs v-show
v-if는 실제로 화면상에 렌더링이 됩니다. 전환할 때 블록 내부의 컴포넌트들이 제거되고, 다시 생성되기 때문입니다.
또한 v-if는 lazy하며, 초기 렌더링 시에 조건이 거짓이면 아무 작업도 하지 않습니다.
조건부 블록은 조건이 처음으로 참이 될 때까지 렌더링되지 않습니다.
이에 비에 v-show의 엘리먼트는 CSS 기반 전환으로 초기 조건과 관게없이 항상 렌더링이 됩니다.
v-show는 엘리먼트를 DOM에 우선 렌더링하고 조건에 따라 display를 `block` 또는 `none` 처리를 합니다.
일반적으로 v-if는 전환 비용이 높은 반면, v-show는 초기 렌더링 비용이 높습니다.
그러므로 무언가 자주 변경 및 전환을 해야한다면 v-show을 사용하는 것이 좋고, 런타임 시에 조건이 변경되지 않는다면 v-if를 사용하는 것이 더 낫습니다.
댓글