데이팅 앱 UX의 원죄: 스와이프
2012년 Tinder가 스와이프 UI를 대중화했습니다. 왼쪽으로 밀면 거절, 오른쪽으로 밀면 좋아요. 이 단순한 인터랙션은 모바일 UX의 역사를 바꿨습니다. 슬롯머신처럼 작동하는 가변 보상 구조(variable ratio reinforcement)는 사용자를 앱에 오래 머물게 만들었고, 이후 거의 모든 데이팅 앱이 이 패턴을 복제했습니다.
그런데 썸블링을 설계하면서 우리는 근본적인 질문을 던졌습니다. “스와이프가 진지한 연애를 원하는 사람에게 정말 맞는 UX인가?”
스와이프의 문제는 속도입니다. 0.5초 안에 결정이 이뤄집니다. 이 속도는 외모 이외의 정보를 처리할 인지적 여유를 허락하지 않습니다. OCEAN 점수, 가치관 정렬도, Soul Score — 이 모든 깊이 있는 정보가 빠른 스와이프 앞에서 무용지물이 됩니다.
스와이프 UI vs 카드 기반 매칭 — 비교 분석
두 패러다임을 나란히 놓고 분석했습니다.
| 항목 | 스와이프 (Tinder 방식) | 카드 기반 (썸블링 방식) |
|---|---|---|
| 결정 속도 | 0.5초 이내 | 30초~수분 |
| 판단 기준 | 주로 외모 | 성격·가치관·점수 |
| 하루 노출 수 | 무제한 | 최대 3명 |
| 사용자 피로 | 높음 (Tinder Fatigue) | 낮음 |
| 연결의 질 | 낮음 | 높음 (가설) |
| 게임화 요소 | 매우 강함 | 의도적으로 최소화 |
썸블링의 결론: 하루 최대 3명, 카드 기반, 깊이 우선 설계. 무한한 선택지는 선택의 역설(paradox of choice)을 만들어 오히려 만족도를 낮춥니다. Barry Schwartz의 연구(2004)에서 선택지가 많을수록 결정 후 만족도가 낮아진다는 것이 반복적으로 확인됩니다.
하루에 3명만 보여주는 것은 제약처럼 보이지만, 각 매칭에 더 진지하게 집중하게 만드는 심리적 효과가 있습니다.
카드 UI 설계: 드래그는 살리되, 속도는 줄이다
스와이프의 즉각성은 제거하되, 드래그 인터랙션 자체의 물리적 만족감은 살렸습니다. Framer Motion의 drag prop으로 구현합니다.
// src/components/matching/MatchCard.tsx
import { motion, useMotionValue, useTransform, AnimatePresence } from 'framer-motion';
interface MatchCardProps {
match: DailyMatch;
onAccept: (matchId: number) => void;
onReject: (matchId: number) => void;
onOpenProfile: (match: DailyMatch) => void;
}
export const MatchCard = React.memo(function MatchCard({
match,
onAccept,
onReject,
onOpenProfile,
}: MatchCardProps) {
const x = useMotionValue(0);
// x 위치에 따라 카드 회전 (-15도 ~ +15도)
const rotate = useTransform(x, [-300, 0, 300], [-15, 0, 15]);
// x 위치에 따라 수락/거절 레이블 투명도
const acceptOpacity = useTransform(x, [0, 150], [0, 1]);
const rejectOpacity = useTransform(x, [-150, 0], [1, 0]);
// 카드 배경색 미세 변화 (오른쪽: 보라빛, 왼쪽: 붉은빛)
const background = useTransform(
x,
[-300, 0, 300],
['rgba(239,68,68,0.08)', 'rgba(0,0,0,0)', 'rgba(127,90,240,0.08)']
);
const handleDragEnd = (_: unknown, info: { offset: { x: number } }) => {
const threshold = 120; // 픽셀 임계값
if (info.offset.x > threshold) {
onAccept(match.matchId);
} else if (info.offset.x < -threshold) {
onReject(match.matchId);
}
// 임계값 미달 시 카드가 원위치로 스프링 복귀
};
return (
<motion.div
style={{ x, rotate, background }}
drag="x"
dragConstraints={{ left: 0, right: 0 }} // 복귀 스프링 활성화
dragElastic={0.7} // 탄성 (1이면 완전 자유)
onDragEnd={handleDragEnd}
whileTap={{ scale: 0.98 }}
className="relative w-full max-w-sm rounded-3xl overflow-hidden
bg-surface shadow-2xl cursor-grab active:cursor-grabbing"
>
{/* 수락 레이블 */}
<motion.div
style={{ opacity: acceptOpacity }}
className="absolute top-8 left-6 z-10 rotate-[-20deg]
border-4 border-primary text-primary text-2xl font-bold
px-4 py-2 rounded-lg"
>
LIKE
</motion.div>
{/* 거절 레이블 */}
<motion.div
style={{ opacity: rejectOpacity }}
className="absolute top-8 right-6 z-10 rotate-[20deg]
border-4 border-red-400 text-red-400 text-2xl font-bold
px-4 py-2 rounded-lg"
>
PASS
</motion.div>
{/* 카드 컨텐츠 */}
<CardContent match={match} onOpenProfile={onOpenProfile} />
</motion.div>
);
});
설계 의도: 드래그는 자유롭지만 dragConstraints={{ left: 0, right: 0 }}로 놓으면 스프링처럼 원위치로 돌아옵니다. 이 물리적 피드백이 “내가 직접 결정을 내렸다”는 느낌을 줍니다. 임계값(120px)을 충분히 크게 설정하여 실수로 스와이프되는 것을 방지합니다.
블러 해제 인터랙션 — 기대감을 설계하다
썸블링에서 처음 매칭 카드를 받으면 상대방 프로필 사진이 블러 처리되어 있습니다. 이것은 단순한 기능이 아니라 인지적 순서의 재설계입니다.
외모를 나중에 보여주면 사용자가 Soul Score와 성격 태그를 먼저 처리합니다. “이 사람과 가치관이 맞겠구나”라는 기대가 형성된 뒤에 얼굴을 보는 것과, 얼굴을 먼저 보고 성격을 확인하는 것은 완전히 다른 인지 경험입니다.
블러 해제에는 두 가지 경로가 있습니다.
flowchart TD
A["매칭 카드 수신<br/>(블러 처리된 상태)"] --> B["Soul Score · 성격 태그<br/>가치관 키워드 확인"]
B --> C{프로필 확인 방법}
C -->|"경로 1"| D["쌍방 수락<br/>→ 채팅방 입장"]
C -->|"경로 2"| E["프로필 언블러 아이템<br/>사용 (카드 소모)"]
D --> F["0.8초 페이드 인<br/>느린 언블러 연출"]
E --> G["파티클 효과<br/>+ 즉시 언블러"]
style F fill:#7F5AF0,stroke:#7F5AF0,color:#fff
style G fill:#FF6B9D,stroke:#FF6B9D,color:#fff
경로 1: 매칭 수락 후 자동 해제
두 사람 모두 매칭을 수락하면 채팅방이 열리고, 채팅방 내에서 프로필 이미지가 자연스럽게 언블러됩니다. 이때 페이드 인 애니메이션을 사용합니다.
// 블러 → 클리어 전환 애니메이션
<motion.div
initial={{ filter: 'blur(20px)' }}
animate={{ filter: isUnblurred ? 'blur(0px)' : 'blur(20px)' }}
transition={{ duration: 0.8, ease: 'easeOut' }}
className="w-full h-full"
>
<Image src={profileImageUrl} alt="프로필" fill objectFit="cover" />
</motion.div>
0.8초의 느린 페이드는 의도적입니다. 빠른 전환보다 천천히 드러나는 이미지가 더 강한 감정적 반응을 만듭니다.
경로 2: 프로필 언블러 아이템 사용
수락 전에 상대방 사진을 미리 보고 싶다면 인벤토리의 프로필 언블러 아이템을 사용합니다. 이 인터랙션에는 별도의 확인 단계와 반짝이는 파티클 효과를 추가했습니다.
const handleUnblur = async () => {
if (inventoryCount < 1) {
setShowCardModal(true); // 상점 유도 모달
return;
}
// 언블러 확인 다이얼로그
const confirmed = await showConfirmDialog({
title: '프로필 언블러',
message: '언블러 아이템 1개를 사용하여 사진을 확인합니다.',
confirmText: '확인',
});
if (!confirmed) return;
await unblurProfile(match.matchId);
// 파티클 효과 트리거
triggerParticles({ color: '#7F5AF0', count: 30 });
};
파티클 효과는 작은 디테일이지만, 아이템을 “소비”하는 행위에 감각적 보상을 더합니다. 아무것도 없이 그냥 이미지가 바뀌는 것보다 훨씬 의미 있는 경험이 됩니다.
매칭 성공 오버레이 — 파티클과 감동
두 사람이 서로 수락하면 매칭 성공 오버레이가 나타납니다. 이 순간은 썸블링에서 가장 감정적으로 충만해야 하는 순간입니다.
// src/components/matching/MatchSuccessOverlay.tsx
export function MatchSuccessOverlay({
match,
isVisible,
onClose,
}: MatchSuccessOverlayProps) {
return (
<AnimatePresence>
{isVisible && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
transition={{ duration: 0.4 }}
className="fixed inset-0 z-50 flex flex-col items-center
justify-center bg-black/80 backdrop-blur-sm"
>
{/* 파티클 캔버스 */}
<ParticleCanvas
colors={['#7F5AF0', '#FF6B9D', '#FFFFFE']}
count={80}
duration={3000}
/>
{/* 두 프로필 이미지 */}
<motion.div
initial={{ scale: 0.5, opacity: 0 }}
animate={{ scale: 1, opacity: 1 }}
transition={{ delay: 0.2, type: 'spring', stiffness: 200 }}
className="flex items-center gap-4 mb-8"
>
<ProfileAvatar src={myProfileUrl} size={80} />
<HeartIcon className="text-accent w-10 h-10" />
<ProfileAvatar src={match.profileImageUrl} size={80} />
</motion.div>
{/* 텍스트 */}
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.4 }}
className="text-center mb-8"
>
<p className="text-text text-3xl font-bold mb-2">
Soul이 연결됐어요
</p>
<p className="text-textMuted text-lg">
Soul Score{' '}
<span className="text-primary font-bold">
{match.soulScore.toFixed(0)}점
</span>
</p>
</motion.div>
{/* 채팅 시작 버튼 */}
<motion.div
initial={{ y: 20, opacity: 0 }}
animate={{ y: 0, opacity: 1 }}
transition={{ delay: 0.6 }}
>
<Button onClick={onClose} size="lg" fullWidth>
대화 시작하기
</Button>
</motion.div>
</motion.div>
)}
</AnimatePresence>
);
}
파티클 시스템: canvas API를 사용하는 경량 파티클 엔진을 직접 구현했습니다. Three.js나 외부 파티클 라이브러리는 번들 크기가 너무 크기 때문입니다. 80개 파티클, 3색 조합(보라/핑크/흰색)으로 브랜드 감성을 유지합니다.
소울 테스트 질문 UI: 채팅 버블 스타일
50문항의 소울 테스트를 일반적인 폼 형식으로 구현하면 단조롭고 이탈률이 높습니다. 썸블링은 질문을 채팅 버블로 보여주는 방식을 선택했습니다. 마치 썸블링이라는 AI와 대화하는 것처럼 느껴지게 합니다.
// src/components/soul-test/QuestionBubble.tsx
export function QuestionBubble({ question, isNew }: QuestionBubbleProps) {
return (
<motion.div
initial={{ opacity: 0, y: 16, scale: 0.96 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
transition={{ duration: 0.35, ease: [0.25, 0.46, 0.45, 0.94] }}
className="flex items-start gap-3 max-w-[85%]"
>
{/* 썸블링 아바타 */}
<div className="w-9 h-9 rounded-full bg-primary flex-shrink-0
flex items-center justify-center text-white text-sm font-bold">
A
</div>
{/* 말풍선 */}
<div className="bg-surface rounded-2xl rounded-tl-sm px-4 py-3
shadow-md shadow-black/20">
<p className="text-text text-base leading-relaxed">
{question.text}
</p>
{question.subText && (
<p className="text-textMuted text-sm mt-1">{question.subText}</p>
)}
</div>
</motion.div>
);
}
새 질문이 나타날 때 y: 16 → 0 슬라이드와 scale: 0.96 → 1 팝업을 조합합니다. 베지어 곡선 [0.25, 0.46, 0.45, 0.94]는 iOS 시스템 애니메이션 곡선에서 가져온 것으로, 자연스러운 감속이 특징입니다.
리커트 척도 선택 후 자동 전환
5점 척도를 선택하면 0.4초의 확인 딜레이 후 자동으로 다음 질문으로 전환합니다. 이 딜레이는 사용자가 자신의 선택을 인식할 수 있는 최소한의 시간입니다.
// src/components/soul-test/LikertScale.tsx
export function LikertScale({ onAnswer }: LikertScaleProps) {
const [selected, setSelected] = useState<number | null>(null);
const handleSelect = (value: number) => {
setSelected(value);
// 선택 확인 후 자동 전환
setTimeout(() => {
onAnswer(value);
setSelected(null);
}, 400);
};
return (
<div className="flex gap-2 justify-center mt-4">
{[1, 2, 3, 4, 5].map((value) => (
<motion.button
key={value}
onClick={() => handleSelect(value)}
whileTap={{ scale: 0.90 }}
animate={{
scale: selected === value ? 1.15 : 1,
backgroundColor:
selected === value ? '#7F5AF0' : 'rgba(255,255,255,0.08)',
}}
transition={{ duration: 0.15 }}
className="w-12 h-12 rounded-full text-text font-semibold
border border-white/10 text-sm"
>
{value}
</motion.button>
))}
</div>
);
}
모바일 키보드 대응 — useVisualViewport
채팅방에서 메시지 입력창을 탭하면 소프트 키보드가 올라옵니다. iOS Safari에서는 이때 100vh가 키보드를 포함한 높이가 아닌 전체 화면 높이로 계산되어 입력창이 키보드 아래로 숨어버리는 문제가 있습니다.
// src/hooks/useVisualViewport.ts
export function useVisualViewport(
scrollTargetRef: React.RefObject<HTMLElement | null>
) {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const viewport = window.visualViewport;
if (!viewport) return;
const handleResize = () => {
if (!containerRef.current) return;
// 실제 보이는 뷰포트 높이로 컨테이너 조정
const offsetFromBottom =
window.innerHeight - viewport.height - viewport.offsetTop;
containerRef.current.style.height = `${viewport.height}px`;
containerRef.current.style.transform =
`translateY(-${offsetFromBottom}px)`;
// 최신 메시지로 스크롤
if (scrollTargetRef.current) {
scrollTargetRef.current.scrollTop =
scrollTargetRef.current.scrollHeight;
}
};
viewport.addEventListener('resize', handleResize);
viewport.addEventListener('scroll', handleResize);
return () => {
viewport.removeEventListener('resize', handleResize);
viewport.removeEventListener('scroll', handleResize);
};
}, [scrollTargetRef]);
return containerRef;
}
window.visualViewport API는 키보드가 올라왔을 때의 실제 가시 영역 크기를 알려줍니다. 이 값으로 채팅 컨테이너 높이와 위치를 동적으로 조정하면 iOS에서도 입력창이 항상 키보드 위에 위치합니다.
다크 모드와 감성적 색상 전략
썸블링은 다크 퍼스트(Dark-First) 앱입니다. #0F0E17 배경은 순수 검정(#000000)보다 덜 차갑고, 심야에 화면을 보는 사용자의 눈 피로를 줄입니다. 데이팅 앱은 대부분 저녁~심야에 사용되기 때문입니다.
Primary 색상 #7F5AF0(보라)와 Accent #FF6B9D(핑크)는 낭만과 신뢰를 동시에 전달합니다. 보라는 지혜·신비·로맨스와 연결되고, 핑크는 따뜻함·애정·설렘을 상징합니다.
그라디언트를 사용할 때는 두 색을 조합하여 썸블링만의 감성적인 분위기를 만듭니다.
/* 소울 점수 게이지 그라디언트 */
.soul-score-gradient {
background: linear-gradient(135deg, #7F5AF0 0%, #FF6B9D 100%);
}
/* 매칭 성공 배경 오버레이 */
.match-success-overlay {
background: radial-gradient(
ellipse at center,
rgba(127, 90, 240, 0.3) 0%,
rgba(15, 14, 23, 0.95) 70%
);
}
설계 원칙 요약
썸블링의 모바일 UX 설계를 관통하는 세 가지 원칙입니다.
flowchart LR
subgraph Principle1["속도보다 깊이"]
P1["하루 3명 제한<br/>카드 기반 매칭"]
end
subgraph Principle2["모션은 의미"]
P2["드래그 피드백<br/>LIKE/PASS 레이블<br/>파티클 효과"]
end
subgraph Principle3["감정을 설계"]
P3["블러 → 언블러<br/>매칭 성공 오버레이<br/>채팅 버블 소울테스트"]
end
P1 --> P2 --> P3
1. 속도보다 깊이: 빠른 스와이프 대신 충분히 생각할 공간을 제공합니다. 하루 3명의 제한, 카드를 탭해서 프로필을 상세히 볼 수 있는 구조, 블러 해제의 단계적 경험이 모두 이 원칙에서 나왔습니다.
2. 모션은 의미를 가진다: 장식적 애니메이션은 최소화하고, 사용자의 행동에 즉각적이고 의미 있는 피드백을 주는 모션만 사용합니다. 드래그 임계값을 넘었을 때의 LIKE/PASS 레이블, 언블러 시의 파티클, 매칭 성공 오버레이 모두 “지금 특별한 일이 일어나고 있다”는 신호를 줍니다.
3. 감정을 설계한다: 데이팅 앱은 가장 감정적인 종류의 소프트웨어입니다. UI 컴포넌트 하나하나가 설렘, 기대, 안도, 실망을 만들어냅니다. 매칭 성공 오버레이의 파티클, 채팅 버블 스타일의 소울 테스트, 블러 해제의 느린 페이드 — 이것들은 기능 구현이 아니라 감정 연출입니다.
Tinder는 더 많이 쓸수록 더 많은 돈을 버는 구조입니다. 썸블링은 덜 쓰더라도 더 좋은 만남을 만드는 것을 목표로 합니다. UX 설계의 철학이 비즈니스 모델과 정렬되어야 진정한 사용자 경험이 완성됩니다.
GitHub 계정으로 로그인하여 댓글을 남길 수 있습니다. 허위사실, 욕설, 사칭 등의 댓글은 통보 없이 삭제될 수 있습니다.