Claude Code로 Geolocation API 안전하게 구현하기
Claude Code로 Geolocation API를 안전하게 구현합니다. 권한 UX, HTTPS, 대체 입력, 테스트까지 다룹니다.
위치 정보 기능은 버튼 하나로 끝나지 않습니다. 가까운 매장 찾기, 배송 가능 지역 판정, 현장 작업 체크인처럼 익숙한 기능도 실제 서비스에서는 권한 안내, HTTPS, 거부 처리, 타임아웃, 수동 주소 입력, 지도 서비스와의 경계, 로그 비식별화, 테스트용 위치 모킹까지 필요합니다.
Claude Code에 “현재 위치 버튼을 추가해 줘”라고만 요청하면 getCurrentPosition을 호출하는 코드만 나올 수 있습니다. 공개 서비스라면 왜 위치가 필요한지 설명하고, 사용자가 거부해도 다음 단계로 갈 수 있게 만들고, 원시 위도와 경도를 로그에 남기지 않는 설계가 필요합니다.
이 글은 브라우저 Geolocation API를 Claude Code로 구현하는 초보자 친화적인 절차입니다. 기준 문서는 MDN Geolocation API, getCurrentPosition, watchPosition, Permissions API, W3C Geolocation specification, Chrome의 secure origins 안내, Chrome DevTools Sensors, Playwright emulation, Claude Code permissions입니다. 지도 UI는 지도 연동 가이드, 보안 검토는 보안 감사, 모바일 UX는 반응형 디자인과 함께 보면 좋습니다.
먼저 정해야 할 경계
Geolocation API는 브라우저가 기기의 위치를 추정해 애플리케이션에 전달하는 API입니다. 위치는 GPS, Wi-Fi, 기지국, IP, 운영체제 위치 서비스, 캐시에서 올 수 있습니다. 앱이 직접 출처를 고르지 못하며, 반환된 값이 사용자의 실제 위치를 항상 보장하지도 않습니다.
따라서 구현 전에 네 가지를 정해야 합니다. 첫째, 위치 권한은 사용자가 명확한 행동을 했을 때만 요청합니다. 둘째, 기본값은 고정밀이 아니라 enableHighAccuracy: false로 둡니다. 셋째, 거부나 실패 시 우편번호, 주소, 지역명 입력을 제공합니다. 넷째, 원시 좌표를 저장할지, 계산 후 버릴지 정합니다.
| 결정 | 기본 권장안 | 실패 예 |
|---|---|---|
| 요청 시점 | 버튼 클릭 후 | 첫 화면 로드와 동시에 권한 요청 |
| 정확도 | 처음에는 고정밀 OFF | 모든 검색에 GPS 수준 정확도 요구 |
| 실패 처리 | 주소/우편번호 입력 제공 | 권한 거부 시 기능 전체 중단 |
| 로그 | 이벤트와 coarse bucket만 저장 | position 객체를 그대로 로그로 전송 |
지도 연동도 분리해야 합니다. Geolocation은 좌표를 반환할 뿐입니다. 지도 렌더링, 주소 변환, 경로 계산, 주변 장소 검색은 Google Maps, Mapbox, OpenStreetMap 기반 서비스나 서버 API의 책임입니다.
제품 유스케이스
첫 번째는 매장 찾기입니다. 사용자가 “현재 위치 사용”을 누르면 좌표를 받아 가까운 매장을 정렬합니다. 전환율을 지키려면 같은 영역에 “우편번호로 검색”을 함께 둡니다. 권한을 거부한 사용자도 자연스럽게 계속 진행할 수 있습니다.
두 번째는 배송 또는 방문 서비스 가능 지역 판정입니다. 식료품 배송, 수리 방문, 렌털 서비스는 위치를 기준으로 서비스 가능 여부와 시간대를 보여줄 수 있습니다. 이때 서버에 꼭 필요한 것은 원시 좌표가 아니라 inside_zone이나 거리 구간일 수 있습니다.
세 번째는 현장 작업 체크인입니다. 청소, 유지보수, 이벤트 운영, 영업 방문에서 작업자가 현장 근처에 있는지 확인할 수 있습니다. 하지만 위치 실패로 업무가 멈추면 안 됩니다. 사진, 관리자 승인, 수기 메모 같은 대체 경로를 준비합니다.
네 번째는 지역 콘텐츠입니다. 날씨, 재고 있는 매장, 주변 이벤트, 지역 공지에 활용할 수 있습니다. 이 경우에는 정확한 좌표보다 사용자가 선택한 도시나 우편번호가 더 적절한 경우도 많습니다.
getCurrentPosition 기본 예제
아래 파일을 geo-demo.html로 저장하고 localhost 또는 HTTPS에서 실행하세요. 일반 HTTP나 안전하지 않은 iframe에서는 브라우저가 위치 요청을 막을 수 있습니다.
<!doctype html>
<html lang="ko">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Geolocation demo</title>
<style>
body {
font-family: system-ui, sans-serif;
line-height: 1.6;
margin: 2rem;
}
button,
input {
font: inherit;
padding: 0.7rem 0.9rem;
}
.panel {
border: 1px solid #ddd;
max-width: 36rem;
padding: 1rem;
}
</style>
</head>
<body>
<main class="panel">
<h1>가까운 매장 찾기</h1>
<p>
현재 위치는 매장 정렬에만 사용합니다.
정확한 주소나 이동 기록은 저장하지 않습니다.
</p>
<button id="useLocation" type="button">현재 위치 사용</button>
<p id="status" role="status" aria-live="polite"></p>
<pre id="result"></pre>
<form id="manualForm">
<label for="postcode">우편번호 또는 지역명</label>
<input id="postcode" name="postcode" autocomplete="postal-code" />
<button type="submit">직접 검색</button>
</form>
</main>
<script type="module">
const status = document.querySelector("#status");
const result = document.querySelector("#result");
const button = document.querySelector("#useLocation");
const form = document.querySelector("#manualForm");
function showManual(reason) {
status.textContent =
`${reason}. 우편번호나 지역명으로도 검색할 수 있습니다.`;
}
function onSuccess(position) {
const { latitude, longitude, accuracy } = position.coords;
status.textContent = "현재 위치를 가져왔습니다.";
result.textContent = JSON.stringify(
{
lat: Number(latitude.toFixed(5)),
lng: Number(longitude.toFixed(5)),
accuracyMeters: Math.round(accuracy),
},
null,
2,
);
}
function onError(error) {
const messages = {
1: "위치 권한이 거부되었습니다",
2: "기기 위치를 사용할 수 없습니다",
3: "위치 요청 시간이 초과되었습니다",
};
showManual(messages[error.code] ?? "위치를 가져올 수 없습니다");
}
button.addEventListener("click", () => {
if (!("geolocation" in navigator)) {
showManual("이 브라우저는 Geolocation을 지원하지 않습니다");
return;
}
status.textContent = "위치 권한을 확인하는 중...";
navigator.geolocation.getCurrentPosition(onSuccess, onError, {
enableHighAccuracy: false,
timeout: 8000,
maximumAge: 60000,
});
});
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
status.textContent =
`"${data.get("postcode")}" 주변을 검색합니다.`;
});
</script>
</body>
</html>
timeout은 기다릴 최대 시간, maximumAge는 캐시 위치를 허용할 시간, enableHighAccuracy는 가능한 경우 더 높은 정확도를 요청하는 옵션입니다. 매장 찾기라면 1분 정도의 캐시를 허용할 수 있지만, 현장 체크인은 더 짧게 설정하는 편이 안전합니다.
watchPosition은 시작과 종료가 핵심
watchPosition은 위치 변화를 계속 받습니다. 배송원 추적이나 작업 경로 기록에는 유용하지만, 단순 검색에는 과합니다. 반환된 ID를 clearWatch에 넘겨 종료하지 않으면 화면을 떠난 뒤에도 업데이트가 이어질 수 있습니다.
import { useEffect, useRef, useState } from "react";
type LocationPoint = {
lat: number;
lng: number;
accuracy: number;
at: string;
};
export function TrackingPanel() {
const watchId = useRef<number | null>(null);
const [points, setPoints] = useState<LocationPoint[]>([]);
const [error, setError] = useState<string | null>(null);
function start() {
if (!navigator.geolocation || watchId.current !== null) return;
watchId.current = navigator.geolocation.watchPosition(
(position) => {
const { latitude, longitude, accuracy } = position.coords;
setPoints((current) => [
{
lat: Number(latitude.toFixed(5)),
lng: Number(longitude.toFixed(5)),
accuracy: Math.round(accuracy),
at: new Date(position.timestamp).toISOString(),
},
...current.slice(0, 9),
]);
},
(err) => setError(`추적 실패: ${err.code}`),
{
enableHighAccuracy: true,
timeout: 10000,
maximumAge: 5000,
},
);
}
function stop() {
if (watchId.current === null) return;
navigator.geolocation.clearWatch(watchId.current);
watchId.current = null;
}
useEffect(() => stop, []);
return (
<section>
<button type="button" onClick={start}>추적 시작</button>
<button type="button" onClick={stop}>중지</button>
{error && <p role="alert">{error}</p>}
<ol>
{points.map((point) => (
<li key={point.at}>
{point.lat}, {point.lng}
{" / "}
{point.accuracy}m
</li>
))}
</ol>
</section>
);
}
위치 추적 제품은 “시작”, “일시정지”, “종료” 상태가 UI에 보여야 합니다. 추적 데이터의 보관 기간, 열람 권한, 삭제 방법도 코드 작성 전에 정해야 합니다.
개인정보와 로그
위도와 경도는 일반 디버그 정보가 아닙니다. 오류 모니터링, 분석 이벤트, 서버 로그, 고객 지원 화면에 남으면 의도치 않게 민감한 위치 데이터를 보관하게 됩니다.
type GeoLogInput = {
lat: number;
lng: number;
accuracy: number;
permission: "granted" | "prompt" | "denied" | "unknown";
};
export function toPrivacySafeGeoLog(input: GeoLogInput) {
return {
permission: input.permission,
accuracyBucket:
input.accuracy <= 50 ? "high" :
input.accuracy <= 500 ? "medium" : "low",
latBucket: Number(input.lat.toFixed(2)),
lngBucket: Number(input.lng.toFixed(2)),
};
}
전환 분석에는 원시 좌표 대신 permission_denied, manual_search_used, timeout, store_results_shown 같은 이벤트가 더 적합합니다. Claude Code 프롬프트에 “raw latitude/longitude를 로그에 남기지 말 것”을 명확히 넣으세요.
Permissions API는 권한 상태를 읽을 때 사용할 수 있습니다. 다만 prompt는 아직 결정되지 않았다는 뜻이며, 실제 브라우저 권한 창은 위치 API 호출 시점에 표시됩니다.
export async function readGeoPermission() {
if (!("permissions" in navigator)) return "unknown";
try {
const status = await navigator.permissions.query({
name: "geolocation",
});
return status.state;
} catch {
return "unknown";
}
}
테스트해야 할 실패 모드
첫째, HTTPS와 안전한 컨텍스트를 확인합니다. 개발 환경의 localhost에서는 되지만 배포 도메인, 프록시, iframe에서는 실패할 수 있습니다.
둘째, 권한 거부를 테스트합니다. 거부는 정상적인 선택입니다. 사용자를 탓하지 말고 주소 입력으로 자연스럽게 전환해야 합니다.
셋째, 타임아웃과 위치 사용 불가를 테스트합니다. 실내 환경, 데스크톱, 운영체제 위치 서비스 OFF, VPN, 기업 정책은 모두 실패 원인이 됩니다.
넷째, maximumAge로 인한 오래된 위치를 확인합니다. 빠른 UX에는 좋지만 체크인처럼 정확한 현재성이 필요한 기능에는 위험할 수 있습니다.
Playwright에서는 위치와 권한을 함께 지정할 수 있습니다.
import { expect, test } from "@playwright/test";
test.use({
geolocation: {
latitude: 37.566535,
longitude: 126.977969,
accuracy: 50,
},
permissions: ["geolocation"],
});
test("shows nearby stores from mocked location", async ({ page }) => {
await page.goto("/stores");
await page.getByRole("button", { name: "현재 위치 사용" }).click();
await expect(page.getByText("현재 위치를 가져왔습니다")).toBeVisible();
});
지도 SDK도 별도로 검증합니다. Geolocation 성공은 지도 타일, 지오코딩, 경로 API, 요금제, API key 제한의 성공을 의미하지 않습니다.
안전한 Claude Code 프롬프트
위치 정보 작업은 범위와 금지 사항을 먼저 써야 합니다. 특히 API key, 결제, 분석 이벤트, 로그는 변경 금지로 두는 편이 안전합니다.
claude <<'PROMPT'
Implement a beginner-friendly Geolocation feature.
Scope:
- Edit only src/features/location and related tests.
- Do not change billing, analytics, or map provider config.
- Preserve existing API keys and environment variable names.
Requirements:
- Request location only after the user clicks a button.
- Explain why location is needed before the browser prompt.
- Use getCurrentPosition with timeout and maximumAge.
- Add manual postcode/address fallback for denied or timeout cases.
- Do not log raw latitude or longitude.
- Add a Playwright test with mocked geolocation.
- Return a short verification checklist.
PROMPT
팀에서는 .claude/settings.json으로 .env 읽기와 자동 push를 막고, 테스트와 lint만 허용하는 식의 운영을 검토할 수 있습니다.
{
"permissions": {
"deny": [
"Read(./.env)",
"Read(./.env.*)",
"Bash(git push *)"
],
"allow": [
"Bash(npm test *)",
"Bash(npm run lint)"
]
}
}
실제 서비스의 위치 기능, 지도 제공자, 개인정보 정책, 테스트 기준을 팀 표준으로 정리하려면 Claude Code training and consultation을 참고하세요. 기존 코드와 실제 사용자 흐름을 기준으로 프롬프트와 리뷰 체크리스트를 만드는 편이 가장 효과적입니다.
직접 검증한 내용
예제는 Chrome의 localhost에서 실행하고 DevTools Sensors로 서울 좌표와 Location unavailable을 확인했습니다. Playwright 예제는 geolocation 권한을 부여한 성공 경로를 검증했습니다. 배포 전에는 OS 위치 서비스 OFF, 기업 관리 브라우저, 권한 거부, iframe의 Permissions-Policy, 지도 API quota, 로그에 원시 좌표가 남지 않는지도 확인해야 합니다.
무료 PDF: Claude Code 치트시트
이메일을 입력하면 명령, 리뷰 습관, 안전한 워크플로를 정리한 PDF를 받을 수 있습니다.
개인정보를 안전하게 관리하며 스팸을 보내지 않습니다.
작성자 소개
Masa
Claude Code 실무 워크플로와 팀 도입을 검증하는 엔지니어입니다.
관련 글
Obsidian 메모를 CLAUDE.md로 바꾸는 Claude Code 워크플로
Obsidian 작업 메모를 CLAUDE.md 운영 노트로 정리해 Claude Code 세션의 문맥 반복을 줄입니다.
Claude Code Revenue CTA Routing: 글에서 PDF, Gumroad, 상담으로 보내기
독자 의도에 따라 무료 PDF, Gumroad 상품, 상담으로 나누는 Claude Code CTA 설계입니다.
Claude Code 팀 인계 규칙: 리뷰 증거, 권한, 롤백, 수익 경로까지 넘기는 법
Claude Code 작업을 팀에 넘길 때 필요한 증거, 권한 규칙, 롤백, 무료 PDF, Gumroad, 상담 경로 체크리스트.