이 글은 누구를 위한 것인가
- 선물하기 기능을 처음 도입하거나 개선하려는 이커머스 백엔드 개발자
- 선물 주문 UX 플로우를 기획하는 PM과 프로덕트 디자이너
- 카카오선물하기·네이버선물하기 같은 선물 전용 플랫폼의 구조가 궁금한 개발자
들어가며
선물하기 기능은 겉보기에 단순해 보인다. "다른 사람에게 배송하면 되는 것 아닌가?" 하지만 실제 구현은 일반 주문보다 훨씬 복잡하다. 수신자가 배송지를 직접 입력해야 하고, 수락/거절 플로우가 있고, 결제 시점과 배송 확정 시점이 분리된다. 이 과정에서 재고 확보, 상태 머신 설계, 만료 처리 등 다양한 문제가 얽힌다.
카카오선물하기가 1조 원 규모로 성장한 배경에는 이 복잡한 선물 주문 경험을 매끄럽게 만든 UX 설계가 있다. 네이버쇼핑, 쿠팡, 무신사도 선물하기 탭을 별도로 운영하며 선물 특화 경험을 제공하고 있다.
이 글에서는 선물하기 기능의 UX 플로우 설계부터 백엔드 상태 머신, API 구조까지 실무에서 바로 적용할 수 있는 수준으로 다룬다.
이 글은 bluefoxdev.kr의 이커머스 주문 설계 패턴 을 참고하고, 선물 주문 특화 관점에서 확장하여 작성했습니다.
1. 선물하기 UX의 핵심 플로우
1.1 발신자 플로우 (구매자)
일반 주문과 선물하기의 UX 분기는 상품 상세 또는 장바구니 단계에서 시작된다.
[상품 상세]
├── 구매하기 → 일반 주문 플로우
└── 선물하기 → 선물 주문 플로우
├── 수신자 연락처 입력 (전화번호 또는 카카오/네이버 ID)
├── 선물 메시지 작성 (선택)
├── 선물 포장 옵션 선택 (선택)
├── 결제 진행 (발신자가 결제)
└── 선물 링크/알림 발송
발신자가 배송지를 모르는 경우가 대부분이다. 따라서 결제 시점에 배송지를 확정하지 않는 것이 선물하기의 핵심 설계 원칙이다.
1.2 수신자 플로우
수신자는 선물 알림(카카오톡, SMS, 앱 푸시)을 받고 링크를 통해 진입한다.
[선물 알림 수신]
└── 선물 확인 페이지 진입
├── 선물 수락
│ ├── 배송지 입력
│ ├── 추가 옵션 선택 (색상/사이즈가 미결정인 경우)
│ └── 수락 완료 → 배송 시작
└── 선물 거절
└── 발신자에게 환불 처리
1.3 수락 기한과 만료 처리
수신자가 수락하지 않으면 재고가 묶이는 문제가 발생한다. 일반적으로 7~30일의 수락 기한을 설정하고, 만료 시 자동 환불 처리한다.
| 이벤트 | 처리 방식 |
|---|---|
| 선물 발송 | 재고 임시 확보 (soft reserve) |
| 수락 완료 | 재고 확정, 배송 시작 |
| 거절 | 재고 해제, 즉시 환불 |
| 기한 만료 | 재고 해제, 자동 환불 |
2. 주문 상태 머신 설계
2.1 선물 주문의 상태 정의
일반 주문(PENDING → PAID → SHIPPING → DELIVERED)과 달리, 선물 주문은 중간에 수신자의 수락 단계가 끼어든다.
GIFT_CREATED ← 발신자 결제 완료, 선물 링크 발송
↓
GIFT_SENT ← 수신자에게 알림 발송 완료
↓
GIFT_ACCEPTED ← 수신자 수락, 배송지 입력 완료
↓
ORDER_CONFIRMED ← 일반 주문 상태 머신으로 합류
↓
SHIPPING → DELIVERED
(분기)
GIFT_SENT → GIFT_REJECTED → REFUND_COMPLETED
GIFT_SENT → GIFT_EXPIRED → REFUND_COMPLETED
2.2 상태 전이 구현 (TypeScript)
type GiftOrderStatus =
| 'GIFT_CREATED'
| 'GIFT_SENT'
| 'GIFT_ACCEPTED'
| 'GIFT_REJECTED'
| 'GIFT_EXPIRED'
| 'ORDER_CONFIRMED'
| 'REFUND_COMPLETED';
const VALID_TRANSITIONS: Record<GiftOrderStatus, GiftOrderStatus[]> = {
GIFT_CREATED: ['GIFT_SENT'],
GIFT_SENT: ['GIFT_ACCEPTED', 'GIFT_REJECTED', 'GIFT_EXPIRED'],
GIFT_ACCEPTED: ['ORDER_CONFIRMED'],
GIFT_REJECTED: ['REFUND_COMPLETED'],
GIFT_EXPIRED: ['REFUND_COMPLETED'],
ORDER_CONFIRMED: [],
REFUND_COMPLETED: [],
};
function transitionGiftOrder(
current: GiftOrderStatus,
next: GiftOrderStatus
): boolean {
const allowed = VALID_TRANSITIONS[current];
if (!allowed.includes(next)) {
throw new Error(`Invalid transition: ${current} → ${next}`);
}
return true;
}
3. 배송지 처리 전략
3.1 배송지 입력 타이밍
선물 주문에서 배송지 입력은 수신자 수락 시점으로 미뤄야 한다. 발신자가 수신자의 주소를 아는 경우에만 예외적으로 발신자가 입력하도록 옵션을 제공할 수 있다.
interface GiftOrder {
id: string;
senderId: string;
receiverPhone: string;
giftToken: string; // 수신자 접근 토큰
expiresAt: Date;
items: OrderItem[];
shippingAddress?: Address; // 수락 전까지 null
status: GiftOrderStatus;
message?: string;
wrappingOption?: WrappingType;
}
3.2 수신자 인증 처리
선물 링크는 고유 토큰 기반으로 생성한다. 토큰은 충분한 엔트로피를 가져야 하고, HTTPS를 통해서만 접근 가능해야 한다.
import { randomBytes } from 'crypto';
function generateGiftToken(): string {
return randomBytes(32).toString('hex'); // 64자 16진수
}
// 링크: https://shop.example.com/gift/{giftToken}
// 토큰으로 수신자 검증 후 배송지 입력 폼 제공
4. 재고 관리와 결제 처리
4.1 Soft Reserve vs Hard Reserve
선물 발송 시점에 재고를 어떻게 확보할지가 핵심이다.
| 방식 | Soft Reserve | Hard Reserve |
|---|---|---|
| 재고 차감 시점 | 수락 완료 시 | 선물 발송 시 |
| 재고 노출 | 수락 전에도 구매 가능 표시 | 구매 불가 표시 |
| 장점 | 재고 효율성 높음 | 재고 보장 확실 |
| 단점 | 수락 시 품절 가능 | 재고 묶임 |
| 적합한 경우 | 재고 충분한 상품 | 한정판·인기 상품 |
대부분의 상품은 Soft Reserve를 쓰되, 수락 시 품절이면 대체 옵션을 제공한다.
4.2 환불 처리 자동화
거절/만료 시 환불은 원래 결제 수단으로 즉시 처리해야 한다.
async function processGiftExpiry(giftOrderId: string): Promise<void> {
const giftOrder = await GiftOrderRepository.findById(giftOrderId);
if (giftOrder.status !== 'GIFT_SENT') return;
if (new Date() < giftOrder.expiresAt) return;
await db.transaction(async (trx) => {
// 재고 해제
await InventoryService.releaseReservation(giftOrder.items, trx);
// 환불 처리
await PaymentService.refund({
paymentId: giftOrder.paymentId,
reason: 'GIFT_EXPIRED',
});
// 상태 업데이트
await GiftOrderRepository.updateStatus(
giftOrderId,
'GIFT_EXPIRED',
trx
);
// 발신자 알림
await NotificationService.notifySender(giftOrder.senderId, {
type: 'GIFT_EXPIRED',
giftOrderId,
});
});
}
5. 선물 메시지와 포장 옵션 설계
5.1 메시지 카드 시스템
선물 메시지는 단순 텍스트 이상의 경험을 제공할 수 있다.
- 텍스트 메시지: 기본 제공 (최대 200자)
- 카드 템플릿: 생일/기념일/감사 등 이벤트별 템플릿
- 음성/영상 메시지: 고급 옵션 (추가 비용 가능)
interface GiftMessage {
type: 'TEXT' | 'TEMPLATE' | 'VOICE';
content: string;
templateId?: string; // TEMPLATE 타입 시
mediaUrl?: string; // VOICE 타입 시
senderName: string; // 발신자 표시명
}
5.2 포장 옵션 상품화
포장 옵션은 별도 SKU로 관리하거나 주문에 부가 서비스로 추가한다. 리본 포장, 선물 박스, 에코백 등을 옵션으로 제공하면 객단가를 높일 수 있다.
마무리: 핵심 정리
선물하기 기능 설계의 핵심은 세 가지다.
- 배송지 결정 분리: 발신자 결제 → 수신자 수락 → 배송지 입력의 단계 분리
- 상태 머신 명확화: GIFT_ 접두사 상태와 일반 ORDER 상태의 명확한 구분
- 만료 자동화: 스케줄러로 만료 처리와 자동 환불을 보장
선물하기는 단순한 배송지 변경 기능이 아니라 완전히 다른 주문 플로우다. 처음 설계 시 이 원칙을 명확히 하면 나중에 생기는 복잡성을 크게 줄일 수 있다.