최근 진행한 프로젝트에서 모바일에서 위치 정보뿐만 아니라 사용자의 방향(heading) 정보를 실시간으로 가져오는 기능을 구현했습니다.
하지만 사용자의 위치와 방향 정보를 가져오는 기능을 구현하면서 예상치 못한 문제를 마주했습니다.
데스크탑에서는 강제로 알파 값을 변경하며 테스트할 때 잘 동작했지만, 모바일에서는 방향 데이터(alpha
)를 받아오지 못했습니다.
많은 조사 끝에 iOS에서는 Device Orientation API 사용 시 권한 허용 절차가 필요하다는 사실을 알게 되었고, 이를 해결하기 위해 권한을 요청하는 로직을 추가해주었습니다.
전에 프로젝트를 할 때에도 녹음과 관련해서 ios에서 권한 문제가 발생했던 적이 있어서, 이번에도 비슷한 문제겠거니 싶어 비교적 금방 해결해 낼 수 있었던 것 같습니다.
이번 포스팅에서는 ios에서 회전 방향을 가져오는 권한을 요청받는 로직을 추가하기 위한 과정을 작성해보려 합니다.
프로젝트를 진행하며 위치(위도/경도)와 방향(heading) 데이터를 동시에 가져오려고 했습니다. Geolocation API
를 통해 heading
값을 얻을 수 있지만, 이 값은 사용자가 움직이고 있을 때만 유효합니다.
그러니까 heading
값을 사용하게 되면, 정지 상태에서는 heading
값이 null로 반환된다는 문제가 발생합니다.
이를 보완하기 위해 Device Orientation API
를 사용하여 alpha(기기 방향) 값을 활용하기로 했습니다.
heading
값 한계일부 개발 문서를 참고했을 때, 위치 정보(GeoLocation)에서 heading
값을 활용할 수도 있었습니다.
이미 이동 위치를 받아오기 위해 GeoLocation
을 사용하고 있었기 때문에, 최대한 GeoLocation
의 값을 사용하는 것이 좋다고 판단하였지만, 위에서 작성한 대로 GeoLocation
에서 방향을 받아오면 저희 프로젝트와 맞지 않는 문제가 있었습니다.
heading
값은 사용자가 이동 중일 때에만 계산되며, 정지 상태에서는 유효하지 않은 값이거나 null
을 반환하는 문제가 있었기 때문에,
정지 상태에서도 사용자의 방향 데이터를 안정적으로 가져오기 위해 Device Orientation API의 alpha
값을 사용하기로 결정했습니다.
웹에서 사용자의 방향 데이터를 얻기 위해 DeviceOrientationEvent를 사용했습니다. 이 이벤트는 alpha
(기본 방향), beta
(위아래 기울기), gamma
(좌우 기울기) 값을 제공합니다.
그런데 데스크탑에서는 잘 동작하던 코드가 모바일(특히 iOS)에서는 alpha
값이 null로 반환되는 문제가 발생했습니다.
아래와 같이, 개발자 도구의 센서를 활용하여 alpha 값을 바꿔주면, 그에 맞게 이미지가 잘 회전하는 것을 확인할 수 있었지만,
ios 환경에서는 이 회전을 제대로 받아오지 못하는 문제가 있었고, 갤럭시에서는 별도의 설정 없이도 잘 동작하는 것을 확인할 수 있었습니다.
갤럭시에서는 되는데 ios에서 되지 않는 경우를 이전 인턴 프로젝트에서 겪어봤었고, 녹음과 관련한 권한 문제였어서 이번에도 ios의 권한 문제일 것이라고 쉽게 추측이 가능했고,
실제로 iOS 환경에서는 Device Orientation API를 사용하는 데 권한 요청이 필요하며, 권한이 없으면 데이터(alpha
, beta
, gamma
)가 null
로 반환된다는 것을 확인할 수 있었습니다.
그래서 ios에서 권한을 허용해주기 위한 로직을 추가해주었습니다.
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);
}
};
권한을 얻은 뒤 deviceorientation
이벤트를 통해 방향 데이터를 가져옵니다.
const handleOrientation = (event: DeviceOrientationEvent) => {
if (event.alpha !== null) {
setLocation(prev => ({ ...prev, alpha: event.alpha }));
}
};
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의 권한 문제인 것을 확인하고….. 역시나 디바이스 별로 세세한 테스트가 꼭 필요하고 중요하다는 것을 한 번 더 깨닫게 되었습니다.