브라우저에게 "너 어디야?"를 묻는 법 — Geolocation API
날씨앱을 만들면 제일 먼저 부딪히는 게 "사용자 위치 가져오기"다. navigator.geolocation의 기본 사용법과, HTTPS·권한·에러 처리·정확도 옵션 같은 실전에서 꼭 데이는 지점들을 퍼블리셔 시선으로 정리한다.
날씨앱(today.nacre.kr)을 만들면 제일 먼저 부딪히는 문제가 있어요.
이 사람 지금 어디 있지.
그걸 알아야 그 동네 날씨를 보여주고, 체감온도에 맞는 옷차림까지 골라줄 수 있으니까요.
브라우저에 그걸 묻는 표준 방법이 Geolocation API예요.
한 번만 위치를 물어보는 건 이게 전부예요.
navigator.geolocation.getCurrentPosition(
(pos) => {
const { latitude, longitude } = pos.coords;
console.log(latitude, longitude); // 여기로 날씨 API를 부른다
},
(err) => {
console.error(err.code, err.message); // 실패했을 때
},
{ enableHighAccuracy: false, timeout: 8000, maximumAge: 60_000 } // 옵션
);
성공 콜백, 실패 콜백, 옵션 셋을 넘겨요.
값은 pos.coords에 위도·경도가 담겨 오고요.
여기까지는 쉬워요.
진짜 문제는 그 다음이에요.
먼저, HTTPS가 아니면 아예 안 돼요.
Geolocation은 secure context에서만 동작해요.
https://나 localhost여야 하고, 그냥 http://로 열면 조용히 실패해요.
로컬 IP(http://192.168...)로 폰에서 테스트할 때 “왜 안 되지?”
하는 게 십중팔구 이거예요.
그리고 권한은 물어봐야 나오고, 거절당해요.
getCurrentPosition을 부르면 브라우저가 팝업을 띄우고, 사용자는 허용·거절·무시 셋 다 할 수 있어요.
우리 코드는 그 셋을 전부 감당해야 하고요.
navigator.geolocation.getCurrentPosition(onOk, (err) => {
switch (err.code) {
case err.PERMISSION_DENIED: // 사용자가 거절
showManualCitySearch(); // → 도시 직접 검색으로 폴백
break;
case err.POSITION_UNAVAILABLE: // 기기가 위치를 못 잡음
showRetry();
break;
case err.TIMEOUT: // timeout 안에 못 받음
showRetry();
break;
}
});
여기서 진짜 중요한 건 거절당했을 때의 화면을 준비하는 거예요.
위치를 못 받았다고 앱이 빈 화면이 되면 안 되잖아요.
날씨앱이라면 “도시 직접 검색” 같은 폴백이 있어야 해요.
퍼블리셔로 치면 이미지가 안 뜰 때 alt와 자리를 챙기던 것과 같은 감각이에요.
실패가 기본값이라 치고 설계하는 것.
팝업을 띄우는 타이밍도 UX예요.
페이지가 열리자마자 띄우면 맥락 없는 요청이라 사용자가 반사적으로 거절해요.
”내 위치 날씨 보기” 버튼을 누른 직후처럼, 이유를 아는 순간에 요청하면 허용률이 훨씬 높고요.
정확도 옵션은 트레이드오프예요.
enableHighAccuracy: true는 GPS까지 동원해 더 정확하지만 느리고 배터리를 더 먹어요 — 날씨처럼 동네 단위면 되는 앱엔 false로 충분하고요.
timeout은 그 시간 안에 못 받으면 TIMEOUT 에러를 내서 무한 대기를 막아요.
maximumAge는 그 시간 안에 받아둔 캐시 위치가 있으면 새로 안 잡고 그걸 써서, 빠르고 배터리도 아끼고요.
지도에서 내 위치가 이동을 따라오게 하려면 watchPosition으로 구독해요.
그리고 안 쓸 땐 반드시 해제해야 해요.
안 그러면 계속 위치를 잡느라 배터리를 태우거든요.
const id = navigator.geolocation.watchPosition(onMove, onErr, opts);
// 화면을 떠날 때
navigator.geolocation.clearWatch(id);
React라면 useEffect의 정리 함수에서 clearWatch를 부르면 돼요.
구독은 해제까지가 한 세트라는 감각, 여기서도 그대로고요.
팝업을 띄우기 전에 지금 권한 상태를 미리 볼 수도 있어요.
const status = await navigator.permissions.query({ name: 'geolocation' });
// status.state === 'granted' | 'prompt' | 'denied'
denied면 팝업을 띄워도 소용없으니 바로 폴백 UI로, granted면 버튼 없이 조용히 위치를 잡는 식으로 흐름을 다듬을 수 있어요.
다만 이 둘은 나이가 꽤 달라요.
위치를 묻는 쪽은 2015년에 이미 주요 브라우저에 다 들어와 있었는데, 권한 상태를 미리 들여다보는 쪽은 사파리가 2022년 9월(16)에야 합류했고 Baseline에서 ‘widely’가 된 건 2025년 3월이에요.
퍼블리셔 시절 호환성 표를 훑던 습관이 여기서도 그대로 쓰여요.
다른 게 있다면 이제 브라우저 칸을 하나씩 세는 대신 초록불 한 줄을 본다는 것 정도.
그래서 permissions.query는 try/catch로 감싸두는 편이에요.
브라우저가 모르는 권한 이름을 받으면 그냥 빈 값이 아니라 TypeError로 거절하거든요.
미리 보려던 코드가 정작 본체인 위치 요청을 막아버리면 곤란하니까요.
여기서 재밌는 이야기 하나.
getCurrentPosition은 콜백 API라 async/await로 예쁘게 쓰려고 Promise로 감싸는 경우가 많아요.
function getPosition(opts) {
return new Promise((resolve, reject) => {
if (!('geolocation' in navigator)) {
reject(new Error('unsupported')); // 미지원 브라우저 분기
return;
}
navigator.geolocation.getCurrentPosition(resolve, reject, opts);
});
}
그런데 날씨앱에선 감싼 진짜 이유가 문법 취향이 아니라 lint였어요.
미지원 브라우저 분기에서 effect 안에서 곧바로 에러 상태를 setState 했더니 react-hooks/set-state-in-effect에 걸린 거예요.
effect가 실행되는 도중에 동기로 상태를 바꾸지 말라는 규칙이죠.
useEffect(() => {
getPosition().then(onOk).catch(onFail); // then/catch는 마이크로태스크
}, []);
then/catch로 넘기니 그 setState가 마이크로태스크로 한 박자 밀려서, 더 이상 “effect 실행 중 동기 setState”가 아니게 됐고 규칙을 통과했어요.
UX가 아니라 실행 타이밍을 바꾼 거예요.
이벤트 루프에서 본 “Promise 콜백은 마이크로태스크”가 이렇게 실전에서 튀어나오더라고요.
마지막으로 프라이버시.
위치는 민감 정보예요.
필요할 때만, 이유를 밝히고 요청하는 게 예의이자 신뢰고요.
받아온 좌표를 굳이 서버에 저장할 필요가 없다면 저장하지 않는 게 제일 안전해요.
한 걸음 더 — 좌표를 굳이 정밀하게 보내지 마세요.
날씨앱은 맛집·명소를 우리 서버(/api/restaurants·/api/places)를 거쳐 받는데, 좌표를 소수점 2자리(약 1.1km)로 눌러서 보내요.
const coarse = (n) => Math.round(n * 100) / 100; // 37.5665 → 37.57
이러면 두 가지가 좋아져요.
프라이버시 — 캐시 키·서버 로그에 정확한 GPS가 아예 안 남아요.
성능 — 같은 동네에서 온 요청이 같은 캐시를 타서, 뒤에 있는 공공 API 호출도 줄어요.
“저장하지 않는다”보다 한 걸음 구체적인, 아예 정밀도를 버리는 방식이죠.
(비밀 값이 어디까지 서버의 영역인가와 같은 결의 고민이에요.)
호출은 세 줄인데 어려운 건 나머지 전부예요.
HTTPS 위에서, 사용자 행동 뒤에, 거절당할 걸 전제로, 정확도는 필요한 만큼만.
날씨앱을 만들며 배운 건 결국 “위치를 가져오는 법”이 아니라 “위치를 못 가져왔을 때 어떻게 할까”였어요.
댓글