브라우저의 Geolocation API는 사용자의 현재 위치를 정확하게 가져오거나 지속적으로 추적할 수 있어 지도, 배달, 근처 매장 찾기 등 위치 기반 기능에 필수적입니다. React에서는 권한/로딩/에러/정리(cleanup)를 올바르게 다루는 것이 핵심입니다.
1. Geolocation API 개요와 주의사항
- HTTPS 환경에서만 동작합니다(로컬호스트 제외). 배포 시 반드시 HTTPS를 사용합니다.
- 사용자가 위치 권한을 허용해야 하며, 거부 시 대안을 제공해야 합니다.
- 오래된 브라우저나 특정 환경에서는 미지원일 수 있으므로 feature-detection을 먼저 수행합니다.
- SSR(예: Next.js)에서는 window가 없으므로 클라이언트에서만 호출합니다.
2. 최소 예제: 현재 위치 한 번 가져오기
버튼 클릭 시 현재 위치를 요청하는 가장 단순한 패턴입니다.
import React from 'react';
function LocateOnce() {
const [coords, setCoords] = React.useState(null);
const [error, setError] = React.useState(null);
const onClick = () => {
if (!('geolocation' in navigator)) {
setError('이 브라우저는 Geolocation을 지원하지 않습니다.');
return;
}
navigator.geolocation.getCurrentPosition(
(pos) => {
setCoords({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
acc: pos.coords.accuracy,
});
},
(err) => setError(err.message),
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0 }
);
};
return (
<div>
<button onClick={onClick}>현재 위치 가져오기</button>
{coords && (
<p>{coords.lat}, {coords.lng} (±{Math.round(coords.acc)}m)</p>
)}
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
export default LocateOnce;
3. watchPosition으로 실시간 추적과 정리(cleanup)
사용자가 이동할 때마다 좌표를 갱신하려면 watchPosition을 사용합니다. 컴포넌트 언마운트 시 반드시 clearWatch로 정리합니다.
import React from 'react';
function useWatchPosition(options) {
const [coords, setCoords] = React.useState(null);
const [error, setError] = React.useState(null);
React.useEffect(() => {
if (!('geolocation' in navigator)) {
setError('Geolocation 미지원');
return;
}
// options는 useMemo 등으로 참조가 안정적이게 유지하세요.
const id = navigator.geolocation.watchPosition(
(pos) =>
setCoords({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
acc: pos.coords.accuracy,
}),
(err) => setError(err.message),
options
);
return () => navigator.geolocation.clearWatch(id);
}, [options]);
return { coords, error };
}
4. 실전: 재사용 가능한 useGeolocation 훅
권한 상태, 로딩, 단발(getCurrent), 추적(start/stopWatch)을 모두 지원하는 훅입니다. 오류 메시지는 사용자 친화적으로 맵핑합니다.
import React, { useEffect, useRef, useState } from 'react';
function mapGeoError(err) {
switch (err.code) {
case 1:
return '위치 권한이 거부되었습니다.';
case 2:
return '위치 정보를 가져올 수 없습니다.';
case 3:
return '요청이 시간 초과되었습니다.';
default:
return err.message || '알 수 없는 위치 오류';
}
}
export function useGeolocation(defaultOptions) {
const isSupported = typeof window !== 'undefined' && 'geolocation' in navigator;
const [permission, setPermission] = useState('prompt');
const [loading, setLoading] = useState(false);
const [coords, setCoords] = useState(null);
const [error, setError] = useState(null);
const watchIdRef = useRef(null);
// 권한 상태 관찰(Permissions API)
useEffect(() => {
let mounted = true;
let status = null;
let changeHandler = null;
if (!isSupported) return;
if (navigator.permissions && navigator.permissions.query) {
navigator.permissions.query({ name: 'geolocation' }).then((res) => {
if (!mounted) return;
status = res;
setPermission(res.state);
changeHandler = () => setPermission(res.state);
res.addEventListener('change', changeHandler);
});
}
return () => {
mounted = false;
if (status && changeHandler) {
status.removeEventListener('change', changeHandler);
}
};
}, [isSupported]);
const getCurrent = React.useCallback(
(opts) => {
if (!isSupported) {
setError('Geolocation을 지원하지 않습니다.');
return;
}
setLoading(true);
setError(null);
navigator.geolocation.getCurrentPosition(
(pos) => {
setLoading(false);
setCoords({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
acc: pos.coords.accuracy,
heading: pos.coords.heading,
speed: pos.coords.speed,
});
},
(err) => {
setLoading(false);
setError(mapGeoError(err));
},
{ enableHighAccuracy: true, timeout: 10000, maximumAge: 0, ...defaultOptions, ...opts }
);
},
[isSupported, defaultOptions]
);
const startWatch = React.useCallback(
(opts) => {
if (!isSupported) {
setError('Geolocation을 지원하지 않습니다.');
return;
}
if (watchIdRef.current != null) return; // 중복 방지
setError(null);
const id = navigator.geolocation.watchPosition(
(pos) =>
setCoords({
lat: pos.coords.latitude,
lng: pos.coords.longitude,
acc: pos.coords.accuracy,
heading: pos.coords.heading,
speed: pos.coords.speed,
}),
(err) => setError(mapGeoError(err)),
{ enableHighAccuracy: true, maximumAge: 1000, timeout: 20000, ...defaultOptions, ...opts }
);
watchIdRef.current = id;
},
[isSupported, defaultOptions]
);
const stopWatch = React.useCallback(() => {
if (watchIdRef.current != null && isSupported) {
navigator.geolocation.clearWatch(watchIdRef.current);
watchIdRef.current = null;
}
}, [isSupported]);
// 언마운트 시 정리
useEffect(() => {
return () => {
if (watchIdRef.current != null && isSupported) {
navigator.geolocation.clearWatch(watchIdRef.current);
}
};
}, [isSupported]);
return { isSupported, permission, loading, coords, error, getCurrent, startWatch, stopWatch };
}
5. 컴포넌트 사용 예와 UI 상태 처리
권한 상태/로딩/에러를 명확히 보여주고, 추적 시작/중지를 토글합니다.
import React from 'react';
import { useGeolocation } from './useGeolocation';
function LocationWidget() {
const { isSupported, permission, loading, coords, error, getCurrent, startWatch, stopWatch } = useGeolocation();
if (!isSupported) return <p>이 브라우저는 위치 기능을 지원하지 않습니다.</p>;
return (
<div>
<p>권한 상태: {permission}</p>
<div>
<button onClick={() => getCurrent()} disabled={loading}>현재 위치</button>
<button onClick={() => startWatch()}>실시간 추적 시작</button>
<button onClick={stopWatch}>추적 중지</button>
</div>
{loading && <p>가져오는 중...</p>}
{coords && (
<p>
{coords.lat}, {coords.lng} (±{Math.round(coords.acc)}m)
{coords.speed != null && `, 속도: ${coords.speed}m/s`}
</p>
)}
{error && <p style={{ color: 'red' }}>{error}</p>}
</div>
);
}
export default LocationWidget;
6. 에러 처리와 옵션 베스트 프랙티스
- enableHighAccuracy: true는 배터리 소모가 증가하지만 정확도가 필요할 때 유용합니다. 필요 시에만 활성화합니다.
- timeout: 너무 짧으면 빈번한 실패, 너무 길면 UX 악화입니다. 10~20초 정도로 시작하고 서비스 특성에 맞춥니다.
- maximumAge: 캐시된 위치 허용 시간입니다. 빠른 응답이 필요하면 0, 배터리 절약이 중요하면 수 초~수 분으로 조정합니다.
- 권한 거부 시: 수동 위치 입력(주소/지번)이나 대략적인 지역 선택 UI를 제공하여 이탈을 줄입니다.
7. SSR/Next.js에서의 처리
지리 API는 클라이언트 전용입니다. App Router 사용 시 클라이언트 컴포넌트로 분리하거나 동적 import로 SSR을 비활성화합니다.
// 1) 클라이언트 컴포넌트 상단에 선언
'use client';
import React from 'react';
import LocationWidget from './LocationWidget';
export default function Page() {
return <LocationWidget />;
}
// 2) 또는 동적 import (pages/app 어디서든)
import dynamic from 'next/dynamic';
const ClientOnlyLocation = dynamic(() => import('./LocationWidget'), { ssr: false });
export default function Page() {
return <ClientOnlyLocation />;
}
8. 주소 역지오코딩 팁(선택)
좌표를 사람이 읽을 수 있는 주소로 변환하려면 외부 지오코딩 API를 사용합니다. 아래는 OpenStreetMap Nominatim 예시입니다(사용 정책을 준수하세요).
async function reverseGeocode({ lat, lng }) {
const res = await fetch(
`https://nominatim.openstreetmap.org/reverse?format=jsonv2&lat=${lat}&lon=${lng}`,
{ headers: { 'Accept-Language': 'ko' } }
);
if (!res.ok) throw new Error('역지오코딩 실패');
const data = await res.json();
return data.display_name;
}
// 사용 예: coords 변경 시 주소 갱신
// useEffect(() => { if (coords) reverseGeocode(coords).then(setAddress); }, [coords]);
9. 테스트와 디버깅
Chrome DevTools > More tools > Sensors에서 위치를 시뮬레이션할 수 있습니다. 실내에서는 GPS 정확도가 낮아질 수 있으므로 야외 테스트도 병행합니다.
10. 구현 체크리스트
- HTTPS 적용, 브라우저 지원 체크, 권한 상태 UI 제공, 에러 메시지 현지화, watchPosition 정리, 옵션 튜닝(정확도/배터리/속도), SSR 분리, 대체 입력(권한 거부 시), 로깅 및 모니터링.
위 원칙과 훅 패턴을 따르면 React에서 안정적이고 사용자 친화적인 위치 기반 기능을 빠르게 구현할 수 있습니다.
'React' 카테고리의 다른 글
| React에서 react-hook-form으로 대규모 폼 최적화하기 (0) | 2026.07.22 |
|---|---|
| React 앱에서 Command Palette 인터페이스 구현하기 (1) | 2026.07.21 |
| React와 tRPC를 활용한 타입 안전 API 통신 구축하기 (0) | 2026.07.16 |
| React 앱에서 Feature Flag 시스템 설계 및 적용하기 (0) | 2026.07.16 |
| React에서 TanStack Table로 고성능 데이터 테이블 구현하기 (0) | 2026.07.15 |