React에서의 IP 지오로케이션: 방문자 위치 컴포넌트
Watch-IP JavaScript SDK를 감싸는 재사용 가능한 useVisitorGeo 훅을 만들어, 실제 React 컴포넌트에 필요한 로딩, 오류, 취소 처리를 구현합니다 — 개발 환경에서 요청이 두 번 발생할 수 있는 이유도 함께 다룹니다.
시작하기 전에
- Watch-IP 계정과, 앱의 오리진(개발용과 프로덕션용 모두)이 오리진 허용 목록에 등록된 공개 가능한 방문자용 키.
- React 18 이상 — 아래에서 설명하는 StrictMode의 이중 호출 동작은 개발 모드에서만 발생합니다.
- @digitload/watch-ip-sdk 설치(npm install @digitload/watch-ip-sdk).
1. useVisitorGeo 훅 만들기
SDK의 getGeo() 호출을 감싸는 훅을 만들어 loading/result/error 상태를 추적하고, 요청이 완료되기 전에 컴포넌트가 언마운트되면 요청을 취소합니다.
TypeScript
import { useEffect, useState } from "react";
import { WatchIP, type GeoResponse } from "@digitload/watch-ip-sdk";
const client = new WatchIP("YOUR_PUBLISHABLE_KEY");
export function useVisitorGeo() {
const [geo, setGeo] = useState<GeoResponse | null>(null);
const [status, setStatus] = useState<"loading" | "ready" | "error">("loading");
useEffect(() => {
const controller = new AbortController();
client
.getGeo({ signal: controller.signal })
.then((result) => {
setGeo(result);
setStatus("ready");
})
.catch((err) => {
if (err.name !== "AbortError") setStatus("error");
});
return () => controller.abort();
}, []);
return { geo, status };
}2. 개발 환경에서 요청이 두 번 발생할 수 있는 이유
React의 StrictMode는 정리(clean up)되지 않는 이펙트를 드러내기 위해 개발 환경에서 각 컴포넌트를 의도적으로 한 번 더 마운트, 언마운트, 재마운트합니다. 즉 이 훅의 이펙트도 두 번 실행되며, 이때 위의 AbortController가 중요해집니다 — 첫 번째 요청은 완료되기 전에 정리 함수에 의해 중단되므로, 실제로 완료되는 getGeo() 호출은 하나뿐입니다. abort 호출을 생략해도 정확성이 깨지지는 않습니다 — GET /v1/geo는 읽기 요청이기 때문입니다 — 하지만 불필요한 진행 중 요청과, 폐기된 첫 번째 인스턴스가 결코 사용하지 않는 상태 업데이트가 남게 됩니다.
3. 로딩, 결과, 대체 상태 렌더링하기
다른 데이터 훅과 동일한 방식으로 이 훅을 사용하며, status가 'loading'을 벗어날 때까지 적절한 기본값을 표시하세요.
TypeScript (JSX)
function RegionBanner() {
const { geo, status } = useVisitorGeo();
if (status === "loading") return <p>지역을 확인하는 중…</p>;
if (status === "error" || !geo?.country) return <p>기본 지역을 표시합니다</p>;
return <p>{geo.city ?? geo.country} 지역에 맞는 콘텐츠를 표시합니다</p>;
}4. 방문자가 명시적으로 선택한 값 유지하기
일반 JavaScript 가이드와 동일한 규칙입니다 — 제안된 country를 적용하기 전에 저장된 선호값이 있는지 먼저 확인하고, 선호값이 아직 없을 때만 훅의 결과로 대체하세요.
TypeScript
const preferred = localStorage.getItem("preferredCountry");
const country = preferred ?? geo?.country ?? "US";오류 및 예외 상황
허용되지 않은 오리진은 코드가 정상 응답을 받기도 전에 실패합니다 — 먼저 앱의 개발용과 프로덕션용 URL을 키의 오리진 허용 목록에 추가하세요. GeoResponse의 모든 필드는 ip와 isEUCountry를 제외하면 null일 수 있으므로, status === 'ready'만으로는 충분하지 않습니다 — 위의 대체 처리가 geo?.country로 하듯이 렌더링하려는 구체적인 필드도 함께 확인하세요.
다음 단계
Next.js를 사용하시나요? Next.js의 서버 렌더링 모델에 따라 이 요청을 안전하게 실행할 수 있는 위치가 달라집니다 — 클라이언트/서버 분리 방식과, 제공된 IP에 대한 시크릿 키 기반 서버 패턴은 Next.js 가이드를 참고하세요.