본문 바로가기
카테고리 없음

Vue 공식 문서 훑어보기 - 감시자 · 템플릿 참조 · 조건부 및 목록 렌더링

by klm hyeon woo 2024. 3. 19.

목차

· 감시자(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를 사용하는 것이 더 낫습니다.

댓글