React Native FlatList: 대용량 데이터 렌더링 최적화와 가상화 전략
FlatList의 가상화 메커니즘과 ScrollView 대비 성능 이점 분석
React Native에서 복잡한 터치 상호작용을 처리할 때, 여러 컴포넌트가 동시에 같은 터치 이벤트에 반응하려고 할 수 있습니다. 스크롤뷰 내부의 버튼을 탭할 때, 또는 드래그 가능한 요소 위에서 스와이프 제스처를 수행할 때, 앱은 어떤 컴포넌트가 해당 터치에 응답할 권한을 가질지 결정해야 합니다.
Gesture Responder System은 이러한 복잡한 터치 협상을 관리하는 React Native의 핵심 메커니즘입니다. 부모-자식 컴포넌트 간의 명시적인 참조 없이도, 표준화된 생명 주기 메서드를 통해 터치 이벤트를 효율적으로 처리합니다.
사용자의 단일 터치 동작도 여러 의도로 해석될 수 있습니다:
시나리오 1: 스크롤뷰 내부의 버튼
<ScrollView>
<Button onPress={handlePress} />
</ScrollView>시나리오 2: 드래그 가능한 카드
<DraggableCard>
<Swipeable>
<Button />
</Swipeable>
</DraggableCard>Gesture Responder System은 다음을 제공합니다:
모든 터치 인터랙션은 다음 원칙을 따라야 합니다:
사용자가 터치한 요소가 반응하고 있음을 명확히 표시해야 합니다.
<View
onResponderGrant={() => {
// 터치 시작 시 하이라이트 표시
setHighlighted(true);
}}
onResponderRelease={() => {
// 터치 종료 시 하이라이트 제거
setHighlighted(false);
}}
style={[styles.button, highlighted && styles.highlighted]}
>
<Text>버튼</Text>
</View>사용자가 터치 도중 손가락을 드래그하여 액션을 취소할 수 있어야 합니다.
<View
onResponderMove={(evt) => {
const { locationX, locationY } = evt.nativeEvent;
// 터치가 영역을 벗어나면 취소
if (locationX < 0 || locationY < 0) {
setHighlighted(false);
}
}}
>
<Text>드래그로 취소 가능한 버튼</Text>
</View>이점: 사용자가 실수 없이 자유롭게 인터랙션을 실험할 수 있어 신뢰감 향상
Gesture Responder System은 강력하지만 복잡합니다. 단순한 탭 인터랙션에는 Touchable 컴포넌트를 사용하는 것이 권장됩니다.
import {
TouchableOpacity, // 터치 시 투명도 변경
TouchableHighlight, // 터치 시 배경색 변경
TouchableWithoutFeedback, // 시각적 피드백 없음
Pressable, // 현대적인 통합 API (권장)
} from 'react-native';
// 현대적 방식: Pressable 사용
<Pressable
onPress={handlePress}
style={({ pressed }) => [
styles.button,
pressed && styles.pressed
]}
>
<Text>Press Me</Text>
</Pressable>웹의 <button>이나 <a> 태그와 동일한 역할
View가 터치 이벤트에 응답할지 여부를 결정합니다.
<View
onStartShouldSetResponder={(evt) => {
// 터치 시작 시 응답자가 될까요?
return true;
}}
onMoveShouldSetResponder={(evt) => {
// 터치가 이동할 때 응답자를 요청할까요?
return true;
}}
>
<Text>터치 가능한 영역</Text>
</View>호출 시점:
onStartShouldSetResponder: 터치 시작 시 (touch down)onMoveShouldSetResponder: 터치가 이동할 때 (응답자가 아닌 경우)요청 결과에 따라 승인 또는 거부 핸들러가 호출됩니다.
<View
onResponderGrant={(evt) => {
// [권장] 응답자가 되었습니다!
// 시각적 피드백 표시
console.log('응답자 승인');
}}
onResponderReject={(evt) => {
// [주의] 다른 컴포넌트가 응답자입니다
console.log('응답자 거부');
}}
>
<Text>버튼</Text>
</View>응답자가 된 후 터치 이동과 종료를 추적합니다.
<View
onResponderMove={(evt) => {
// 터치가 이동 중
const { locationX, locationY } = evt.nativeEvent;
console.log('이동:', locationX, locationY);
}}
onResponderRelease={(evt) => {
// 터치 종료 (touch up)
console.log('터치 종료');
}}
>
<Text>드래그 가능</Text>
</View>다른 컴포넌트가 응답자를 요청하거나 시스템이 강제로 해제할 수 있습니다.
<View
onResponderTerminationRequest={(evt) => {
// 다른 컴포넌트가 응답자를 요청합니다
// true 반환 시 응답자 양도
return true;
}}
onResponderTerminate={(evt) => {
// 응답자가 해제되었습니다
// (다른 컴포넌트에 양도 또는 시스템 강제 해제)
console.log('응답자 해제됨');
}}
>
<Text>컨텐츠</Text>
</View>강제 해제 시나리오:
interface TouchEvent {
nativeEvent: {
changedTouches: Touch[]; // 변경된 터치들
identifier: number; // 터치 ID (멀티터치 구분)
locationX: number; // 컴포넌트 기준 X 좌표
locationY: number; // 컴포넌트 기준 Y 좌표
pageX: number; // 화면 기준 X 좌표
pageY: number; // 화면 기준 Y 좌표
target: number; // 이벤트 대상 노드 ID
timestamp: number; // 터치 시간 (속도 계산용)
touches: Touch[]; // 현재 모든 터치들
};
}<View
onResponderMove={(evt) => {
const {
locationX,
locationY,
pageX,
pageY,
timestamp
} = evt.nativeEvent;
// 컴포넌트 내부 좌표로 드래그 처리
setPosition({ x: locationX, y: locationY });
// 화면 전체 좌표로 전역 제스처 처리
updateGlobalGesture({ x: pageX, y: pageY });
// 속도 계산 (스와이프 제스처)
const velocity = calculateVelocity(timestamp);
}}
>
<Text>드래그 가능한 요소</Text>
</View>기본적으로 onStartShouldSetResponder는 가장 깊은 자식부터 호출됩니다.
<View onStartShouldSetResponder={() => true}>
{/* 부모 */}
<View onStartShouldSetResponder={() => true}>
{/* 자식 - 이 컴포넌트가 먼저 응답자가 됨 */}
<Text>버튼</Text>
</View>
</View>결과: 자식 View가 응답자가 됩니다 (모든 버튼과 컨트롤이 동작하도록 보장)
부모가 먼저 응답자가 되도록 하려면 Capture 핸들러를 사용합니다.
<View
onStartShouldSetResponderCapture={() => {
// 부모가 먼저 평가됨
return true; // 자식의 응답자 요청을 차단
}}
>
{/* 부모가 응답자가 됨 */}
<View onStartShouldSetResponder={() => true}>
{/* 자식은 응답자가 될 수 없음 */}
<Text>버튼</Text>
</View>
</View>// 스와이프 컨테이너가 우선권을 가져야 함
<SwipeableRow
onMoveShouldSetResponderCapture={(evt) => {
const { dx } = getGesture(evt);
// 수평 스와이프 시 응답자 획득
return Math.abs(dx) > 10;
}}
>
{/* 버튼 클릭은 여전히 동작 */}
<Button onPress={handlePress} />
</SwipeableRow>const DraggableBox = () => {
const [position, setPosition] = useState({ x: 0, y: 0 });
const [dragging, setDragging] = useState(false);
return (
<View
onStartShouldSetResponder={() => true}
onResponderGrant={() => {
setDragging(true);
}}
onResponderMove={(evt) => {
const { pageX, pageY } = evt.nativeEvent;
setPosition({ x: pageX, y: pageY });
}}
onResponderRelease={() => {
setDragging(false);
}}
style={[
styles.box,
{
left: position.x,
top: position.y,
opacity: dragging ? 0.7 : 1,
},
]}
>
<Text>드래그하세요</Text>
</View>
);
};const SwipeDetector = ({ onSwipe }) => {
const startX = useRef(0);
return (
<View
onStartShouldSetResponder={() => true}
onResponderGrant={(evt) => {
startX.current = evt.nativeEvent.pageX;
}}
onResponderRelease={(evt) => {
const endX = evt.nativeEvent.pageX;
const distance = endX - startX.current;
if (distance > 100) {
onSwipe('right');
} else if (distance < -100) {
onSwipe('left');
}
}}
>
<Text>좌우로 스와이프하세요</Text>
</View>
);
};Gesture Responder System은 React Native에서 복잡한 터치 인터랙션을 처리하는 강력한 메커니즘입니다. 응답자 생명 주기와 캡처/버블링 메커니즘을 이해하면, 여러 컴포넌트가 중첩된 상황에서도 정확한 터치 처리를 구현할 수 있습니다.
핵심 권장사항:
참고 자료:
FlatList의 가상화 메커니즘과 ScrollView 대비 성능 이점 분석
ScrollView의 활용 패턴과 FlatList 대비 적절한 사용 시나리오 분석