터치와 제스처 처리 — Pressable, TouchableOpacity 비교
모바일 앱에서 터치는 마우스 클릭과 다릅니다. 탭, 롱프레스, 더블탭 등 다양한 제스처를 구분해서 처리해야 합니다.
웹에서는 onClick 하나로 충분하지만, 모바일에서는 터치 피드백 이 사용자 경험에 큰 영향을 미칩니다. React Native는 이를 위해 여러 터치 컴포넌트를 제공합니다.
터치 컴포넌트 비교
| 컴포넌트 | 피드백 | 권장 여부 | 특징 |
|---|---|---|---|
Pressable | 커스텀 | 권장 | 가장 유연, 최신 API |
TouchableOpacity | 투명도 변화 | 사용 가능 | 가장 널리 사용됨 |
TouchableHighlight | 배경색 변화 | 레거시 | 리스트 아이템에 적합 |
TouchableWithoutFeedback | 없음 | 비권장 | 피드백 없는 터치 |
Button | 플랫폼 기본 | 제한적 | 커스터마이징 불가 |
Pressable (권장)
React Native 0.63에서 도입된 최신 터치 API입니다. 터치 상태에 따라 세밀한 제어가 가능합니다.
import { Pressable, Text, StyleSheet } from 'react-native';
function PressableButton() {
return (
<Pressable
onPress={() => console.log('탭!')}
onLongPress={() => console.log('롱프레스!')}
onPressIn={() => console.log('손가락 닿음')}
onPressOut={() => console.log('손가락 뗌')}
delayLongPress={500} // 롱프레스 인식 시간 (ms)
// 터치 상태에 따른 동적 스타일
style={({ pressed }) => [
styles.button,
pressed && styles.pressed,
]}
>
{({ pressed }) => (
<Text style={[styles.text, pressed && styles.pressedText]}>
{pressed ? '누르는 중...' : '눌러보세요'}
</Text>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#007AFF',
paddingVertical: 12,
paddingHorizontal: 24,
borderRadius: 8,
},
pressed: {
backgroundColor: '#0056CC',
transform: [{ scale: 0.96 }],
},
text: {
color: 'white',
fontSize: 16,
fontWeight: 'bold',
textAlign: 'center',
},
pressedText: {
opacity: 0.8,
},
});
hitSlop — 터치 영역 확장
작은 버튼의 터치 영역을 시각적 크기보다 크게 만들 수 있습니다.
// 시각적으로는 작지만 터치 영역은 넓음
<Pressable
onPress={handlePress}
hitSlop={20} // 상하좌우 20dp 확장
// 또는 개별 지정
// hitSlop={{ top: 10, bottom: 10, left: 20, right: 20 }}
>
<Text style={{ fontSize: 12 }}>작은 버튼</Text>
</Pressable>
Apple Human Interface Guidelines에서는 터치 타겟을 최소 44x44pt로 권장합니다. hitSlop을 활용하면 디자인을 해치지 않으면서 터치 영역을 확보할 수 있습니다.
android_ripple — Android 리플 효과
<Pressable
onPress={handlePress}
android_ripple={{
color: 'rgba(0, 0, 0, 0.1)',
borderless: false,
}}
style={styles.button}
>
<Text>Android 리플 버튼</Text>
</Pressable>
TouchableOpacity
가장 많이 사용되는 터치 컴포넌트입니다. 누르면 투명도가 변하는 시각적 피드백을 제공합니다.
import { TouchableOpacity, Text, StyleSheet } from 'react-native';
function OpacityButton() {
return (
<TouchableOpacity
onPress={() => console.log('탭!')}
activeOpacity={0.7} // 누를 때 투명도 (기본: 0.2)
disabled={false}
style={styles.button}
>
<Text style={styles.text}>투명도 버튼</Text>
</TouchableOpacity>
);
}
const styles = StyleSheet.create({
button: {
backgroundColor: '#34C759',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
text: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
});
실전 버튼 컴포넌트 만들기
import { Pressable, Text, ActivityIndicator, StyleSheet } from 'react-native';
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary' | 'outline';
loading?: boolean;
disabled?: boolean;
}
// 재사용 가능한 버튼 컴포넌트
function CustomButton({
title,
onPress,
variant = 'primary',
loading = false,
disabled = false,
}: ButtonProps) {
const isDisabled = disabled || loading;
return (
<Pressable
onPress={onPress}
disabled={isDisabled}
style={({ pressed }) => [
styles.base,
styles[variant],
pressed && styles.pressed,
isDisabled && styles.disabled,
]}
>
{loading ? (
<ActivityIndicator color="white" />
) : (
<Text style={[
styles.text,
variant === 'outline' && styles.outlineText,
]}>
{title}
</Text>
)}
</Pressable>
);
}
const styles = StyleSheet.create({
base: {
paddingVertical: 14,
paddingHorizontal: 24,
borderRadius: 8,
alignItems: 'center',
justifyContent: 'center',
minHeight: 48,
},
primary: {
backgroundColor: '#007AFF',
},
secondary: {
backgroundColor: '#5856D6',
},
outline: {
backgroundColor: 'transparent',
borderWidth: 1.5,
borderColor: '#007AFF',
},
pressed: {
opacity: 0.8,
transform: [{ scale: 0.98 }],
},
disabled: {
opacity: 0.5,
},
text: {
color: 'white',
fontSize: 16,
fontWeight: '600',
},
outlineText: {
color: '#007AFF',
},
});
터치 이벤트 흐름
onPressIn → onPressOut → onPress
│
└── (500ms 이상 유지) → onLongPress → onPressOut
function TouchFlow() {
return (
<Pressable
onPressIn={() => {
// 손가락이 화면에 닿는 순간
// 시각적 피드백 시작 (크기 줄이기, 색상 변경 등)
}}
onPressOut={() => {
// 손가락을 떼는 순간
// 시각적 피드백 해제
}}
onPress={() => {
// 정상적인 탭 완료
// 실제 액션 실행
}}
onLongPress={() => {
// 길게 누르기 완료
// 컨텍스트 메뉴, 삭제 확인 등
}}
>
<Text>터치 이벤트 테스트</Text>
</Pressable>
);
}
더블탭 구현
React Native에는 기본 더블탭 이벤트가 없어서 직접 구현해야 합니다.
import { useRef, useCallback } from 'react';
import { Pressable, Text } from 'react-native';
function DoubleTap() {
const lastTap = useRef<number>(0);
const handlePress = useCallback(() => {
const now = Date.now();
const DOUBLE_TAP_DELAY = 300;
if (now - lastTap.current < DOUBLE_TAP_DELAY) {
// 더블탭 감지
console.log('더블탭!');
} else {
// 싱글탭
console.log('싱글탭');
}
lastTap.current = now;
}, []);
return (
<Pressable onPress={handlePress}>
<Text>더블탭 테스트</Text>
</Pressable>
);
}
스크롤 영역 안의 터치 처리
import { ScrollView, Pressable, Text, View } from 'react-native';
function ScrollWithTouchable() {
return (
<ScrollView>
{/* 스크롤 안에서도 터치 이벤트 동작 */}
{Array.from({ length: 20 }, (_, i) => (
<Pressable
key={i}
onPress={() => console.log(`아이템 ${i} 탭`)}
style={({ pressed }) => ({
padding: 16,
backgroundColor: pressed ? '#f0f0f0' : 'white',
borderBottomWidth: 1,
borderBottomColor: '#eee',
})}
>
<Text>아이템 {i}</Text>
</Pressable>
))}
</ScrollView>
);
}
정리
Pressable을 기본으로 사용 하세요 — 가장 유연하고 최신 API입니다TouchableOpacity도 여전히 많이 사용되며, 간단한 투명도 피드백에 적합합니다hitSlop으로 작은 터치 타겟의 영역을 넓혀 UX를 개선하세요- 터치 피드백은 사용자 경험에 직결되므로, 눌림 상태의 시각적 변화를 꼭 넣으세요
- Android에서는
android_ripple로 플랫폼 네이티브 느낌을 줄 수 있습니다
댓글 로딩 중...