헷갈리는 defineProps와 defineEmits 뿌시기🔨

김규연·2024년 8월 12일

Vue

목록 보기
4/7

필자는 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 값이다.

  1. 데이터 흐름 방향
    • :current-page, :total-pages, :rows-per-page : 부모 -> 자식
    • @update:total-pages: 자식 -> 부모
  2. 사용 목적
    • :current-page, :total-pages, :rows-per-page : 자식 컴포넌트에 초기값이나 업데이트된 값을 전달할 때 사용
    • @update:total-pages : 자식 컴포넌트에서 계산된 값이나 변경된 값을 부모에게 알릴 때 사용
  3. 수정 가능성
    • :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 속성의 주요 목적과 장점은 다음과 같다.

  1. Props와 내부 상태의 동기화
    • props는 읽기 전용이므로 직접 수정할 수 없다.
    • currentPageModel을 통해 props.currentPage의 값을 읽고, 변경사항을 부모에게 알릴 수 있다.
  2. 양방향 바인딩 구현
    • v-model과 함께 사용하여 컴포넌트 내부의 값과 부모 컴포넌트의 값을 동기화 할 수 있다.
  3. 캡슐화와 재사용
    • 페이지 변경 로직을 캡슐화하여 템플릿을 더 깔끔하게 유지 가능하다.
    • 동일한 페턴을 다른 props에도 쉽게 적용할 수 있다.

결론은 사용자가 페이지를 변경할 때, set 함수가 호출되어 부모 컴포넌트에 새 값을 알리고 부모 컴포넌트가 currentPage prop을 업데이트 하면, get 함수를 통해 새 값이 반영된다.

profile
오늘도 뚠뚠 개미 개발자

0개의 댓글