본문 바로가기

Vue

Vue 템플릿 재사용을 위한 "slot"


안녕하심까!!!

어쩌다보니 이제야 쓰게되는 블로그 글 ㅎㅅaㅎ... (죄삼다! OTL)

 

개념을 정확하게 알고 정보를 전달하기보다는

제가 코드 짜면서 생각했던 것과 공부한 걸 팀원들에게 전달하는 게 목적이기 때문에 !!

말은 편하게 하겠습니닷 ㅎㅎ 

 

개발 배경부터 설명드리자면~~

저는 바우처&리워드쪽 백엔드 코드를 어느정도 마무리하고 더 필요한 api를 뽑아내기 위해서 프론트 작업으로 넘어왔답니다

일단 데이터가 화면에 제대로 찍히는 지 부터 확인하기 위해서 바우처 리스트를 출력하는 화면부터 만들어보기로 했는데요

결과는 다음과 같았습니다!

와아아~~~ 개쩐다ㅋㅋ

그런데 순간 드는 생각...

잠깐... 이 테이블 형식을 저장해두고 다른 컴포넌트에서 갖다 쓸 순 없을까?!?

 

이게 무슨 말이냐 ?

객체지향 수업시간에 항상 나오는 붕어빵틀로 예시로 들자면

이 테이블 형식을 붕어빵 틀로 삼고

고객 리스트, 직원 리스트, 바우처 리스트 등등을 이 붕어빵 틀로 찍어내자는 겁니당

물론 테이블 컬럼명이랑 가져와야하는 데이터들이 다르니 그것들은 각 컴포넌트에 맞춰서 커스텀을 하는 거죵

붕어빵 소에 따라 팥 붕어빵, 슈크림 붕어빵, 피자 붕어빵이 되는 것 마냥 ㅋㅂㅋ (안 웃김.)

 

 

Vue 공부를 시작하셨다면 아시겠지만 !

  1. props를 통해 부모 컴포넌트에서 자식 컴포넌트로 데이터를 전달할 수 있고
  2. 한 컴포넌트(부모 컴포넌트)에서 컴포넌트(자식 컴포넌트)를 랜딩할 수는 있는데

지금 이 상황은 부모 컴포넌트에서 여러 개의 자식 컴포넌트를 바꿔 끼려는 게 아니라

여러 부모 컴포넌트에서 한 자식 컴포넌트를 가져와서 커스텀해서 쓰려는 상황.

.

.

.

.

.

해줘.

 

 

가 아니라 구글링을 해봤습니다.

https://labs.brandi.co.kr//2020/02/04/chunbs.html

여기에 설명이 너~~무 잘 돼있어서 꼭 먼저 읽어보시길 바라고 (브랜디의 천보성 팀장님. 개큰감사 드립니다.)

제가 앞으로 드리는 설명에서는 위 포스팅을 기반으로 이게 저희 코드에 어떻게 적용 되었는지 + vue3이상은 v-slot이라는 조금은 다른 문법을 사용해야하기 때문에 어떻게 변형되었는지를 위주로 봐주시면 될 것 같습니다

 

 

일단은 slot을 사용하지 않은 기존의 VoucherList 컴포넌트는 어떻게 작성되었을 지 먼저 볼까요?

아 참고로 프론트 개발 전에 의논했던 대로 views폴더 안에는 상단 메뉴바에서 메뉴 클릭시 이동되는 페이지들이 있고

components 에서는 페이지에 들어가는 구성 컴포넌트들이 있습니다

저희는 VoucherPage컴포넌트에서 VoucherList 컴포넌트를 띄우는 것!

 

👇🏻 VoucherList.vue 

<template>
  <div class="voucher-list-container">
    <div class="voucher-list">
      <h2>바우처 목록</h2>
      <div v-if="vouchers.length > 0">
        <div class="voucher-header">
          <span class="voucher-column">회원 ID</span>
          <span class="voucher-column">회원명</span>
          <span class="voucher-column">발급 직원 ID</span>
          <span class="voucher-column">발급 직원명</span>
          <span class="voucher-column">금액</span>
          <span class="voucher-column">바우처 코드</span>
        </div>
        <ul class="voucher-items">
          <li
            v-for="voucher in vouchers"
            :key="voucher.id"
            class="voucher-item"
          >
            <div class="voucher-info">
              <span class="voucher-customerId">{{ voucher.customerId }}</span>
              <span class="voucher-customerName">{{
                voucher.customerName
              }}</span>
              <span class="voucher-employeeId">{{ voucher.employeeId }}</span>
              <span class="voucher-employeeName">{{
                voucher.employeeName
              }}</span>
              <span class="voucher-amount">{{ voucher.amount }}</span>
              <span class="voucher-voucherCode">{{ voucher.voucherCode }}</span>
            </div>
          </li>
        </ul>
      </div>
      <p v-else class="empty-message">바우처가 없습니다.</p>
      <p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
    </div>
  </div>
</template>

<script>
import axios from "axios";
export default {
  data() {
    return {
      vouchers: [],
      errorMessage: "",
    };
  },
  created() {
    this.fetchVouchers();
  },
  methods: {
    async fetchVouchers() {
      try {
        const response = await axios.get(
          "http://localhost:8080/api/v1/vouchers"
        );
        this.vouchers = response.data.data; // response.data.data로 접근
        this.errorMessage = "";
      } catch (error) {
        console.error("바우처 목록을 불러오는 중 에러 발생:", error);
        this.errorMessage = "바우처 목록을 불러오는 중 에러가 발생했습니다.";
      }
    },
  },
};
</script>

<style scoped>
.voucher-list-container {
  max-width: 800px;
  margin: 0 auto;
}

.voucher-list {
  background-color: #f5f5f5;
  border-radius: 10px;
  padding: 20px;
}

.voucher-header {
  display: flex;
  justify-content: space-between;
  font-weight: bold;
  margin-bottom: 10px;
}

.voucher-column {
  flex: 1;
  text-align: center;
}

.voucher-items {
  list-style-type: none;
  padding: 0;
}

.voucher-item {
  background-color: #ffffff;
  padding: 15px;
  border-radius: 5px;
  margin-bottom: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

.voucher-info {
  display: flex;
  justify-content: space-between;
}

.voucher-info span {
  flex: 1;
  text-align: center;
}

.empty-message,
.error-message {
  margin-top: 20px;
  text-align: center;
  color: #555555;
}

.error-message {
  color: red;
}
</style>

 

VoucherPage에서 이런 VoucherList를 띄우는 것 처럼

CustomerPage에선 이런 CustomerList를, EmployeePage에선 이런 EmployeeList를 불러와야하자나요?

이런 데이터 테이블의 형식과 CSS를 그대로 적용하게 하는, 붕어빵'틀'이 되는 컴포넌트를 "DataTable"이라는 이름으로 만들어봅시다!

 

 

👇🏻 DataTable.vue ( template / script / css 나눠서 )

<template>
  <div class="list-container">
    <div class="list">
      <slot name="list-name"></slot>
      <div class="header">
        <slot name="header"></slot>
      </div>
      <ul class="items">
        <slot name="items" :data="data"></slot>
      </ul>
      <p v-if="!hasData" class="empty-message">데이터가 없습니다.</p>
      <p v-if="errorMessage" class="error-message">{{ errorMessage }}</p>
    </div>
  </div>
</template>

 

부모 컴포넌트에서 커스텀 될 부분을 slot 태그로 비워두고 name 속성으로 커스텀되는 부분을 구분합니다. 

<slot name="list-name"> 은 리스트명을,

<slot name="header"> 은 리스트의 컬럼명들을,

<slot name="items"> 은 리스트의 데이터들을 담는 부분입니다.

 

<script>
import axios from "axios";

export default {
  props: {
    apiUrl: {
      type: String,
      required: true,
    },
  },
  data() {
    return {
      data: [],
      errorMessage: "",
    };
  },
  computed: {
    hasData() {
      return this.data && this.data.length > 0;
    },
  },
  created() {
    this.fetchData();
  },
  methods: {
    async fetchData() {
      try {
        const response = await axios.get(this.apiUrl);
        this.data = response.data.data;
        this.errorMessage = "";
      } catch (error) {
        console.error("데이터를 불러오는 중 에러 발생:", error);
        this.errorMessage = "데이터를 불러오는 중 에러가 발생했습니다.";
      }
    },
  },
};
</script>

 

DataTable을 갖다 쓰는 부모 컴포넌트는 어차피 목록을 출력하는 컴포넌트일테니

목록을 불러오는 메서드도 DataTable에서 제너럴하게 정의하고 재사용성을 늘리는 게 좋겠죠?

 

부모 컴포넌트에서 apiUrl을 전달하면 DataTable에서 정의한 fetchData 메서드를 해당 apiUrl에 맞춰 실행합니다.

부모 컴포넌트는 리스트 컴포넌트일테니 목록을 불러오는 api(ex."http://localhost:8080/api/v1/vouchers")를 전달하면 우리가 지정한response가 response.data로 들어오는데
저희가 response값을 code, message, data로 나눈 result response로 감싸놔서 목록을 불러오려면 response.data.data로 불러와야합니다!

 

<style scoped>
.list-container {
  max-width: 800px;
  margin: 0 auto;
}

.list {
  background-color: #f5f5f5;
  border-radius: 10px;
  padding: 20px;
}

.header {
  display: flex;
  justify-content: space-between;
  font-weight: bold;
  margin-bottom: 10px;
}

.items {
  list-style-type: none;
  padding: 0;
}

:deep(.item) {
  background-color: #ffffff;
  padding: 15px;
  border-radius: 5px;
  margin-bottom: 15px;
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
}

:deep(.item-info) {
  display: flex;
  justify-content: space-between;
}

:deep(.item-info span) {
  flex: 1;
  text-align: center;
}

.empty-message,
.error-message {
  margin-top: 20px;
  text-align: center;
  color: #555555;
}

.error-message {
  color: red;
}
</style>

 

CSS 부분에서 봐야할 것은 :deep 이라는 셀렉터인데요

DataTable 템플릿에서 설정한 css 클래스의 style은 당연히 여기서 작성이 가능하지만

이 컴포넌트를 갖다 쓰는 부모 컴포넌트의 스타일도 여기서 지정해주고 싶을 때 이걸 쓰면 된답니당

(물론 부모 컴포넌트에서도 DataTable 내의 부분들을 말하는 것! 마지막에 나오는 VoucherList 코드를 참고해주세요)

 

VouhcerList 템플릿의 item 클래스에 CSS를 적용시키고싶다면 

:deep(.item-info) {
  display: flex;
  justify-content: space-between;
}

 

이렇게 써주면 된다는 겁니당

그래서.. VoucherList 뿐만 아니라 DataTable을 갖다 쓰는 List 관련 컴포넌트들의 css class 명들을 규칙에 맞게 통일 시켜준다면

각 컴포넌트에 매번 길게 CSS를 설정해주지 않아도 되겠다는 것. 쯤은 다들 예상하셨겠쬬?! 😎

 

 

마지막으로 DataTable 컴포넌트를 갖다 쓰도록 수정한 VoucherList 컴포넌트를 한 번 보실까요?

👇🏻 VoucherList.vue

<template>
  <DataTable apiUrl="http://localhost:8080/api/v1/vouchers">
    <template v-slot:list-name>
      <h2>바우처 목록</h2>
    </template>
    <template v-slot:header>
      <span>회원 ID</span>
      <span>회원명</span>
      <span>발급 직원 ID</span>
      <span>발급 직원명</span>
      <span>금액</span>
      <span>바우처 코드</span>
    </template>
    <template v-slot:items="{ data }">
      <li v-for="voucher in data" :key="voucher.id" class="item">
        <div class="item-info">
          <span>{{ voucher.customerId }}</span>
          <span>{{ voucher.customerName }}</span>
          <span>{{ voucher.employeeId }}</span>
          <span>{{ voucher.employeeName }}</span>
          <span>{{ voucher.amount }}</span>
          <span>{{ voucher.voucherCode }}</span>
        </div>
      </li>
    </template>
  </DataTable>
</template>

<script>
import DataTable from "./DataTable.vue";

export default {
  components: { DataTable },
};
</script>

<style scoped></style>

 

DataTable 태그로 DataTable 컴포넌트를 불러오고, apiUrl도 props로 전달합니다.

<template v-slot:list-name>, <template v-slot:header>, <template v-slot:items>이 각각

DataTable에서 <slot name="list-name">, <slot name="header">, <slot name="items"> 로 비워둔 부분을
해당 컴포넌트에 맞게 커스텀하는 부분입니다.

 

DataTable은 전달받은 apiUrl을 통해 데이터를 불러오고 data 속성에 저장한 뒤 data를 items 슬롯에 전달합니다. (DataTable 코드 참고)

VoucherList는 v-slot:items="{ data }" 를 통해 data를 받아서, data에 있는 항목들을 v-for을 사용해 반복하여 리스트를 출력합니다.

 

 

처음에 작성한 VoucherList 코드보다 훨씬 간결해졌쬬?

이렇게 slot을 활용하면 DataTable 하나만 구현해 두면 List 형식의 컴포넌트에서 테이블을 도장 찍듯이 찍어낼 수 있답니다

코드가 간결해질 뿐만 아니라 템플릿 형식과 CSS도 통일시켜줄 수 있다니!! 완전 럭키비키잖아~?🤭🍀

 

 

일단은 빠른 실습을 위해 간결하게 slot 코드를 작성해보았는데요

이제 잘 다듬어서 CustomerList, EmployeeList 등 모든 리스트 컴포넌트에 예쁘게 적용될 수 있도록 노력해봐야겠습니당

아자아자 !!! ٩(*•̀ᴗ•́*)و 

 

 

ps. 개열시미 썼으니까 댓글 써주세요 안 써주면 이게 마지막 포스팅이 될 것임...

'Vue' 카테고리의 다른 글

[vue3 Quasar with pinia] 로그인 및 인증(2)  (0) 2024.06.13
[vue3 with pinia] 로그인 및 인증(1)  (0) 2024.06.13