🤔당시 고민했던 것들
카페 24 보다 차별성이 있는가?
카페 24를 활용하면 제공해주는 템플릿을 통해 빠른 플랫폼 구축이 가능합니다.
직접 디자인과 개발없이도 결제, 회원가입, 장바구니 등의 핵심 기능이 내장되어 있어 회사는 '비즈니스' 에 몰입할 수 있습니다.
백오피스를 개발하지 않아도 관리자 페이지에서 재고, 주문, CS 관리를 할 수있게 제공해주는데,
가격은 기본적인 PG사 가입 비용 등을 제외하고 호스팅, 쇼핑몰 솔루션에 대한 초기 비용은 10만원이 안됩니다..
그래서 카페24 장점이 이렇게 많은데 왜 직접 개발했나요?
우리 제품은 B2B에 특화되었고 IoT와 로봇 부품이라는 특수한 도메인이었습니다.
부품별로 특화된 실시간 호환성을 체크 해주는게 기능이 필요하고
ex: 아두이노 우노 선택 시 호환 가능한 센서/ 모터만 자동 필터링
프로젝트별로 맞춤 제작 & 부품 리스트를 어필하는 것이 포인트였습니다.
ex: "스마트 화분" 선택 시
필수 부품 : 아두이노, LED, 온습도 센서, 토양습도 센서, 펌프 등
특히 타깃 고객은 연구진이나 개발진인데 이들에게는 기술적인 지원이 필요했습니다
제품, 부품 구매 후 코드 제공 서비스라던지
-> 구매한 프로젝트에 맞는 아두이노/ 라즈베리파이 코드 제공
-> GitHub 연동으로 지속적 업데이트
-> 버그 리포트 시 실제 엔지니어 지원
실시간 기술 지원이라던지
-> 1:1 문의 게시판 및 실시간 채팅으로 지원
따라서 차별화된 기능과 워크플로우가 필요했기 때문에 직접 개발을 선택하게되었습니다.
UI/UX 설계
1. 주문 취소/환불 프로세스 설계
주문 취소와 환불은 매우 복잡한 비즈니스 로직이었습니다. 결제 방식별로 다른 처리 과정, 부분 취소, 환불 지원 그리고 법적 요구사항까지 고려해야 해서 많은 고민이 필요했습니다. 고객 신뢰와 직결되는 중요한 부분으로 처음에는 단순하게 생각 했지만,
구현 과정에서 복잡성을 깨달았습니다.
1) 용어 정리
주문 취소: 아직 배송이 시작되지 않은 상태에서 주문을 철회
주문 환불: 이미 배송된 상품을 반품하고 돈을 돌려받음
주문 취소는 배송도 결제도 일어나지 않아 재고만 복구해주면 되었지만,
환불은 실물 반품부터 받은 상품 상태를 확인해야하고 배송비 등의 문제도 있었습니다.
2) 주문 상태별 취소/ 환불 정책
| 주문 상태 |
고객 취소 가능 |
관리자 취소 가능 |
환불 가능 |
비고 |
| 주문접수 |
✅ 즉시 |
✅ 즉시 |
❌ |
- |
| 결제완료 |
✅ 자동 |
✅ 수동 |
❌ |
결제 취소 API 호출 필요 |
| 배송중 |
❌ |
❌ |
✅ 배송 완료 후 |
택배사 연동 필요 |
| 배송완료 |
❌ |
❌ |
✅ 7일 이내 |
전자상거래법 준수 |
3) 프론트엔드로서 고민한 부분
주문 상태에 따라 UI 와 버튼 노출이 달라져야합니다.
취소 가능 여부, 환불 가능 여부, 관리자 승인 필요 여부를 고려해 조건부 렌더링이 필요한 부분이었습니다.
가능한 진행 상태를 명확히 보여주려고 했습니다. 사용자가 쉽게 실수하지 않도록 Confirm modal을 적극 활용하고
취소 요청 -> "승인 대기 중", 환불 요청 -> "환불 처리 중" OR "완료" 등 단계별로 텍스트 상태를 구분해주었습니다.
에러 및 예외 처리도 가능한 명확하게 안내하려고 했습니다.
export type ApiError = { status: number; code?: string; message?: string; };
const ERROR_MSG: Record<string, string> = {
PAYMENT_CANCEL_FAILED: "결제 취소에 실패했어요. 고객센터로 문의해주세요.",
ALREADY_SHIPPED: "이미 배송이 시작되어 취소가 불가해요. 반품/환불을 진행해주세요.",
PARTIAL_ONLY: "일부 품목만 환불 가능합니다.",
TIMEOUT: "네트워크 지연으로 실패했어요. 잠시 후 다시 시도해주세요.",
UNKNOWN: "요청 처리 중 문제가 발생했어요. 잠시 후 다시 시도해주세요.",
};
export function toUserMessage(err: unknown): string {
const e = err as ApiError;
// 1. HTTP Status 기반 처리
if (e.status && STATUS_MSG[e.status]) {
return STATUS_MSG[e.status];
}
// 2. 비즈니스 에러 코드
if (e.code && ERROR_MSG[e.code]) {
return ERROR_MSG[e.code];
}
// 3. 서버 기본 메시지 or UNKNOWN
return e.message || ERROR_MSG.UNKNOWN;
}
개발 시 고려했던 점
1) 좋아요 상태관리
Tanstack Query 무한스크롤로 가져온 상품을 좋아요하는 기능이 있습니다. 좋아요는 서버 상태를 변경하는 이벤트니 좋아요 or 좋아요 취소 시 쿼리 키를 무효화하거나 queryQlient.setQueryData 를 통해 캐시 데이터를 업데이트 해주어야 합니다.
그런데 무한스크롤은 이미 많은 일을 하고 있습니다.
queryKey는 제품의 필터링, 검색 , 정렬등의 기능을 함께 가지고 있어 무한스크롤의 데이터를 건드리는데는 사이드 이팩트가 발생할 확률이 높습니다.
좋아요의 상태를 즉시 UI에 반영하되 사이드 이팩트는 줄이기 위해 상태를 전역으로 관리해 책임을 분리했습니다.
// useProductStore.ts
likedProducts: new Set<string>(),
// Actions
toggleLike: (productId: string) => {
set((state) => {
const newLikedProducts = new Set(state.likedProducts);
if (newLikedProducts.has(productId)) {
newLikedProducts.delete(productId);
} else {
newLikedProducts.add(productId);
}
return { likedProducts: newLikedProducts };
});
},
좋아요의 서버 상태와 전역 상태 동기화를 위해 상위 컴포넌트에서 setter 함수로 좋아요 한 상품의 id를 연동시켰습니다.
// useProductStore.ts
setterLikedProduct: (productIds: string[]) => {
set({ likedProducts: new Set(productIds) });
},
// App.tsx
const { data: likedProducts, isSuccess } = useLikedProducts();
const setterLikedProduct = useProductStore(
(state) => state.setterLikedProduct
);
const isSynced = useRef(false);
useEffect(() => {
if (isSuccess && likedProducts && !isSynced.current) {
const likedProductIds = likedProducts.map((product) => product.id);
setterLikedProduct(likedProductIds);
isSynced.current = true; // 한 번만 동기화
}
}, [isSuccess, likedProducts, setterLikedProduct]);
이제 무한스크롤과 독립적으로 좋아요를 관리 할 수 있게되었고 productCard 컴포넌트에서 각각 좋아요 상태를 체크해주면 됩니다. 무한스크롤로 가져온 data는 좋아요 상태와 별도로 움직이기 때문에 productCard 에서 체크해주지 않으면 좋아요 상태가 누락하게됩니다.
// productCard.tsx
const isLiked = useProductStore((state) => state.isLiked);
const liked = isLiked(product.id);
// useProductStore.ts
isLiked: (productId: string) => {
return get().likedProducts.has(productId);
},
2) 조건부 관리자 패널
커스텀훅을 활용해 권한에 따라 UI를 조건부 렌더링했습니다.
export const useAuth = () => {
const { user } = useAuthStore();
const isAdmin = user?.role === 'admin';
const isUser = user?.role === 'user';
const hasPermission = (requiredRole) => {
if (requiredRole === 'admin') return isAdmin;
return isAdmin || isUser; // admin은 모든 권한 포함
};
return { user, isAdmin, isUser, hasPermission };
};
// components/PermissionGate.jsx
import { useAuth } from '../hooks/useAuth';
const PermissionGate = ({
requiredRole,
children,
fallback = null
}) => {
const { hasPermission } = useAuth();
return hasPermission(requiredRole) ? children : fallback;
};
HOC (Higher-Order Component) 방식으로 관리자 패널을 관리해주었습니다.
// 사용 예시
<PermissionGate requiredRole="admin">
<AdminPanel />
</PermissionGate>
프로젝트를 시작할 때 주변에서 이런 질문을 했습니다. "카페 24쓰면 되는데 왜 굳이 직접 개발해?"
처음엔 저조차도 드는 질문이었습니다. 하지만 카페24는 훌륭한 솔루션이었지만, 우리 고객에게 필요한 기능이 없었습니다.
이커머스 도메인을 개발한 시간동안 정말 많은 걸 배웠고 그 때문에 추가로 유사한 사이드 프로젝트를 하면서 부족했던 부분들을 채워나간 좋은 경험이었습니다. (UI와 애니메이션, 결제 등)
이 프로젝트의 기술적 선택과 고민의 중심에는 항상 사용자가 있었습니다.
어려운 워크플로우를 클릭 몇번으로 사용자가 원하는 목표를 달성하도록 하는 고민이 중요하다는걸 깨달았습니다.
여러분은 어떤 기준으로 UI를 설계하시나요? 댓글로 경험을 공유해 주세요! 🙌
ps.나중에 참고하려고 내용 정리
const routes = [
{ to: '/login', label: '로그인', auth: false },
{ to: '/product', label: '제품', auth: false },
{ to: '/product/:id', label: '상세페이지', auth: false },
{ to: '/user/cart', label: '카트', auth: true },
{ to: '/history', label: '주문목록', auth: true },
]
// paymentModel
{
-- 토스페이먼츠 필수 식별자
payment_key // 토스페이면츠 결제 고유키
order_id // 상점 주문번호
customer_key // 구매자 고유 식별자
-- 주문 정보
order_name // 상품명
amount // 결제 금액
currency // 통화 (ex: KRW)
-- 구매자 정보
customer_name
customer_email
customer_phone
status // 결제 상태 ( READY, PROGRESS, DONE, CANCELED(환불) )
}
// userModel
{
name,
password,
email,
role: number (관리자, 일반 사용자)
cart: { (product)id, quantatiy, date }[],
history: { (product)name, quantatiy, price, dateOfPurchase, paymentId(*중요) }
}
// productModel
{
write: (user)id,
title,
description,
price,
images: string[],
sold: number, (얼마나 팔렸는지)
views: number,
createdAt,
updatedAt
}