반응형 객체의 변화를 감지하는 것은 Watcher 뿐만이 아닙니다. 이전에 filteredList
를 만들고 watcher에서 할당했습니다. 이와 같은 방법으로 반응형 객체의 변화에 따라 DOM을 새롭게 그릴 수 있는 두 가지 방법이 있습니다.
- Method (getter 함수)
- Computed
예를 들어, 버튼을 3번 누를 때마다 1이 증가하는 코드를 만들었다고 가정해 봅시다.
<template>
<button @click="counter++">
{{ parseInt(counter / 3) }}
</button>
</template>
<script setup>
import { ref } from "vue";
const counter = ref(0);
</script>

위 예제를 Watcher, Method, Computed 3가지 방식으로 동일하게 구현하면서 비교해본 다음, 우리 블로그 프로젝트에 적용해보겠습니다.
Watcher
먼저 기존에 배운 Watcher를 사용했다면 다음과 같이 구현할겁니다.
<template>
<button @click="counter++">
{{ computedCounter }}
</button>
</template>
<script setup>
import { ref, watch } from "vue";
const counter = ref(0);
const computedCounter = ref(0);
watch(counter, (c) => {
computedCounter.value = parseInt(c / 3);
});
</script>
computedCounter
변수를 추가하고 watch
함수를 이용해서 업데이트합니다. 처음 예시로 들었던 방법에 비해 template의 코드가 줄어들어 보기 편해졌지만, 반대로 script에서의 코드가 많이 늘어났습니다.
Method
함수에서 반응형 객체를 사용하는 경우, 값이 변하면 함수의 반환값도 달라지게 됩니다. Vue는 이를 감지하고 DOM을 자동으로 업데이트합니다. 따라서 다음과 같이 바꿀 수 있습니다.
<template>
<button @click="counter++">{{ computedCounter() }}</button>
</template>
<script setup>
import { ref } from "vue";
const counter = ref(0);
const computedCounter = () => parseInt(counter.value / 3);
</script>
코드가 더 짧아졌을 뿐만 아니라 더 직관적이게 되었습니다. 이렇게해도 충분하지만 성능면에서 한 가지 아쉬운 점이 있습니다. 이 값을 동일하게 다른 곳에서도 사용하고 싶어서 태그를 추가했다고 해보겠습니다.
<template>
<button @click="counter++">{{ computedCounter() }}</button>
<div>{{ computedCounter() }}</div>
<div>{{ computedCounter() }}</div>
<div>{{ computedCounter() }}</div>
</template>
<script setup>
import { ref } from "vue";
const counter = ref(0);
const computedCounter = () => {
console.log(counter.value);
return parseInt(counter.value / 3);
};
</script>
이런 경우 값을 넣기 위해 넣는 곳마다 함수를 실행시킵니다.

Watcher는 Method보다 한 번만 실행되지만 코드가 길고 불편합니다. computed
은 Watcher처럼 한 번만 실행하면서, Method처럼 간결한 코드로 DOM을 업데이트할 수 있는 방법입니다.
Computed
computed
함수는 반응형 객체를 사용하는 함수를 입력 받아, 해당 함수가 반환한 값을 이용해 읽기 전용 반응형 객체를 반환합니다. 함수 내부의 반응형 객체 변화를 자동으로 감지하여 DOM을 업데이트하는 것은 이전과 동일합니다.
<template>
<button @click="counter++">{{ computedCounter }}</button>
<div>{{ computedCounter }}</div>
<div>{{ computedCounter }}</div>
<div>{{ computedCounter }}</div>
</template>
<script setup>
import { ref, computed } from "vue";
const counter = ref(0);
const computedCounter = computed(() => {
console.log(counter.value);
return parseInt(counter.value / 3);
});
</script>
computed
에 넣은 함수가 반환하는 값을 computedCounter
라는 반응형 객체가 받게됩니다.

computed
는 Method
와 달리, 한 번 계산한 값을 재활용합니다. 이를 캐싱(caching)이라고 합니다. 덕분에 불필요한 호출을 방지할 수 있습니다.
코드 수정하기
이제 이전에 구현했던 코드를 수정해보겠습니다. 이전에는 filteredList
값을 만들고, getPageTable
에서도 사용하고 watch
에서도 사용했었습니다.
<template>
...
<li v-for="item in filteredList" :key="item.id">
...
</template>
<script setup>
import { ref, watch } from "vue";
// ...
const filteredList = ref([]);
getPageTable("088ad41f4b9f4293aa11a4670359f085").then((data) => {
// ...
filteredList.value = postList.value[props.selectedCategory];
});
watch(search, (s) => {
if (s == "") {
filteredList.value = postList.value[props.selectedCategory];
} else {
const result = [];
for (const category in postList.value) {
for (const item of postList.value[category]) {
if (item.title.includes(s)) {
result.push(item);
}
}
}
filteredList.value = result;
}
});
</script>
이제 다음과 같이 수정하면 훨씬 간결해집니다.
<template>
...
<li v-for="item in filteredList" :key="item.id">
...
</template>
<script setup>
import { ref, computed } from "vue";
// ...
const filteredList = computed(() => {
if (search.value == "") {
return postList.value[props.selectedCategory];
} else {
const result = [];
for (const category in postList.value) {
for (const item of postList.value[category]) {
if (item.title.includes(search.value)) {
result.push(item);
}
}
}
return result;
}
});
</script>
getPageTable
에서 굳이 할당할 필요없이 postList
와 selectedCategory
의 값 변화를 감지해 filteredList
값을 업데이트 해줍니다.
전체코드
<template>
<input class="search" v-model="search" />
<main class="post-list">
<h2 class="category" v-show="!search">카테고리 : {{ selectedCategory }}</h2>
<ul>
<li v-for="item in filteredList" :key="item.id">
<button class="post" @click="$emit('click-post', item.id)">
{{ item.title }}
</button>
</li>
</ul>
</main>
</template>
<script setup>
import { ref, computed } from "vue";
import { getPageTable } from "vue-notion";
const search = ref("");
const props = defineProps(["selectedCategory"]);
const postList = ref({});
getPageTable("088ad41f4b9f4293aa11a4670359f085").then((data) => {
const result = {};
for (const post of data) {
const c = post.category;
if (!result[c]) {
result[c] = [];
}
result[c].push(post);
}
postList.value = result;
});
const filteredList = computed(() => {
if (search.value == "") {
return postList.value[props.selectedCategory];
} else {
const result = [];
for (const category in postList.value) {
for (const item of postList.value[category]) {
if (item.title.includes(search.value)) {
result.push(item);
}
}
}
return result;
}
});
</script>
<style>
.search {
margin-top: 10px;
padding: 5px 3px;
border-radius: 5px;
border: 1px solid #777;
}
.post-list button {
border: 0;
background-color: transparent;
color: rgb(101, 129, 139);
cursor: pointer;
}
.post-list button:hover {
color: blue;
}
</style>