📝 Outline
- 메인 페이지에 이어서 SHOP 페이지 구현이 끝났다.
- SHOP 페이지는 비교적 구현이 단순해서 어렵지 않게 구현할 수 있었다.
- 구조를 살펴보면 상품 리스트들이 쭉 나열되어 있고 상품 이름과 가격이 명시되어 있다.
- 추가로 Select Box를 통해 정렬 기준을 정하여 상품 리스트를 원하는 옵션대로 정렬하여 볼 수 있도록 구현하였다.
- 페이지 하단에는 "Load More" 버튼을 두어, 버튼 클릭 시 상품 리스트를 추가로 로드할 수 있도록 구현해주었다.
📝 Review
- 이번에도 역시 리뷰하는 시간을 가져볼까 한다.
- SHOP 페이지를 구현하면서 사용한 코드와 그 코드를 사용한 이유, 그리고 새롭게 알게된 내용에 대해 이야기 해보겠다.
💬 <a> 태그를 사용하지 않고 <router-link>를 사용한 이유
<!-- Home Button -->
<router-link :to="'/'" class="a-to_home">
<font-awesome-icon :icon="['fab', 'blogger-b']" size="2xl" />
</router-link>
<!-- Home Button End -->
- 위 코드는
Header.vue
에 작성되어 있는 코드 중 일부이다. - 기존에
<a>
태그로 작성해주었던 부분을 이번에<router-link>
로 바꾸어주었다. - 그 이유는 다음과 같다.
<a>
태그의href
로 페이지를 이동하면 상태 값을 잃고 속도가 저하된다.- Vue는 단일 url을 가지고 SPA(Single Page Application)로 사이트를 표현하는 프레임워크로, 하나의 HTML 페이지와 애플리케이션 실행에 필요한 JavaScript나 CSS와 같은 모든 자산을 로드하는 애플리케이션이다.
- 해당 이유로 페이지를 새로 불러오게 되면 앱이 지니고 있는 상태가 초기화되고, 렌더링 된 컴포넌트도 모두 사라지기 때문에 새로 렌더링을 해야 한다.
- 따라서 상태 유지와 속도의 효율성을 위해서는 새로운 페이지를 불러오는 대신 업데이트하는 방식으로 구현해야 한다.
<router-link>
컴포넌트는 HTML5 History API를 사용하여 브라우저의 주소만 바꿀 뿐, 페이지를 새로 불러오지는 않는다. 따라서 Vue에서는<router-link>
컴포넌트 사용을 권장한다.
💬 CSS 네이밍 규칙
- 프로젝트를 진행하면서 CSS 관련 코드가 늘어남에 따라 이후의 작업에 규칙성을 두기 위해 CSS 네이밍 규칙을 설정해보았다.
- 찾아본 바로는 CSS 네이밍 규칙은 주로 다음과 같이 구성한다고 한다.
- (형태)(의미)(순서)_(상태)
- ex)
msgbox-toggle-01_off
- 따라서 이와 비슷하게 클래스 이름을 변경해주었다.
- 아래는 내가 작성한 CSS 코드의 일부이다.
<style>
.ul-clothing_list {
list-style: none;
}
.li-clothing_list_item {
float: left;
margin: 0 20px 80px 0;
}
.div-clothing_list_item_wrapper {
width: 345px;
height: 345px;
}
.div-clothing_name {
font-size: 17px;
line-height: 23.8px;
margin-top: 5px;
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.div-clothing_price {
font-size: 16px;
line-height: 16px;
color: #666666;
margin-top: 5px;
}
.div-load_more_btn_wrapper {
margin: 40px 0 40px 0;
clear: both;
}
.btn-load_more {
background-color: black !important;
color: white !important;
border-radius: 0px !important;
font-size: 17px;
line-height: 23.8px;
text-align: center;
word-spacing: 0px;
height: 43.8px;
width: 316px;
padding: 10px 20px 10px 20px;
}
</style>
💬 JPG가 아닌 WEBP를 사용한 이유
- WebP는 인터넷에서 이미지가 로딩되는 시간을 단축하기 위해 Google이 출시한 파일 포맷이다.
- 기존에 JPG 파일을 사용하여 이미지를 로드했었는데, 이를 WebP 파일로 변경해주었다.
- WebP 파일을 사용했을 때 얻는 장점은 다음과 같다.
- WebP 포맷은 PNG, JPEG 등 기존 포맷보다 파일 크기가 작아진다. 이미지 크기가 작을수록 사용자의 광대역 네트워크 연결에 대한 부담이 줄어들고 웹 사이트 탐색 속도가 빨라져 웹 경험이 향상된다.
- WebP 포맷은 Google Chrome, Microsoft Edge, Mozilla Firefox 등 다양한 웹 브라우저와 호환된다.
- WebP는 웹 사이트 이미지를 관리하기 쉬운 크기로 압축하므로, 파일 저장 공간이 여유롭다.
- 다만 WebP 파일 포맷은 인터넷을 염두에 두고 설계되었으므로 오프라인에서 이미지를 사용하는 경우 도움이 되지 않을 수 있으며, Internet Explorer 등 일부 옛 브라우저의 경우, WebP 이미지를 지원하지 못할 수 있다.
- 또한 약간의 압축으로도 이미지 품질이 저하될 수 있기 때문에, 고품질 이미지로 작업해야 하는 경우 이를 고려해야 한다.
💬 Load More
- 이번에 "Load More" 버튼을 구현하면서 Vue에 대해 느낀점은 정말 쉽고 편하다는 것이다.
- 이전에 JSP를 사용했을 때에는 자바스크립트 문법을 사용해서 상당히 복잡하고 귀찮게 수행했던 작업을 매우 간결하게 처리할 수 있었다.
- 그 방법은 다음과 같은데,
<template>
<div class="div-container">
<!-- Product List -->
<div>
<ul class="ul-clothing_list">
<li class="li-clothing_list_item" v-for="i in clothingToShow" :key="i">
<div class="div-clothing_list_item_wrapper">
<v-img :src="require(`@/assets/${clothing[i - 1].url}`)" />
<div align="left">
<div class="div-clothing_name">{{clothing[i - 1].name}}</div>
<div class="div-clothing_price">{{clothing[i - 1].price}}₩</div>
</div>
</div>
</li>
</ul>
</div>
<!-- Product List End -->
<!-- Load More -->
<div v-if="clothingToShow < totalClothing" class="div-load_more_btn_wrapper" align="center">
<v-btn class="btn-load_more" elevation="0" @click="loadMore">Load More</v-btn>
</div>
<!-- Load More End -->
</div>
</template>
<script>
export default {
data() {
return {
clothing: [],
totalClothing: 0,
clothingToShow: 0,
loadDataCount: 16,
}
},
mounted() {
this.getData();
},
methods: {
// 상품 데이터 가져오기
getData() {
this.$axios.get('/test').then((res) => {
res.data.forEach((element) => {
this.clothing.push({
name: element.name,
price: element.price.toLocaleString("ko-KR"),
url: element.url,
register_date: element.register_date
});
});
this.totalClothing = this.clothing.length;
this.clothingToShow = Math.min(this.loadDataCount, this.totalClothing);
});
},
// 상품 데이터 추가 로드
loadMore() {
this.clothingToShow += Math.min(this.loadDataCount, this.totalClothing - this.clothingToShow);
},
},
};
</script>
- 코드가 꽤 많으니 하나씩 살펴보도록 하자.
- 우선 axios를 통해 서버에서 데이터를 가져오는 것까지는 동일하다.
getData() {
this.$axios.get('/test').then((res) => {
res.data.forEach((element) => {
this.clothing.push({
name: element.name,
price: element.price.toLocaleString("ko-KR"),
url: element.url,
register_date: element.register_date
});
});
this.totalClothing = this.clothing.length;
this.clothingToShow = Math.min(this.loadDataCount, this.totalClothing);
});
},
- 다만 여기서 핵심은 이 부분이다.
this.totalClothing = this.clothing.length;
this.clothingToShow = Math.min(this.loadDataCount, this.totalClothing);
totalClothing
과clothingToShow
는 각각 얻어온 전체 데이터의 수와 리스트에 출력될 데이터의 수를 의미하며,data()
부분에 정의되어 있다.loadDataCount
는 한 번에 출력될 데이터의 수를 의미하며, 즉 "Load More" 버튼 클릭 시 추가로 로드될 데이터의 수를 의미하기도 한다.- 만약 전체 데이터의 수(
totalClothing
)가 기본으로 출력되어야 하는 데이터의 수(loadDataCount
)보다 작을 경우, 위의 반복문(v-for="i in clothingToShow"
)에서 예외가 발생할 수 있기 때문에 이를 방지하기 위해Math.min()
함수를 사용하여 둘 중 최솟값을 선택하도록 작성하였다.
<!-- Product List -->
<div>
<ul class="ul-clothing_list">
<li class="li-clothing_list_item" v-for="i in clothingToShow" :key="i">
<div class="div-clothing_list_item_wrapper">
<v-img :src="require(`@/assets/${clothing[i - 1].url}`)" />
<div align="left">
<div class="div-clothing_name">{{clothing[i - 1].name}}</div>
<div class="div-clothing_price">{{clothing[i - 1].price}}₩</div>
</div>
</div>
</li>
</ul>
</div>
<!-- Product List End -->
- 상품 리스트를 출력하는 코드는 위와 같으며,
v-for
를 통해 반복문을 돌리며, 리스트를 출력하고 있다. - "Load More" 버튼은 아래와 같이 구현되어 있다.
<!-- Load More -->
<div v-if="clothingToShow < totalClothing" class="div-load_more_btn_wrapper" align="center">
<v-btn class="btn-load_more" elevation="0" @click="loadMore">Load More</v-btn>
</div>
<!-- Load More End -->
- 살펴보면,
v-if
를 통해 현재 보여지고 있는 데이터의 수(clothingToShow
)가 전체 데이터의 수(totalClothing
)보다 작을 경우, 즉 아직 추가로 로드할 데이터가 남아있는 경우에만 해당 버튼이 보여지도록 구현되어 있다. - 버튼 클릭 시 호출되는
loadMore()
메서드는 아래와 같이 구현되어 있다.
// 상품 데이터 추가 로드
loadMore() {
this.clothingToShow += Math.min(this.loadDataCount, this.totalClothing - this.clothingToShow);
},
- 즉,
clothingToShow
를 증가시킴으로써 위에서 구현한 반복문의 반복 횟수(v-for="i in clothingToShow"
)를 업데이트하는 것이다. - 다만, 이때 사용자에게 보여질 데이터의 수(
clothingToShow
)가 전체 데이터의 수(totalClothing
)보다 커지면 안되기 때문에 역시Math.min()
함수를 사용해주었다. - 이후 결과를 살펴보면?
💬 Sort By
- 정렬 기능을 구현하면서도 Vue의 편리함을 확인할 수 있었다.
- 우선 코드는 아래와 같다.
<template>
<div class="div-container">
<!-- Select Box -->
<div class="div-select_parent">
<div class="div-select_child">
<v-select
class="select-sort"
label="Sort by"
:items="sortOptions"
@change="sortBy"
outlined
dense
:menu-props="{ bottom: true, offsetY: true }"
></v-select>
</div>
</div>
<!-- Select Box End -->
</div>
</template>
<script>
export default {
data() {
return {
sortOptions: [
'Newest',
'Price (low to high)',
'Price (high to low)',
'Name A-Z',
'Name Z-A'
]
}
},
methods: {
// 상품 리스트 정렬
sortBy(option) {
if (option == this.sortOptions[0]) { // Newest
this.clothing.sort((a, b) => {
return new Date(b.register_date).getTime() - new Date(a.register_date).getTime();
});
} else if (option == this.sortOptions[1]) { // Price (low to high)
this.clothing.sort((a, b) => {
return Number(a.price.replace(',', '')) - Number(b.price.replace(',', ''));
});
} else if (option == this.sortOptions[2]) { // Price (high to low)
this.clothing.sort((a, b) => {
return Number(b.price.replace(',', '')) - Number(a.price.replace(',', ''));
});
} else if (option == this.sortOptions[3]) { // Name A-Z
this.clothing.sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else if (option == this.sortOptions[4]) { // Name Z-A
this.clothing.sort((a, b) => {
return b.name.localeCompare(a.name);
});
}
}
},
};
</script>
- 역시 하나씩 살펴보도록 하자.
- 우선 Select Box 구현 코드이다.
<!-- Select Box -->
<div class="div-select_parent">
<div class="div-select_child">
<v-select
class="select-sort"
label="Sort by"
:items="sortOptions"
@change="sortBy"
outlined
dense
:menu-props="{ bottom: true, offsetY: true }"
></v-select>
</div>
</div>
<!-- Select Box End -->
:items
는 Select Box에 표시될 Option들을 배열의 형태로 저장하도록 해주는 속성으로,sortOptions
는 아래와 같이 정의되어 있다.
<script>
export default {
data() {
return {
sortOptions: [
'Newest',
'Price (low to high)',
'Price (high to low)',
'Name A-Z',
'Name Z-A'
]
}
},
}
- 문장만 보아도 알겠지만, 각각 날짜순, 가격순, 이름순으로 정렬하는 것을 의미한다.
- 또한
@change
는 Select Box의 옵션이 변경될 시 수행할 행위를 설정할 수 있으며, 이때 수행할 메서드는 아래와 같이 구현하였다.
// 상품 리스트 정렬
sortBy(option) {
if (option == this.sortOptions[0]) { // Newest
this.clothing.sort((a, b) => {
return new Date(b.register_date).getTime() - new Date(a.register_date).getTime();
});
} else if (option == this.sortOptions[1]) { // Price (low to high)
this.clothing.sort((a, b) => {
return Number(a.price.replace(',', '')) - Number(b.price.replace(',', ''));
});
} else if (option == this.sortOptions[2]) { // Price (high to low)
this.clothing.sort((a, b) => {
return Number(b.price.replace(',', '')) - Number(a.price.replace(',', ''));
});
} else if (option == this.sortOptions[3]) { // Name A-Z
this.clothing.sort((a, b) => {
return a.name.localeCompare(b.name);
});
} else if (option == this.sortOptions[4]) { // Name Z-A
this.clothing.sort((a, b) => {
return b.name.localeCompare(a.name);
});
}
}
- Select Box로 선택한 옵션의 종류는 해당 메서드의 매개변수로 들어오게 되며, 우리는 이를 통해 분기시켜 옵션마다 다른 행위를 취할 수 있다.
- 여기서는 당연히 옵션에 따라 정렬 방식을 다르게 하여 데이터를 정렬하고 있다.
- 이후 결과를 살펴보면?
'🚗 Backend Toy Project > Baeg-won Clothing Gallery' 카테고리의 다른 글
[Baeg-won Clothing Gallery] 5. SALE, CONTACT 페이지 구현 (0) | 2023.07.15 |
---|---|
[Baeg-won Clothing Gallery] 4. CLOTHING, FOOTWEAR, ACCESSORIES 페이지 구현 (0) | 2023.07.14 |
[Baeg-won Clothing Gallery] 3. BRANDS 페이지 구현 (0) | 2023.07.08 |
[Baeg-won Clothing Gallery] 1. 메인 페이지 구현 (0) | 2023.07.04 |
[Baeg-won Clothing Gallery] 0. 프로젝트 개요 (0) | 2023.07.02 |