DevLog

[React Native] 과도한 중복 이벤트 실행을 막기 위한 디바운스(debounce) 처리 전략 - useState 대신 useRef를 사용하여 값을 저장하고 이벤트 제어하기 본문

Front-end

[React Native] 과도한 중복 이벤트 실행을 막기 위한 디바운스(debounce) 처리 전략 - useState 대신 useRef를 사용하여 값을 저장하고 이벤트 제어하기

김만콩 2025. 5. 28. 15:25

expo-camera 라이브러리를 이용하여 앱 내에 QR코드 스캐너를 구현하고 있다.

스캔한 QR코드에 담긴 데이터에 따라 유효성 검사를 진행하여 올바른 코드가 스캔되었는지를 판별하는 코드를 작성했다. 잘못된 QR을 인식했을 경우에는 '유효하지 않은 코드' 임을 알리는 alert를, 올바른 QR을 스캔한 경우에는 '스캔 완료' alert를 띄우려고 한다.

하지만 CameraView에 넣을 onBarcodeScanned(바코드 스캔 이벤트 처리 함수)를 구현하고 동작 테스트하는 과정에서 문제가 발생했다.


scanned 콜백 함수가 돌아가는 중에도 카메라가 끊임없이 연속적으로 코드를 스캔하면서 이벤트 동작 하나를 다 처리하기도 전에 수십 개의 alert가 쌓이고 있었다.
아무리 확인 버튼을 눌러도 사라지지 않는 무한 alert의 굴레... 어우 징그러,,

useState를 이용한 스캔 이벤트 상태 관리

const [scanned, setScanned] = useState(false);

/** 바코드 스캔 상태 초기화 함수 */
const handleResetScan = () => setScanned(false);

/** 바코드 스캔 데이터 처리 함수 */
const handleBarcodeScan = ({ data }: { data: string }) => {
	if (scanned) return;
	setScanned(true);

	if (!data.startsWith('myapp://')) {
		Alert.alert('유효하지 않은 QR코드입니다.', '어플 공식 QR코드를 인식해주세요!', [
			{
				text: '확인',
				onPress: handleResetScan,
			},
		]);
	} else {
		// 링크 url에서 cardId 정보 get
		const parsedURL = data.split('/');
		const scannedCardId = parseInt(parsedURL[parsedURL.length - 1]);

		console.log(scanned, scannedCardId);
		Alert.alert(`스캔 완료! id: ${scannedCardId}`, '스캔 결과를 저장합니다...', [
			{
				text: '저장',
				onPress: () => handleSaveCard(scannedCardId),
			},
			{ text: '취소', onPress: handleResetScan },
		]);
	}
};

처음 작성한 코드는 이렇다. 스캔 완료/미완료 상태를 구분하는 state를 선언했다.
스캔과 동시에 상태를 true로 업데이트 하고, QR코드를 다시 인식해야 하는 경우 false로 초기화 하는 로직이다.

결과는? 놀랍게도 전혀 도움이 되지 않았다.

스캔 여부에 따른 컴포넌트 조건부 렌더링

{!scanned && (
  <CameraView
	style={styles.scanner}
	onBarcodeScanned={handleBarcodeScan}
	barcodeScannerSettings={{
	  barcodeTypes: ['qr'],
	}}
  />
)}

컴포넌트 함수의 리턴 문 내부에 state에 따른 조건부 렌더링을 추가하여 `scanned === true` 인 경우에는 CameraView 자체가 렌더링 되지 않도록 처리했다. 이렇게 하면 코드가 이미 한 번 스캔된 상황에서 추가적인 중복 스캔이 발생하지 않을 것으로 기대했으나.. 여전히 여러 번 반복해서 alert 창이 띄워졌다.

useRef를 이용한 디바운스 처리

const scannedRef = useRef(false);

/** 바코드 스캔 상태 초기화 함수 */
const handleResetScan = () => (scannedRef.current = false);

/** 바코드 스캔 데이터 처리 함수 */
const handleBarcodeScan = ({ data }: { data: string }) => {
	if (scannedRef.current) return;
	scannedRef.current = true;

	if (!data.startsWith('myapp://')) {
		Alert.alert('유효하지 않은 QR코드입니다.', '어플 공식 QR코드를 인식해주세요!', [
			{
				text: '확인',
				onPress: handleResetScan,
			},
		]);
	} else {
    
		(...)

이번에는 useState 대신 useRef를 사용해서 중복 이벤트 발생을 방지하기 위한 디바운싱 처리를 시도했다.
결과는? 성 공 적👍

debouncing이란?

이벤트가 연속으로 발생하는 경우, 즉시 처리하지 않고 내부적으로 잠시 기다렸다가 마지막에 요청된 한 번만 처리하는 방식을 말한다.

앞선 문제는 카메라가 QR코드를 매우 빠른 속도로 여러 번 감지하면서 onBarcodeScanned 함수가 수십 번 연속 호출되어 중복 실행되면서 발생했다. 이런 경우, 이벤트가 너무 중복으로 발생하지 않도록 하는 디바운스 처리가 필요하다.

왜 useState가 제대로 동작하지 않았을까?

초기 코드에서는 바코드 스캔 이벤트가 중복 처리되지 않도록 제어하는 `scanned` 상태를 정의하고 사용하고자 했지만 기대처럼 동작하지 않았다. 왜 useState로 상태 관리가 제대로 되지 않았을까?

리액트에서 state 변경은 비동기로 처리된다.

즉, 상태 업데이트 처리가 요청한 즉시 이루어지는 것이 아니다. useState의 set 함수는 즉각적으로 상태 변경을 수행하는 것이 아니라 리액트의 상태 변경 스케줄을 설정한다. 다만 스케줄링에서 실제 수행 단계까지 몇 밀리초밖에 걸리지 않기 때문에 사용자의 시선에서는 UI에 변경된 값이 문제 없이 즉각적으로 변하는 것처럼 보일 뿐.. set 함수를 호출했다고 해서 호출 시점에 바로 상태 값 업데이트가 이루어지지는 않는 것이다.

실제로 set 함수를 실행한 직후 로그를 출력해보면 기대와는 달리 변경 이전의 상태 값이 출력되는 걸 볼 수 있다. 이는 상태 업데이트 이전에 로그가 먼저 출력되고, 그 후에 App이 재실행되면서 컴포넌트 함수에 업데이트가 반영되기 때문이다.

`setState(false)` → ❌ `setState((prev) => false)` → ⭕


상태 변경 시 함수 내부에서 이전 상태 값을 받아온 후, 해당 값을 기반으로 업데이트 처리하는 것도 이러한 이유로 예상치 못한 에러를 막고 안전하게 상태를 관리하기 위함이다. 이렇게 코드를 작성하면 리액트가 현재 상태 => 가장 최신 버전 값임을 보장하기 때문!

이러한 state의 특징 때문에 앞선 버그가 발생했다고 판단했다.
카메라 스캔과 같이 매우매우 빠른 속도로 이벤트가 반복 실행되는 경우, 이벤트를 제어하는 state의 변경이 이벤트 콜백 함수 호출 속도를 따라가지 못해서 예상치 못한 동작이 나타나게 되었을 것!

디바운싱에 useRef를 사용한 이유

일반적으로 지금까지는 useRef를 컴포넌트에 연결하여 DOM 요소에 접근하기 위해 사용해왔다.
대표적인 예로 input 컴포넌트에 ref를 지정하고 외부 버튼 클릭 시 ref와 연결된 input에 focus를 주는 경우가 있다.

하지만 이번에 useRef를 다른 용도로도 사용할 수 있다는 걸 알게 되었는데..!
바로 useState처럼 어떠한 값을 저장하고, 리렌더링 시에도 초기화 없이 값을 유지하기 위한 저장소로 사용할 수 있다는 것이다.

useState와의 차이점이 있다면 "렌더링 여부" 를 꼽을 수 있다.
리액트에서 state 개념은 state로 관리되는 값이 변경될 시 해당 컴포넌트가 리렌더링 된다는 점에서 매우 중요하게 작용한다.
반면 useRef의 `ref.current`에 값을 저장할 경우, current 값이 변경되어도 렌더링을 유발하지 않는다.

또한 useRef의 current 값을 변경하는 것은 useState의 set 함수 동작과는 다르게 지연 없이 바로 반영된다.
따라서 중복 처리 방지 플래그와 같이 UI에 영향을 주지는 않지만 특정한 값을 저장하고 유지해야 하는 경우에 ref를 유용하게 사용할 수 있겠다.

처리 결과

디바운싱 결과, 제대로 한 번에 하나의 스캔 이벤트만 발생하고 처리되는 것을 볼 수 있었다.
끔찍했던 무한 alert여 이제 안녕,,

편안~