본문 바로가기

React

React 앱에서 Device Orientation API 활용하기

모바일에서 기기의 기울기와 방향을 읽어 게임, AR, 파노라마 뷰어, 컴퍼스 등 몰입형 UI를 만들고 싶다면 Device Orientation API를 활용하면 됩니다. React에서는 훅으로 추상화해 권한 요청, 이벤트 구독, 성능 최적화까지 깔끔하게 관리하는 것이 실무적으로 가장 안전합니다.

1. 브라우저 지원과 권한 모델

대부분의 모바일 브라우저가 deviceorientation 이벤트를 지원합니다. 다만 iOS Safari는 사용자 제스처를 통한 명시적 권한 요청이 필요하며, HTTPS 환경에서만 동작하는 점을 유의해야 합니다. iOS 13+에서는 DeviceMotionEvent.requestPermission, iOS 16+에서는 DeviceOrientationEvent.requestPermission도 제공됩니다. 크롬·안드로이드에서는 별도 요청 없이 동작하는 경우가 많지만 역시 보안 컨텍스트에서 테스트해야 합니다.

2. 기본 사용 패턴

핵심 패턴은 기능 탐지, 필요 시 권한 요청, 이벤트 리스너 등록과 클린업, 그리고 과도한 렌더를 막는 스로틀 또는 rAF입니다. 또한 일부 기기에서는 절대 방향(지구 좌표계) 대신 기기 상대 방향을 제공하므로 absolute 플래그와 알파·베타·감마 값의 의미를 로그로 확인하는 것이 좋습니다.

3. 실무용 React 훅 구현

아래 훅은 기능 탐지, iOS 권한 요청, 스로틀, 클린업을 포함합니다. 프로젝트 어디서든 재사용 가능합니다.

import { useCallback, useEffect, useRef, useState } from 'react';

export function useDeviceOrientation(options = {}) {
  const { throttleMs = 100 } = options;
  const [orientation, setOrientation] = useState({
    alpha: null,
    beta: null,
    gamma: null,
    absolute: false,
  });
  const [supported, setSupported] = useState(false);
  const [permission, setPermission] = useState('default'); // 'default' | 'granted' | 'denied'

  const lastTimeRef = useRef(0);
  const cleanupRef = useRef(null);

  // 권한 요청 함수 (iOS)
  const requestPermission = useCallback(async () => {
    let granted = false;
    try {
      if (typeof DeviceMotionEvent !== 'undefined' &&
          typeof DeviceMotionEvent.requestPermission === 'function') {
        const r = await DeviceMotionEvent.requestPermission();
        granted = granted || r === 'granted';
      }
      if (typeof DeviceOrientationEvent !== 'undefined' &&
          typeof DeviceOrientationEvent.requestPermission === 'function') {
        const r2 = await DeviceOrientationEvent.requestPermission();
        granted = granted || r2 === 'granted';
      }
    } catch (e) {
      // 권한 요청 실패
    }
    setPermission(granted ? 'granted' : 'denied');
  }, []);

  // 이벤트 구독
  useEffect(() => {
    const hasAPI = typeof window !== 'undefined' && 'DeviceOrientationEvent' in window;
    setSupported(!!hasAPI);
    if (!hasAPI) return;

    const needPermission = typeof DeviceOrientationEvent !== 'undefined' &&
                           typeof DeviceOrientationEvent.requestPermission === 'function';
    if (needPermission && permission !== 'granted') return; // iOS: 권한 필요 시 대기

    const handle = (e) => {
      const now = Date.now();
      if (throttleMs && now - lastTimeRef.current < throttleMs) return;
      lastTimeRef.current = now;

      const { alpha, beta, gamma, absolute } = e;
      setOrientation({
        alpha: Number.isFinite(alpha) ? alpha : null,
        beta: Number.isFinite(beta) ? beta : null,
        gamma: Number.isFinite(gamma) ? gamma : null,
        absolute: !!absolute,
      });
    };

    window.addEventListener('deviceorientation', handle, { passive: true });
    // 일부 브라우저는 절대 방향 이벤트를 제공합니다.
    window.addEventListener('deviceorientationabsolute', handle, { passive: true });

    cleanupRef.current = () => {
      window.removeEventListener('deviceorientation', handle);
      window.removeEventListener('deviceorientationabsolute', handle);
    };
    return cleanupRef.current;
  }, [permission, throttleMs]);

  // 컴포넌트 언마운트 시 클린업 보장
  useEffect(() => {
    return () => {
      if (cleanupRef.current) cleanupRef.current();
    };
  }, []);

  return { orientation, supported, permission, requestPermission };
}

4. 권한 요청 UI와 디버그 컴포넌트

모바일 Safari에서는 사용자가 버튼을 누르는 등의 제스처로 권한을 요청해야 합니다. 디버그 뷰를 만들어 값이 정상적으로 들어오는지 확인합니다.

import React from 'react';
import { useDeviceOrientation } from './useDeviceOrientation';

export function OrientationDebug() {
  const { orientation, supported, permission, requestPermission } = useDeviceOrientation({ throttleMs: 100 });

  if (!supported) {
    return <p>해당 기기 또는 브라우저가 센서를 지원하지 않습니다.</p>;
  }

  return (
    <div>
      {permission !== 'granted' && (
        <button onClick={requestPermission}>센서 권한 요청</button>
      )}
      <p>alpha(방위): {orientation.alpha != null ? orientation.alpha.toFixed(1) : 'n/a'}</p>
      <p>beta(앞뒤 기울기): {orientation.beta != null ? orientation.beta.toFixed(1) : 'n/a'}</p>
      <p>gamma(좌우 기울기): {orientation.gamma != null ? orientation.gamma.toFixed(1) : 'n/a'}</p>
      <p>absolute: {String(orientation.absolute)}</p>
    </div>
  );
}

5. 실전 예제: 컴퍼스 UI

alpha 값을 활용해 화살표를 북쪽으로 향하게 만들 수 있습니다. 센서 노이즈가 있으므로 스로틀 또는 rAF로 부드럽게 처리합니다.

import React from 'react';
import { useDeviceOrientation } from './useDeviceOrientation';

export function Compass() {
  const { orientation, supported, permission, requestPermission } = useDeviceOrientation({ throttleMs: 50 });
  const heading = orientation.alpha ?? 0; // 일부 기기에서는 지자기 보정이 안 될 수 있습니다.
  const arrowStyle = {
    width: 2,
    height: 45,
    background: 'red',
    transform: `rotate(${Math.round(-heading)}deg)`, // 화면 좌표계를 고려해 반대로 회전
    transition: 'transform 50ms linear',
  };

  return (
    <div style={{ width: 140, margin: '16px auto' }}>
      {supported ? null : <p>센서를 지원하지 않습니다.</p>}
      {permission !== 'granted' && <button onClick={requestPermission}>활성화</button>}
      <div style={{ width: 120, height: 120, borderRadius: '50%', border: '2px solid #333', display: 'grid', placeItems: 'center' }}>
        <div style={arrowStyle} />
      </div>
      <p>heading: {Math.round(heading)}°</p>
    </div>
  );
}

6. 성능 최적화 팁

빠른 센서 업데이트로 인해 React 렌더가 과도해질 수 있습니다. 스로틀 시간을 적절히 조정하거나 requestAnimationFrame으로 배치 업데이트하는 방법이 효과적입니다. 또한 값이 미세하게 변할 때는 toFixed로 텍스트 변화를 줄여 레이아웃 재계산을 최소화합니다.

7. 폴백과 주의사항

일부 환경에서는 alpha가 null이거나 절대값이 아니어서 방위가 정확하지 않을 수 있습니다. 이 경우 화면 회전 각도(window.screen.orientation?.angle)나 사용자 입력 중심 인터랙션으로 폴백을 제공합니다. HTTPS 환경에서 테스트하고, iOS에서는 설정의 Motion & Orientation Access 권한이 차단되어 있지 않은지 확인합니다. 또한 디바이스를 자력이 강한 곳에서 사용할 경우 지자기 간섭으로 편차가 생길 수 있습니다.

8. 개발·테스트 가이드

Chrome DevTools의 Sensors 패널에서 Orientation을 에뮬레이션하여 값 변화를 확인할 수 있습니다. 실제 기기에서 Safari와 Chrome 모두 테스트하고, 앱 초기 진입 시 권한 요청 버튼이 명확히 보이도록 배치합니다. A/B 테스트가 가능하다면 스로틀 값과 UI 부드러움을 비교해 사용자 만족도를 최적화합니다.

9. 마무리

Device Orientation API는 모바일 웹에서 센서 기반 인터랙션을 쉽게 구현할 수 있게 해줍니다. React 훅으로 권한·구독·클린업을 표준화하면 팀 내 재사용성과 유지보수가 향상됩니다. 위 코드로 시작해 사용자의 맥락에 맞는 실감형 UI를 구축해 보시기 바랍니다.