이 글은 누구를 위한 것인가
- 모바일 앱 제스처 UX를 처음 설계하는 디자이너
- 햅틱 피드백을 어떤 상황에 써야 하는지 모르는 팀
- 스와이프 삭제·롱프레스 메뉴 구현 방법을 알고 싶은 개발자
들어가며
좋은 모바일 UX는 눈으로만 피드백을 주지 않는다. 버튼을 눌렀을 때의 미세한 진동, 스와이프가 한계에 닿을 때의 탄성감 — 이 물리적 피드백이 앱을 "살아있게" 만든다.
이 글은 bluefoxdev.kr의 모바일 인터랙션 설계 를 참고하여 작성했습니다.
1. 제스처 패턴과 용도
[표준 제스처 패턴]
탭 (Tap):
용도: 버튼, 링크, 선택
주의: 최소 44×44pt 터치 영역 확보
더블 탭 (Double Tap):
용도: 좋아요, 확대/축소 토글
주의: 탭과 더블탭이 충돌하지 않게
롱프레스 (Long Press):
용도: 컨텍스트 메뉴, 아이템 선택 모드
기준: iOS 0.5초, Android 0.4초
주의: 롱프레스 가능하다는 힌트 필요
스와이프 (Swipe):
수평: 카드 넘기기, 리스트 항목 액션
수직: 스크롤, 시트 닫기
주의: 시스템 제스처와 충돌 피하기
핀치 (Pinch):
용도: 확대/축소 (이미지, 지도)
주의: 스크롤과 동시에 동작해야 함
드래그 (Drag):
용도: 순서 변경, 슬라이더
주의: 드래그 핸들 시각적으로 명확하게
[제스처 발견가능성 (Discoverability)]
제스처는 발견이 어려움 → 힌트 필요
- 리스트 아이템: 스와이프 힌트 애니메이션
- 롱프레스: 첫 방문 시 코치마크
- 상단 시트: 핸들 바(drag indicator) 표시
2. 햅틱 피드백 구현
// iOS 햅틱 피드백 사용 가이드
import UIKit
class HapticManager {
// 가벼운 탭 피드백 (버튼 클릭, 토글)
static func light() {
let generator = UIImpactFeedbackGenerator(style: .light)
generator.prepare()
generator.impactOccurred()
}
// 중간 강도 (카드 스와이프 임계점)
static func medium() {
let generator = UIImpactFeedbackGenerator(style: .medium)
generator.prepare()
generator.impactOccurred()
}
// 강한 피드백 (삭제 확인, 중요 액션)
static func heavy() {
let generator = UIImpactFeedbackGenerator(style: .heavy)
generator.prepare()
generator.impactOccurred()
}
// 성공 피드백 (결제 완료, 주문 확인)
static func success() {
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.success)
}
// 오류 피드백 (결제 실패, 입력 오류)
static func error() {
let generator = UINotificationFeedbackGenerator()
generator.prepare()
generator.notificationOccurred(.error)
}
// 선택 피드백 (피커 스크롤)
static func selection() {
let generator = UISelectionFeedbackGenerator()
generator.prepare()
generator.selectionChanged()
}
}
// 사용 예시
class AddToCartButton: UIButton {
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
super.touchesEnded(touches, with: event)
HapticManager.medium() // 장바구니 담기 시 중간 강도
}
}
3. 스와이프 삭제 패턴
// SwiftUI 리스트 스와이프 액션
struct OrderListView: View {
@State private var orders: [Order] = []
var body: some View {
List {
ForEach(orders) { order in
OrderRowView(order: order)
.swipeActions(edge: .trailing, allowsFullSwipe: false) {
// 삭제 액션
Button(role: .destructive) {
HapticManager.medium()
withAnimation {
deleteOrder(order)
}
} label: {
Label("취소", systemImage: "trash")
}
// 보조 액션
Button {
HapticManager.light()
archiveOrder(order)
} label: {
Label("보관", systemImage: "archivebox")
}
.tint(.orange)
}
}
}
}
}
마무리
햅틱 피드백의 원칙은 "의미 있는 순간에만 사용"이다. 모든 버튼에 햅틱을 넣으면 무감각해진다. 결제 완료·오류·중요한 상태 변경에만 써야 의미가 있다. iOS에서는 UIFeedbackGenerator.prepare()를 미리 호출해 지연을 없애는 것이 중요하다.