본문 바로가기

React

React에서 브라우저 Geolocation API 활용하기

브라우저의 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에서 안정적이고 사용자 친화적인 위치 기반 기능을 빠르게 구현할 수 있습니다.