나의 개발 일지

디자인 마켓플레이스 - 주요기능 1차 개발 + 스켈레톤 + 기본 예외처리 적용

designer DK 2025. 1. 28. 10:23
728x90

디자인 마켓플레이스 프로젝트 1차 개발물에 기본적인 예외처리를 적용하고 코드들을 한번 점검하는 시간을 가졌다.

// 에러 메시지 상수 정의
export const SYSTEM_MESSAGES = {
// 에러
general: "We're having trouble with our service",
category: "Unable to load categories",
asset: "Unable to load assets",
collection: "Unable to load collection",
cart: "Unable to load cart",
orderSummary: "Unable to load order summary",
api: "Server communication error",
cartDuplicate: "Already in cart",

// 빈페이지
empty: "No data found",
assetEmpty: "No assets found",
cartEmpty: "No assets in cart",
collectionEmpty: "No assets in collection",
orderSummaryEmpty: "No order history",

// 성공
cartAdd: "Added to cart",
collectionAdd: "Added to collection",

// 안내
loginRequired: "Please login to use this feature",
cartRemove: "Removed from cart",
collectionRemove: "Removed from collection"
};

// 전역 에러 및 빈페이지 핸들링
window.handleError = (error) => {
const main = document.querySelector('main');
main.innerHTML = '';
systemUI.showErrorUI(error.message, main);
systemUI.hideLoading();
console.error(error);
}
window.handleEmpty = (error) => {
const main = document.querySelector('main');
main.innerHTML = '';
systemUI.showEmptyUI(error.message, main);
systemUI.hideLoading();
console.error(error);
}
window.handleAssetEmpty = (error) => {
const assetListContainer = document.getElementById('asset-list-container');
assetListContainer.innerHTML = '';
systemUI.showEmptyUI(error.message, assetListContainer);
systemUI.hideLoading();
console.error(error);
}
window.handleCartEmpty = (error) => {
const cartListContainer = document.getElementById('cart-list-container');
const cartOrderContainer = document.getElementById('cart-order-container');
cartListContainer.innerHTML = '';
cartOrderContainer.innerHTML = '';
systemUI.showEmptyUI(error.message, cartListContainer);
systemUI.hideLoading();
console.error(error);
}
window.handleOrderSummaryEmpty = (error) => {
const orderSummaryListContainer = document.getElementById('order-summary-list-container');
orderSummaryListContainer.innerHTML = '';
systemUI.showEmptyUI(error.message, orderSummaryListContainer);
systemUI.hideLoading();
console.error(error);
}

// UI 관련 유틸리티 함수들
export class SystemUI {
constructor() {
if (SystemUI.instance) {
return SystemUI.instance;
}
SystemUI.instance = this;
 
// DOM 요소 캐싱
this.loadingOverlay = document.querySelector('loading-overlay');
 
// 토스트 컨테이너 생성
this.createToastContainer();
}

// 로딩 관련
showLoading() {
if (this.loadingOverlay) {
this.loadingOverlay.show();
}
}

hideLoading() {
if (this.loadingOverlay) {
this.loadingOverlay.hide();
}
}

// 토스트 관련
createToastContainer() {
const toastContainer = document.createElement('div');
toastContainer.id = 'toast-container';
toastContainer.style.cssText = `
position: fixed;
top: 20px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
display: flex;
flex-direction: column;
align-items: center;
z-index: 1000;
`;
document.body.appendChild(toastContainer);
this.toastContainer = toastContainer;
}

showToast(message, type = 'success') {
const toast = document.createElement('div');
toast.className = `toast toast-${type}`;
 
// 타입별 아이콘 설정
const icon = {
'success': '✓',
'error': '!',
'info': 'ℹ'
}[type] || 'ℹ';
 
toast.innerHTML = `
<div class="toast-content">
${icon} ${message}
</div>
`;
 
toast.style.cssText = `
background-color: ${
type === 'success' ? '#4CAF50' :
type === 'error' ? '#f44336' :
'#2196F3' // info 색상
};
color: white;
padding: 16px;
border-radius: 4px;
margin-top: 8px;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
animation: slideIn 1s ease-out forwards;
opacity: 0;
transform: translateY(-20px);
`;

this.toastContainer.appendChild(toast);

// 애니메이션 적용 (위에서 아래로)
requestAnimationFrame(() => {
toast.style.opacity = '1';
toast.style.transform = 'translateY(0)';
});

// 3초 후 제거
setTimeout(() => {
toast.style.opacity = '0';
toast.style.transform = 'translateY(-20px)';
setTimeout(() => {
this.toastContainer.removeChild(toast);
}, 300);
}, 3000);
}
// 성공 실패 토스트
showSuccessToast(message) {
this.showToast(message, 'success');
}

showErrorToast(message) {
this.showToast(message, 'error');
}

showInfoToast(message) {
this.showToast(message, 'info');
}

// 시스템 페이지 관련
showEmptyUI(message, targetElement) {
if (targetElement) {
targetElement.innerHTML = `
<div class="empty-ui">
<div>${message}</div>
</div>
`;
}
}
showErrorUI(message, targetElement) {
if (targetElement) {
targetElement.innerHTML = `
<div class="retry-ui">
<div>${message}</div>
<button class="retry-btn" onclick="window.location.reload()">
Retry
</button>
</div>
`;
} else {
this.showErrorToast(message);
}
}
showFormErrorUI(message, targetElement) {
if (targetElement) {
targetElement.innerHTML = `
<div class="form-error-ui">
<div style="color: red;">${message}</div>
</div>
`;
}
}
}

// 싱글톤 인스턴스 생성 및 export
export const systemUI = new SystemUI();




 

먼저 이런식으로 시스템 유틸을 정리하고 이 함수들을 끌어다 썼다.

 


export async function getAsset(page, searchQuery, stateSort, stateMainCategory, stateSubCategory) {
try {
const token = localStorage.getItem("token");
const { assets: userCollection } = await collectionUtils.getUserCollection(token);

// 전달받은 파라미터를 직접 사용하도록 수정
const currentSort = stateSort || 'popular';
const currentMainCategory = stateMainCategory === '' ? 'all' : stateMainCategory;
const currentSubCategory = stateSubCategory === '' ? 'all' : stateSubCategory;

// 먼저 스켈레톤 UI 표시
const assetListContainer = document.getElementById('asset-list-container');
const skeletonCardsHTML = Array(5).fill().map(() => `
<div class="asset-list-card skeleton">
<div class="skeleton-image"></div>
<div class="asset-list-info">
<div class="skeleton-text"></div>
<div class="skeleton-text"></div>
<div class="asset-list-icon-container">
<div class="skeleton-icon"></div>
<div class="skeleton-icon"></div>
</div>
</div>
</div>
`).join('');

assetListContainer.innerHTML = skeletonCardsHTML;

// 3초 딜레이 추가
await new Promise(resolve => setTimeout(resolve, 300));

// 실제 데이터 로드
const response = await fetch(
`${URI}/api/asset?page=${page}&name=${searchQuery || ''}&sort=${currentSort}&mainCategory=${currentMainCategory}&subCategory=${currentSubCategory}`
);
const data = await response.json();

if (!data || !data.data || data.data.length === 0) {
// 검색 결과가 없을 때
window.handleAssetEmpty(new Error(SYSTEM_MESSAGES.assetEmpty));
return;
}

if (data.data && data.data.length > 0) {
if (searchQuery && (!stateMainCategory || stateMainCategory === 'all')) {
const mainCategoryIds = data.data.map(item => item.mainCategory.id);
const uniqueMainCategories = [...new Set(mainCategoryIds)];

const mainCategoriesData = await getMainCategory();

await categoryUtils.updateCategoryState(
uniqueMainCategories,
mainCategoriesData.mainCategory,
true
);
}

const allCardsHTML = data.data.map((item, i) => {
const isInCollection = token ? collectionUtils.isInCollection(userCollection, item._id) : false;
return `
<div class="asset-list-card" id="asset-list-card-${i}" data-id="${item._id}">
<img class="asset-list-img" id="asset-list-img-${i}" src="${item.mainImage}" alt="Asset Image">
<div class="asset-list-info">
<div class="asset-list" id="asset-list-name-${i}">${item.name}</div>
<div class="asset-list" id="asset-list-creator-${i}">${item.creatorName}</div>
<div class="asset-list-icon-container">
<img class="asset-list-icon" src="/data/icons/collection-${isInCollection ? 'on' : 'off'}.svg" alt="collect" id="asset-list-collect-${i}">
<button class="asset-list-icon" id="asset-list-cart-${i}">cart</button>
</div>
</div>
</div>
`;
}).join('');
 
assetListContainer.innerHTML = allCardsHTML;

// 이벤트 리스너 추가
data.data.forEach((item, i) => {
const card = document.getElementById(`asset-list-img-${i}`);
const collectIcon = document.getElementById(`asset-list-collect-${i}`);
const cartIcon = document.getElementById(`asset-list-cart-${i}`);
 
// 에셋 디테읿 페이지 이동
card.addEventListener('click', () => {
router.navigateTo(`/market/${item._id}`);
});
// 컬렉션 토글
collectIcon.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
await collectionUtils.handleCollectionToggle(item._id, token, userCollection, collectIcon);
});
// 카트 추가
cartIcon.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
try {
const result = await cartUtils.addToCart(token, item._id);
if (result.status === "success") {
systemUI.showSuccessToast(SYSTEM_MESSAGES.cartAdd);
} else {
systemUI.showErrorToast(SYSTEM_MESSAGES.cartDuplicate);
}
} catch (error) {
systemUI.showErrorToast(SYSTEM_MESSAGES.cartDuplicate);
}
});
});

if (data.totalPageNum) {
setupQueryPagination(data.totalPageNum, getAsset);
}
}

return data;

} catch (error) {
window.handleError(new Error(SYSTEM_MESSAGES.asset));
}
}

 

이부분이 현재 에셋 코드인데

// 먼저 스켈레톤 UI 표시
const assetListContainer = document.getElementById('asset-list-container');
const skeletonCardsHTML = Array(5).fill().map(() => `
<div class="asset-list-card skeleton">
<div class="skeleton-image"></div>
<div class="asset-list-info">
<div class="skeleton-text"></div>
<div class="skeleton-text"></div>
<div class="asset-list-icon-container">
<div class="skeleton-icon"></div>
<div class="skeleton-icon"></div>
</div>
</div>
</div>
`).join('');

 

이렇게 스켈레톤 로딩도 추가해보았다.

스켈레톤을 평소에 보면서 따로 어떤 코드적 노하우가 있는게 아닐까 싶었는데 다른게 아니라 본 컴포넌트가 등장하기 전에 이런식으로 똑같이 생긴 가상의 컴포넌트 껍데기가 한번 뜨고 본 컴포넌트가 모두 로드되었을 때 본 컴포넌트가 뜨는 원리였다.

 

아래는 스타일 코드

/* 마켓 카드 스켈레톤 */
.skeleton {
background: #f0f0f0;
position: relative;
overflow: hidden;
}

.skeleton::after {
content: "";
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
transform: translateX(-100%);
background-image: linear-gradient(
90deg,
rgba(255, 255, 255, 0) 0,
rgba(255, 255, 255, 0.2) 20%,
rgba(255, 255, 255, 0.5) 60%,
rgba(255, 255, 255, 0)
);
animation: shimmer 2s infinite;
}

@keyframes shimmer {
100% {
transform: translateX(100%);
}
}

.skeleton-image {
width: 540px;
height: 360px;
background: #e0e0e0;
border-radius: 8px;
}

.skeleton-text {
height: 20px;
margin: 8px 0;
background: #e0e0e0;
border-radius: 4px;
}

.skeleton-icon {
width: 24px;
height: 24px;
background: #e0e0e0;
border-radius: 4px;
margin: 0 4px;
}

 

// 카트 추가
cartIcon.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
try {
const result = await cartUtils.addToCart(token, item._id);
if (result.status === "success") {
systemUI.showSuccessToast(SYSTEM_MESSAGES.cartAdd);
} else {
systemUI.showErrorToast(SYSTEM_MESSAGES.cartDuplicate);
}
} catch (error) {
systemUI.showErrorToast(SYSTEM_MESSAGES.cartDuplicate);
}
});

 

카트 추가부분을 보면 추가 성공시 성공에 대한 토스트를 띄우고 중복되게 카트가 담기면 에러토스트가 뜨도록 설정되었다.

이런식으로 상황에 맞게 시스템 ui를 사용

 

} catch (error) {
window.handleError(new Error(SYSTEM_MESSAGES.asset));
}

 

트라이 캐치 구문을 통해 에러들을 이런식으로 처리하면

window.handleError = (error) => {
const main = document.querySelector('main');
main.innerHTML = '';
systemUI.showErrorUI(error.message, main);
systemUI.hideLoading();
console.error(error);
}

 

자동으로 이런식으로 에러 상황이 UI로 표현되도록 만들었다.

 

일부러 에러상황을 발생시켜 보았을 때 이런식으로 에러상황 UI가 표현이 된다.

 

 

1차적인 개발이 마무리됨에 따라 전체적인 스케줄 계획도 다시 짜 보아야겠다.

(에셋 디자인, ui 디자인, 개발 고도화, 기획, 배포 계획 등)