πŸ“ iosμ—μ„œ μ‚¬μš©μžμ˜ μ‹€μ‹œκ°„ λ°©ν–₯ κ°€μ Έμ˜€κΈ°

μ •ν˜œμΈΒ·2024λ…„ 12μ›” 12일
1

졜근 μ§„ν–‰ν•œ ν”„λ‘œμ νŠΈμ—μ„œ λͺ¨λ°”μΌμ—μ„œ μœ„μΉ˜ μ •λ³΄λΏλ§Œ μ•„λ‹ˆλΌ μ‚¬μš©μžμ˜ λ°©ν–₯(heading) 정보λ₯Ό μ‹€μ‹œκ°„μœΌλ‘œ κ°€μ Έμ˜€λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν–ˆμŠ΅λ‹ˆλ‹€.
ν•˜μ§€λ§Œ μ‚¬μš©μžμ˜ μœ„μΉ˜μ™€ λ°©ν–₯ 정보λ₯Ό κ°€μ Έμ˜€λŠ” κΈ°λŠ₯을 κ΅¬ν˜„ν•˜λ©΄μ„œ μ˜ˆμƒμΉ˜ λͺ»ν•œ 문제λ₯Ό λ§ˆμ£Όν–ˆμŠ΅λ‹ˆλ‹€.

λ°μŠ€ν¬νƒ‘μ—μ„œλŠ” κ°•μ œλ‘œ μ•ŒνŒŒ 값을 λ³€κ²½ν•˜λ©° ν…ŒμŠ€νŠΈν•  λ•Œ 잘 λ™μž‘ν–ˆμ§€λ§Œ, λͺ¨λ°”μΌμ—μ„œλŠ” λ°©ν–₯ 데이터(alpha)λ₯Ό λ°›μ•„μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€.

λ§Žμ€ 쑰사 끝에 iOSμ—μ„œλŠ” Device Orientation API μ‚¬μš© μ‹œ κΆŒν•œ ν—ˆμš© μ ˆμ°¨κ°€ ν•„μš”ν•˜λ‹€λŠ” 사싀을 μ•Œκ²Œ λ˜μ—ˆκ³ , 이λ₯Ό ν•΄κ²°ν•˜κΈ° μœ„ν•΄ κΆŒν•œμ„ μš”μ²­ν•˜λŠ” λ‘œμ§μ„ μΆ”κ°€ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

전에 ν”„λ‘œμ νŠΈλ₯Ό ν•  λ•Œμ—λ„ λ…ΉμŒκ³Ό κ΄€λ ¨ν•΄μ„œ iosμ—μ„œ κΆŒν•œ λ¬Έμ œκ°€ λ°œμƒν–ˆλ˜ 적이 μžˆμ–΄μ„œ, μ΄λ²ˆμ—λ„ λΉ„μŠ·ν•œ λ¬Έμ œκ² κ±°λ‹ˆ μ‹Άμ–΄ 비ꡐ적 금방 ν•΄κ²°ν•΄ λ‚Ό 수 μžˆμ—ˆλ˜ 것 κ°™μŠ΅λ‹ˆλ‹€.

이번 ν¬μŠ€νŒ…μ—μ„œλŠ” iosμ—μ„œ νšŒμ „ λ°©ν–₯을 κ°€μ Έμ˜€λŠ” κΆŒν•œμ„ μš”μ²­λ°›λŠ” λ‘œμ§μ„ μΆ”κ°€ν•˜κΈ° μœ„ν•œ 과정을 μž‘μ„±ν•΄λ³΄λ € ν•©λ‹ˆλ‹€.


πŸ› οΈ μ‚¬μš©ν•  λ°©ν–₯ 데이터 κ²°μ •

ν”„λ‘œμ νŠΈλ₯Ό μ§„ν–‰ν•˜λ©° μœ„μΉ˜(μœ„λ„/경도)와 λ°©ν–₯(heading) 데이터λ₯Ό λ™μ‹œμ— κ°€μ Έμ˜€λ €κ³  ν–ˆμŠ΅λ‹ˆλ‹€. Geolocation APIλ₯Ό 톡해 heading 값을 얻을 수 μžˆμ§€λ§Œ, 이 값은 μ‚¬μš©μžκ°€ 움직이고 μžˆμ„ λ•Œλ§Œ μœ νš¨ν•©λ‹ˆλ‹€.

κ·ΈλŸ¬λ‹ˆκΉŒ heading 값을 μ‚¬μš©ν•˜κ²Œ 되면, 정지 μƒνƒœμ—μ„œλŠ” heading 값이 null둜 λ°˜ν™˜λœλ‹€λŠ” λ¬Έμ œκ°€ λ°œμƒν•©λ‹ˆλ‹€.

이λ₯Ό λ³΄μ™„ν•˜κΈ° μœ„ν•΄ Device Orientation APIλ₯Ό μ‚¬μš©ν•˜μ—¬ alpha(κΈ°κΈ° λ°©ν–₯) 값을 ν™œμš©ν•˜κΈ°λ‘œ ν–ˆμŠ΅λ‹ˆλ‹€.

1️⃣ GeoLocation의 heading κ°’ ν•œκ³„

일뢀 개발 λ¬Έμ„œλ₯Ό μ°Έκ³ ν–ˆμ„ λ•Œ, μœ„μΉ˜ 정보(GeoLocation)μ—μ„œ heading 값을 ν™œμš©ν•  μˆ˜λ„ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

이미 이동 μœ„μΉ˜λ₯Ό λ°›μ•„μ˜€κΈ° μœ„ν•΄ GeoLocation을 μ‚¬μš©ν•˜κ³  μžˆμ—ˆκΈ° λ•Œλ¬Έμ—, μ΅œλŒ€ν•œ GeoLocation의 값을 μ‚¬μš©ν•˜λŠ” 것이 μ’‹λ‹€κ³  νŒλ‹¨ν•˜μ˜€μ§€λ§Œ, μœ„μ—μ„œ μž‘μ„±ν•œ λŒ€λ‘œ GeoLocationμ—μ„œ λ°©ν–₯을 λ°›μ•„μ˜€λ©΄ 저희 ν”„λ‘œμ νŠΈμ™€ λ§žμ§€ μ•ŠλŠ” λ¬Έμ œκ°€ μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

heading 값은 μ‚¬μš©μžκ°€ 이동 쀑일 λ•Œμ—λ§Œ κ³„μ‚°λ˜λ©°, 정지 μƒνƒœμ—μ„œλŠ” μœ νš¨ν•˜μ§€ μ•Šμ€ κ°’μ΄κ±°λ‚˜ null을 λ°˜ν™˜ν•˜λŠ” λ¬Έμ œκ°€ μžˆμ—ˆκΈ° λ•Œλ¬Έμ—,

정지 μƒνƒœμ—μ„œλ„ μ‚¬μš©μžμ˜ λ°©ν–₯ 데이터λ₯Ό μ•ˆμ •μ μœΌλ‘œ κ°€μ Έμ˜€κΈ° μœ„ν•΄ Device Orientation API의 alpha 값을 μ‚¬μš©ν•˜κΈ°λ‘œ κ²°μ •ν–ˆμŠ΅λ‹ˆλ‹€.

2️⃣ 기본적인 λ°©ν–₯ 데이터 ꡬ쑰

μ›Ήμ—μ„œ μ‚¬μš©μžμ˜ λ°©ν–₯ 데이터λ₯Ό μ–»κΈ° μœ„ν•΄ DeviceOrientationEventλ₯Ό μ‚¬μš©ν–ˆμŠ΅λ‹ˆλ‹€. 이 μ΄λ²€νŠΈλŠ” alpha(κΈ°λ³Έ λ°©ν–₯), beta(μœ„μ•„λž˜ 기울기), gamma(쒌우 기울기) 값을 μ œκ³΅ν•©λ‹ˆλ‹€.

πŸ› οΈ 문제 상황

그런데 λ°μŠ€ν¬νƒ‘μ—μ„œλŠ” 잘 λ™μž‘ν•˜λ˜ μ½”λ“œκ°€ λͺ¨λ°”일(특히 iOS)μ—μ„œλŠ” alpha 값이 null둜 λ°˜ν™˜λ˜λŠ” λ¬Έμ œκ°€ λ°œμƒν–ˆμŠ΅λ‹ˆλ‹€.

μ•„λž˜μ™€ 같이, 개발자 λ„κ΅¬μ˜ μ„Όμ„œλ₯Ό ν™œμš©ν•˜μ—¬ alpha 값을 λ°”κΏ”μ£Όλ©΄, 그에 맞게 이미지가 잘 νšŒμ „ν•˜λŠ” 것을 확인할 수 μžˆμ—ˆμ§€λ§Œ,

ios ν™˜κ²½μ—μ„œλŠ” 이 νšŒμ „μ„ μ œλŒ€λ‘œ λ°›μ•„μ˜€μ§€ λͺ»ν•˜λŠ” λ¬Έμ œκ°€ μžˆμ—ˆκ³ , κ°€λŸ­μ‹œμ—μ„œλŠ” λ³„λ„μ˜ μ„€μ • 없이도 잘 λ™μž‘ν•˜λŠ” 것을 확인할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

κ°€λŸ­μ‹œμ—μ„œλŠ” λ˜λŠ”λ° iosμ—μ„œ λ˜μ§€ μ•ŠλŠ” 경우λ₯Ό 이전 인턴 ν”„λ‘œμ νŠΈμ—μ„œ κ²ͺμ–΄λ΄€μ—ˆκ³ , λ…ΉμŒκ³Ό κ΄€λ ¨ν•œ κΆŒν•œ λ¬Έμ œμ˜€μ–΄μ„œ μ΄λ²ˆμ—λ„ ios의 κΆŒν•œ 문제일 것이라고 μ‰½κ²Œ 좔츑이 κ°€λŠ₯ν–ˆκ³ ,

μ‹€μ œλ‘œ iOS ν™˜κ²½μ—μ„œλŠ” Device Orientation APIλ₯Ό μ‚¬μš©ν•˜λŠ” 데 κΆŒν•œ μš”μ²­μ΄ ν•„μš”ν•˜λ©°, κΆŒν•œμ΄ μ—†μœΌλ©΄ 데이터(alpha, beta, gamma)κ°€ null둜 λ°˜ν™˜λœλ‹€λŠ” 것을 확인할 수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.


πŸ”‘ ν•΄κ²° 방법

κ·Έλž˜μ„œ iosμ—μ„œ κΆŒν•œμ„ ν—ˆμš©ν•΄μ£ΌκΈ° μœ„ν•œ λ‘œμ§μ„ μΆ”κ°€ν•΄μ£Όμ—ˆμŠ΅λ‹ˆλ‹€.

1️⃣ Device Orientation κΆŒν•œ μš”μ²­ 둜직 μΆ”κ°€

DeviceOrientationEvent.requestPermission() λ©”μ„œλ“œλ₯Ό μ‚¬μš©ν•˜μ—¬ κΆŒν•œμ„ μš”μ²­ν•©λ‹ˆλ‹€. 이 λ©”μ„œλ“œλŠ” ν”„λ‘œλ―ΈμŠ€λ₯Ό λ°˜ν™˜ν•˜λ©°, μ„±κ³΅μ μœΌλ‘œ κΆŒν•œμ΄ λΆ€μ—¬λ˜λ©΄ 이벀트 λ¦¬μŠ€λ„ˆλ₯Ό μΆ”κ°€ν•©λ‹ˆλ‹€.

const requestOrientationPermission = async () => {
  const DeviceOrientationEventTyped =
    DeviceOrientationEvent as unknown as IDeviceOrientationEventWithPermission;

  if (
    typeof DeviceOrientationEventTyped !== 'undefined' &&
    typeof DeviceOrientationEventTyped.requestPermission === 'function'
  ) {
    try {
      const permission = await DeviceOrientationEventTyped.requestPermission();
      if (permission === 'granted') {
        window.addEventListener('deviceorientation', handleOrientation);
      } else {
        console.error('Device Orientation permission denied.');
      }
    } catch (error) {
      console.error('Failed to request Device Orientation permission:', error);
    }
  } else {
    console.warn('DeviceOrientationEvent.requestPermission is not supported on this browser.');
    window.addEventListener('deviceorientation', handleOrientation);
  }
};

2️⃣ λ°©ν–₯ 데이터 핸듀링

κΆŒν•œμ„ 얻은 λ’€ deviceorientation 이벀트λ₯Ό 톡해 λ°©ν–₯ 데이터λ₯Ό κ°€μ Έμ˜΅λ‹ˆλ‹€.

const handleOrientation = (event: DeviceOrientationEvent) => {
  if (event.alpha !== null) {
    setLocation(prev => ({ ...prev, alpha: event.alpha }));
  }
};

3️⃣ μœ„μΉ˜μ™€ λ°©ν–₯ 데이터λ₯Ό 병합

useStateλ₯Ό 톡해 μ‚¬μš©μžμ˜ μœ„μΉ˜(μœ„λ„, 경도)와 λ°©ν–₯(alpha) 데이터λ₯Ό μ €μž₯ν•©λ‹ˆλ‹€.

λ§Œμ•½ λ°©ν–₯ 데이터λ₯Ό κ°€μ Έμ˜€μ§€ λͺ»ν•˜λŠ” 경우, λ””ν΄νŠΈ κ°’μœΌλ‘œ μ„€μ •ν•©λ‹ˆλ‹€.

const [location, setLocation] = useState<IGetUserLocation>({
  lat: null,
  lng: null,
  alpha: null,
  error: null,
});

πŸ“„ 전체 μ½”λ“œ μš”μ•½

useEffect(() => {
  let watchId: number;

  const handlePosition = (position: GeolocationPosition) => {
    setLocation(prev => ({
      ...prev,
      lat: position.coords.latitude,
      lng: position.coords.longitude,
      error: null,
    }));
  };

  const handleError = (error: GeolocationPositionError) => {
    setLocation({
      lat: 37.3595704,
      lng: 127.105399,
      alpha: 0,
      error: error.message,
    });
  };

  if (navigator.geolocation) {
    watchId = navigator.geolocation.watchPosition(handlePosition, handleError, {
      enableHighAccuracy: true,
      maximumAge: 5000,
      timeout: 10000,
    });
  } else {
    setLocation({
      lat: 37.3595704,
      lng: 127.105399,
      alpha: 0,
      error: 'ν˜„μž¬ μœ„μΉ˜λ₯Ό λΆˆλŸ¬μ˜€μ§€ λͺ»ν–ˆμŠ΅λ‹ˆλ‹€',
    });
  }

  const handleOrientation = (event: DeviceOrientationEvent) => {
    if (event.alpha !== null) {
      setLocation(prev => ({ ...prev, alpha: event.alpha }));
    }
  };

  const requestOrientationPermission = async () => {
    const DeviceOrientationEventTyped =
      DeviceOrientationEvent as unknown as IDeviceOrientationEventWithPermission;

    if (
      typeof DeviceOrientationEventTyped !== 'undefined' &&
      typeof DeviceOrientationEventTyped.requestPermission === 'function'
    ) {
      try {
        const permission = await DeviceOrientationEventTyped.requestPermission();
        if (permission === 'granted') {
          window.addEventListener('deviceorientation', handleOrientation);
        } else {
          console.error('Device Orientation permission denied.');
        }
      } catch (error) {
        console.error('Failed to request Device Orientation permission:', error);
      }
    } else {
      console.warn('DeviceOrientationEvent.requestPermission is not supported on this browser.');
      window.addEventListener('deviceorientation', handleOrientation);
    }
  };

  requestOrientationPermission().then(() => {
    window.addEventListener('deviceorientation', handleOrientation);
  });

  return () => {
    if (watchId) navigator.geolocation.clearWatch(watchId);
    window.removeEventListener('deviceorientation', handleOrientation);
  };
}, []);

πŸš€ κ²°λ‘ 

결둠적으둜 iOSμ—μ„œ λ°©ν–₯ 데이터λ₯Ό λ°›μ•„μ˜€κΈ° μœ„ν•΄ κΆŒν•œ μš”μ²­ λ‘œμ§μ„ μΆ”κ°€ν–ˆκ³  λͺ¨λ°”μΌμ—μ„œλ„ μ •μƒμ μœΌλ‘œ 데이터λ₯Ό μ²˜λ¦¬ν•  수 μžˆμ—ˆμŠ΅λ‹ˆλ‹€.

μ΄λ²ˆμ—λ„ ios의 κΆŒν•œ 문제인 것을 ν™•μΈν•˜κ³ β€¦.. μ—­μ‹œλ‚˜ λ””λ°”μ΄μŠ€ λ³„λ‘œ μ„Έμ„Έν•œ ν…ŒμŠ€νŠΈκ°€ κΌ­ ν•„μš”ν•˜κ³  μ€‘μš”ν•˜λ‹€λŠ” 것을 ν•œ 번 더 κΉ¨λ‹«κ²Œ λ˜μ—ˆμŠ΅λ‹ˆλ‹€.

0개의 λŒ“κΈ€