필자는 Page를 관리하는 컴포넌트인 PageComponent.vue 와 게시글 조회를 관리하는 컴포넌트인 PostsComponent.vue를 따로 만들었다. 그리고 PostsPage.vue에 두 component를 합쳤다. 여기서 PostsPage가 부모 컴포넌트고 PageComponent와 PostsComponent가 자식 컴포넌트라고 봤다.
먼저 부모 컴포넌트인 PostsPage.vue를 살펴보자.
<template>
<q-page class="column items-center justify-start">
<div class="col-12 q-pa-md">
<posts-component
:current-page="currentPage"
:rows-per-page="rowsPerPage"
@update:total-pages="totalPages = $event"
></posts-component>
</div>
<div class="col-12 q-pa-md">
<page-component
v-model:current-page="currentPage"
:total-pages="totalPages"
></page-component>
</div>
</q-page>
</template>
<script setup lang="ts">
import { ref } from 'vue';
import PostsComponent from 'components/PostsComponent.vue';
import PageComponent from 'components/PageComponent.vue';
const currentPage = ref(1);
const rowsPerPage = ref(20);
const totalPages = ref(0);
</script>
📌 여기서 :current-page="currentPage" 와 :total-pages="totalPages", :rows-per-page="rowsPerPage"는 단반향 바인딩이다. 부모 컴포넌트에서 자식 컴포넌트로 데이트를 전달하며, 자식 컴포넌트는 이 값을 읽을 수 있지만, 직접 수정할 수 없다. 부모의 값이 변경되면 자식 컴포넌트에 자동으로 반영된다.
📌 @update:total-pages="totalPages = $event"는 이벤트 리스너이다. 자식 컴포넌트에서 부모 컴포넌트로 데이터를 전달하는 방식이다. 자식 컴포넌트에서 emit('update:total-pages', newValue)를 호출하면, 부모 컴포넌트의 totalPages가 새 값으로 업데이트된다. $event는 자식 컴포넌트에서 emit한 값, 즉 newValue 값이다.
- 데이터 흐름 방향
- :current-page, :total-pages, :rows-per-page : 부모 -> 자식
- @update:total-pages: 자식 -> 부모
- 사용 목적
- :current-page, :total-pages, :rows-per-page : 자식 컴포넌트에 초기값이나 업데이트된 값을 전달할 때 사용
- @update:total-pages : 자식 컴포넌트에서 계산된 값이나 변경된 값을 부모에게 알릴 때 사용
- 수정 가능성
- :current-page, :total-pages, :rows-per-page : 자식 컴포넌트에서 직접 수정 불가
- @update:total-pages: 자식 컴포넌트에서 이벤트를 통해 부모의 값을 간접적으로 수정 가능
🔗 vue3에서 v-model을 사용하면 이 두 가지 방식을 결합할 수 있다.
<page-component
v-model:current-page="currentPage"
:total-pages="totalPages"
></page-component>
이 방식은 :current-page="currentPage"와 @update:current-page="currentPage = $event"를 동시에 적용한 것과 같다. 이를 통해 양방향 바인딩을 간단하게 구현할 수 있다.
결론적으로 단순의 데이터를 전달만 할때는 :prop명을 사용하고, 자식 컴포넌트에서 부모의 데이터를 업데이트해야 할 때는 @update:prop명 이번트를 사용하거나 v-model:prop명을 사용하는 것이 일반적이다.
PostsComponent.vue 를 살펴보자.
<template>
<div class="q-pa-md">
<q-table
flat
bordered
title="게시물 목록"
:rows="rows"
:columns="postColumn"
:rows-per-page-options="[pagination.rowsPerPage]"
row-key="brdId"
hide-bottom
/>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { posts } from '../apicontroller/posts';
import { PostDto, PostData } from '../assets/interfaces/index';
import { postColumn } from '../assets/column/index';
const props = defineProps<{
currentPage: number;
rowsPerPage: number;
}>();
const emit = defineEmits(['update:totalPages']);
const rows = ref<PostData[]>([]);
const pagination = ref({
page: props.currentPage,
rowsPerPage: props.rowsPerPage,
totalPages: 0,
});
const fetchPosts = async () => {
try {
const response = await posts(
pagination.value.page,
pagination.value.rowsPerPage
);
rows.value = response.items.map((item: PostDto) => ({
brdId: item.post_id,
postTitle: item.post_title,
writer: item.post_username,
department: item.mem_address1,
postDatetime: item.post_datetime,
postHit: item.post_hit,
}));
pagination.value.totalPages = response.totalPages;
emit('update:totalPages', response.totalPages);
} catch (error) {
console.error('게시물 가져오기 실패: ', error);
}
};
watch(
() => props.currentPage,
(newPage) => {
pagination.value.page = newPage;
fetchPosts();
}
);
onMounted(fetchPosts);
</script>
📌 defineProps에서 currentPage와 rowsPerPage는 부모 컴포넌트에서 받아온거다. 여기서 defineProps는 반응형 객체를 반환하므로, props.currentPage와 같이 접근이 가능하다.
📌 defineEmits는 컴포넌트가 발생시킬 수 있는 이벤트를 정의한다. 컴포넌트에서 emit('update:total-pages', value)를 호출하여 이벤트를 발생시킬 수 있다.
emit('update:totalPages', response.totalPages);
🔗 watch는 특정 반응형 데이터의 변화를 감시하고, 변화가 발생했을 때 콜백 함수를 실행하는 함수이다.
watch(
() => props.currentPage,
(newPage) => {
pagination.value.page = newPage;
fetchPosts();
}
);
첫 번째 인자는 감시할 데이터 소스이다. 여기서는 props.current-page를 감시한다.
두 번째 인자는 데이터가 변경될 때 실행될 콜백 함수이다. props.currentPage가 변경될 때마다 pagination.value.page를 업데이트하고 fetchPosts()를 호출한다.
PageComponent.vue를 살펴보자.
<template>
<div class="q-pa-lg flex flex-center">
<q-pagination
v-model="currentPageModel"
:max="totalPages"
:max-pages="9"
:ellipses="false"
:boundary-numbers="false"
direction-links
boundary-links
icon-first="skip_previous"
icon-last="skip_next"
icon-prev="fast_rewind"
icon-next="fast_forward"
/>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
const props = defineProps<{
currentPage: number;
totalPages: number;
}>();
const emit = defineEmits(['update:currentPage']);
const currentPageModel = computed({
get: () => props.currentPage,
set: (value) => emit('update:currentPage', value),
});
</script>
📌 defineProps에서 currentPage와 totalPages는 부모 컴포넌트에서 받아온 것이다.
📌 currentPageModel은 computed 속성을 사용하여 만들었고 getter와 setter를 모두 가지고 있어 양방향 바인딩을 구현한다.
get 함수는 currentPageModel의 값을 읽을 때 호출된다. 여기서 props.currentPage의 값을 반환한다. 즉, 부모 컴포넌트에서 전달받은 현재 페이지 번호를 사용한다.
set 함수는 currentPageModel의 값을 변경하려고 할 때 호출된다. 새로운 값(value)를 인자로 받아, update:currentPage 이벤트를 발생시킨다. 해당 이벤트는 부모 컴포넌트에 새로운 페이지 번호를 알리는 역할을 한다.
🔗 여기서 computed 속성의 주요 목적과 장점은 다음과 같다.
결론은 사용자가 페이지를 변경할 때, set 함수가 호출되어 부모 컴포넌트에 새 값을 알리고 부모 컴포넌트가 currentPage prop을 업데이트 하면, get 함수를 통해 새 값이 반영된다.