React Native 바텀시트에서 슬라이더 드래그 시 깜빡임 문제 해결

언젠가만날날·2025년 9월 25일

문제상황

React Native 앱에서 바텀시트 모달 내부의 슬라이더(ProgressBar)를 드래그할 때 다음과 같은 문제가 발생했습니다:

  • 드래그 시작 시: 바텀시트 위치가 순간적으로 초기화되었다가 다시 복구됨(내려갔다가 트랜지션을 거치며 다시 올라옴)
  • 드래그 중: 지속적인 깜빡임 발생
  • 드래그 종료 시: 바텀시트가 내려갔다가 올라온 후, 슬라이더 값이 원래 위치로 되돌아감

시도한 방법들

1. 디바운스 적용

onChange 콜백이 연속적으로 호출되는 것이 문제라고 예상하여 디바운스를 적용해보았습니다.

const handleLightChange = useCallback((value) => {
  setLocalLight(value);
  setTimeout(() => {
    setLight(value);
  }, 100);
}, [setLight]);
  • 그러나 호출 빈도만 줄일 뿐 리렌더링을 완전히 하지 않는 것이 아니기 때문에 100ms 후 부모 컴포넌트 상태가 업데이트되어 문제를 근본적으로 해결하지 못했습니다.

  • 여러 타이머가 동시에 실행되어 race condition 가능성이 존재한다고 판단했습니다.

2. 전용 상태 도입

const [isDragging, setIsDragging] = useState(false);

const updateValueFromX = useCallback((x) => {
  const newValue = calculateValue(x);
  setLocalValue(newValue);
  
  if (!isDragging) {
    onChange(newValue);
  }
}, [isDragging, onChange]);

//...

const panResponder = PanResponder.create({
  onPanResponderGrant: (evt) => {
    setIsDragging(true);
  },
  onPanResponderRelease: () => {
    setIsDragging(false);
    onChange(localValue);
  },
});
  • 드래그 중에는 부모 상태 업데이트를 차단하는 데는 성공했지만, 드래그 종료 시 onChange()가 의존성으로 포함되어 있어서 변경될 때 마다 updateValueFromX 함수가 재생성되어 성능 오버헤드가 있습니다.

  • 또한 panResponder의 핸들러가 계속 변경되어 이벤트 처리가 불안정하다고 생각했습니다.

  • React의 batch 업데이트로 인해 setIsDragging()onChange()의 실행 순서가 랜덤해서 이따금씩 데이터 반영이 제대로 되지 않는 문제가 있었습니다.

3. requestAnimationFrame 사용

const handleLightChange = useCallback((value) => {
  setLocalLight(value);
  requestAnimationFrame(() => {
    setLight(value);
  });
}, [setLight]);
  • requestAnimationFrame은 브라우저 렌더링 파이프라인과 동기화되기 때문에 네이티브 엔진과 불일치가 있을 것이라고 예상했습니다.

    • RN에서는 JS 스레드의 rAF와 네이티브 UI 스레드 커밋 타이밍이 분리되어 있어, 프레임 경계를 가르는 업데이트 조합은 UI 위치 불안정을 유발하기 쉽다고 생각했습니다.
  • 이전 프레임 결과가 잠깐 보였다가, 다음 프레임 직전 rAF에서 상태가 다시 적용되며 레이아웃 점프가 발생했습니다.

    • React의 자동 배칭은 같은 콜백/같은 틱 내부의 업데이트만 묶는데, setLocalLight()는 현재 틱, rAF(() => setLight())는 다음 프레임의 별도 콜백이라 서로 다른 배치로 처리되어 두 번의 렌더가 생겼다고 생각했습니다.

원인 분석

1. React 렌더링 사이클과 상태 업데이트

const handleLightChange = (value) => {
  setLocalLight(value);
  setLight(value);
};

슬라이더 드래그 시 onChange 콜백이 연속적으로 호출됨

  • 각 호출마다 부모 컴포넌트의 상태가 업데이트되어 리렌더링 발생
  • 바텀시트 컴포넌트가 리렌더링되면서 위치가 재계산됨

2. 이벤트 전파와 상태 동기화

const updateValueFromX = (x) => {
  const newValue = calculateValue(x);
  onChange(newValue);  // 부모에게 즉시 알림 → 리렌더링 트리거
};
  • PanResponder의 onPanResponderMove에서 연속적인 상태 업데이트
  • 부모 컴포넌트의 리렌더링이 바텀시트의 애니메이션과 충돌
  • 상태 업데이트와 UI 렌더링이 동기화되지 않음

최종 솔루션

작성 완료 시에만 최종 값 전달

이전 시도들에서는 부모 상태를 실시간으로 업데이트하려다 보니

  • 리렌더링이 과도하게 발생합니다.
  • 두 컴포넌트 간의 데이터 이동이 잦아서 결합도가 높았습니다.
  • UI 위치가 불안정해지는 문제가 있었습니다.

이것을 해결하기 위해 드래그 중에는 오직 로컬 상태만 사용하고, 사용자가 명시적으로 작성 완료 버튼을 누를 때만 부모 상태를 업데이트하는 전략을 선택했습니다.

PR 링크

ProgressBar 컴포넌트
ReviewsModal 컴포넌트

참고 자료

https://react.dev/learn/queueing-a-series-of-state-updates
https://flaviocopes.com/requestanimationframe

0개의 댓글