들어가며
수행 과제로 날씨 애플리케이션을 개발하게 되었고, 요구 기능 중 다음 두 가지를 해결해야 했다.
1. 사용자의 현재 위치 정보(위/경도, 지역명)와 날씨 정보를 출력
2. 지역명을 검색하여 해당 지역의 날씨 정보를 출력
사용자의 현재 위치는 브라우저에서 기본 제공하는 Geolocation API를 사용하면 확인할 수 있다.
Geolocation API 사용하기 - Web API | MDN
developer.mozilla.org
Geolocation API 예제
navigator.geolocation.getCurrentPosition((position) => {
doSomething(position.coords.latitude, position.coords.longitude);
});
위/경도는 확인했으니 날씨 정보를 가져올 수 있는 수단이 필요한데,
나는 기상청 API를 사용하기로 했다.
그런데 문제가 하나 있다.
지역명을 출력해야 하는데 이게 생각보다 골칫거리다.
Geolocation API는 현재 사용자의 위/경도를 알려줄 뿐이지 지역명은 알 수 없고
검색으로 지역명을 입력받는데 지역명만으로는 위/경도를 구할 수가 없다.
때문에 임의로 지역명과 위/경도를 매핑해서 String 뭉텅이(Array)로 만들어서 JSON으로 저장하고 있었다.
어느 정도 문제가 해결 됐지만 좋은 방법은 아니다.
지역명과 위/경도가 강하게 결합된 상태이기 때문에 지역명이 바뀌게 되면 그에 맞게 수정이 필요하다.
단순 예시
[
{
district: "서울특별시-종로구-세종로"
lat: 32.xxxx
lon: 127.xxxx
},
{
district: "서울특별시-종로구-청운동"
lat: 32.xxxx
lon: 127.xxxx
},
{ ... },
...
]
1건, 2건이면 모르겠지만 2만 건이 넘어가는 Array 안에서 데이터를 관리하기란 비효율적이다.
해결책을 찾기 위해 알아본 바에 따르면 카카오맵, 네이버지도, 구글지도 등이 제공하는 API를 활용하면
위/경도로 지역명을 얻거나 반대의 경우도 가능하다고 한다.
마침 카카오 API를 몇 번 사용해 봐서 익숙하기 때문에 카카오맵을 사용하기로 했다.
내용
#1. 카카오맵 API 설정
좌표값을 파라미터로 받아서 대략적인 지역 정보를 제공하는 API이다.
사용하기 위해서는 REST API 키가 필요하며 Kakao Developers에서 등록한 애플리케이션에 대해
카카오맵의 사용 여부를 ON으로 변경해야 한다.
※ REST API 키 발급 과정은 생략

#2. 좌표로 행정구역정보 변환
카카오의 coord2regioncode API를 사용하면 위도와 경도를 기반으로 행정구역 정보를 조회할 수 있다.
카카오맵 API 문서를 읽어보면 어렵지 않게 사용법을 알 수 있다.
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
HTTP GET method를 사용하며 url query param으로 x와 y값을 필수로 받게 되는데
x는 경도(longitude)에 해당하고 y는 위도(latitude)에 해당한다.
요청(request)
curl -v -G GET "https://dapi.kakao.com/v2/local/geo/coord2regioncode.json?x=127.1086228&y=37.4012191" \
-H "Authorization: KakaoAK ${REST_API_KEY}"
카카오 서버가 요청을 정상 처리(200)하면 법정동과 행정동으로 구분되는 지역 데이터를 응답으로 내려준다.
응답(response)
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"meta": {
"total_count": 2
},
"documents": [
{
"region_type": "B",
"address_name": "경기도 성남시 분당구 삼평동",
"region_1depth_name": "경기도",
"region_2depth_name": "성남시 분당구",
"region_3depth_name": "삼평동",
"region_4depth_name": "",
"code": "4113510900",
"x": 127.10459896729914,
"y": 37.40269721785548
},
{
"region_type": "H",
"address_name": "경기도 성남시 분당구 삼평동",
"region_1depth_name": "경기도",
"region_2depth_name": "성남시 분당구",
"region_3depth_name": "삼평동",
"region_4depth_name": "",
"code": "4113565500",
"x": 127.1163593869371,
"y": 37.40612091848614
}
]
}
#2. 주소로 좌표 변환
검색으로 입력된 지역명으로 좌표를 구하기 위한 search/address API이다.
마찬가지로 카카오 API 문서에 사용법이 잘 작성되어 있다.
Kakao Developers
카카오 API를 활용하여 다양한 어플리케이션을 개발해 보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.
developers.kakao.com
HTTP GET method를 사용하며 API 키를 제외하면 별도의 필수 값은 없다.
요청
curl -v -G GET "https://dapi.kakao.com/v2/local/search/address.json" \
-H "Authorization: KakaoAK ${REST_API_KEY}" \
--data-urlencode "query=전북 삼성동 100"
카카오 서버가 요청을 정상 처리(200)하면 법정동과 행정동으로 구분되는 지역 데이터를 응답으로 내려준다.
검색 결과 중 가장 일치율이 높은 첫 번째 데이터를 기준 좌표로 사용했다.
응답
HTTP/1.1 200 OK
Content-Type: application/json;charset=UTF-8
{
"meta": {
"total_count": 4,
"pageable_count": 4,
"is_end": true
},
"documents": [
{
"address_name": "전북 익산시 부송동 100",
"y": "35.97664845766847",
"x": "126.99597295767953",
"address_type": "REGION_ADDR",
"address": {
"address_name": "전북 익산시 부송동 100",
"region_1depth_name": "전북",
"region_2depth_name": "익산시",
"region_3depth_name": "부송동",
"region_3depth_h_name": "삼성동",
"h_code": "4514069000",
"b_code": "4514013400",
"mountain_yn": "N",
"main_address_no": "100",
"sub_address_no": "",
"x": "126.99597295767953",
"y": "35.97664845766847"
},
"road_address": {
"address_name": "전북 익산시 망산길 11-17",
"region_1depth_name": "전북",
"region_2depth_name": "익산시",
"region_3depth_name": "부송동",
"road_name": "망산길",
"underground_yn": "N",
"main_building_no": "11",
"sub_building_no": "17",
"building_name": "",
"zone_no": "54547",
"y": "35.976749396987046",
"x": "126.99599512792346"
}
},
...
]
}
#3. 구현
사용 예시를 확인했으니 직접 구현해 보겠다.
먼저 API를 처리하기 위한 axios를 정의한다.
카카오 API 뿐만 아니라 기상청 API도 함께 처리하도록 만들기 위해 타입도 정의해서 적용한다.
API Base URL 기준 API 타입
const API_BASE_URL = {
weather: "/api",
kakao: "/api/kakao",
} as const;
export type ApiClientName = keyof typeof API_BASE_URL;
axios 정의
/**
* 공통 Axios 인스턴스
*/
const createAxiosInstance = (baseURL: string): AxiosInstance => {
const instance = axios.create({
baseURL,
timeout: 5000,
});
// 요청 인터셉터
instance.interceptors.request.use((config: InternalAxiosRequestConfig) => {
config.params = {
...config.params,
};
return config;
});
// 응답 인터셉터
instance.interceptors.response.use(
(response) => response,
(error: AxiosError) => {
if (error.response) {
console.error("API Error: ", error.response?.status, error.response.data);
} else if (error.request) {
console.error("Network Error: ", error.message);
} else {
console.error("Unexpected Error: ", error.message);
}
return Promise.reject(error);
},
);
return instance;
};
axios 반환 팩토리
const apiClients: Record<ApiClientName, AxiosInstance> = {
weather: createAxiosInstance(API_BASE_URL.weather),
kakao: createAxiosInstance(API_BASE_URL.kakao),
};
/**
* axios 객체 반환 (weather / kakao 택 1)
* @param name
* @returns
*/
export const getApiClient = (name: ApiClientName): AxiosInstance => {
return apiClients[name];
};
이후 2개의 카카오 API를 호출할 수 있는 fetch 메서드를 정의한다.
좌표로 행정구역정보 변환 fetch
// axios
const kakaoApiClient = getApiClient("kakao");
export const fetchRegionNameFromCoord = async ({ lat, lon }: LatLon): Promise<string> => {
const response = await kakaoApiClient.get<KakaoCoord2RegionResponse>("coord2regioncode", {
params: {
x: lon,
y: lat,
input_coord: "WGS84",
},
});
const region =
response.data.documents.find((document) => document.region_type === "B") ??
response.data.documents[0];
if (!region) {
throw new Error("좌표에 해당하는 지역명을 찾을 수 없습니다.");
}
return [region.region_1depth_name, region.region_2depth_name, region.region_3depth_name]
.filter(Boolean)
.join(" ");
};
주소로 좌표 변환 fetch
const kakaoApiClient = getApiClient("kakao");
/**
* 지역명으로 위/경도 조회 api
* @param region
* @returns
*/
export const fetchLatLonByRegion = async (region: string): Promise<LatLon> => {
const response = await kakaoApiClient.get<KakaoAddressSearchResponse>("search/address", {
params: {
query: region,
},
});
// 검색 결과 중 일치률이 가장 높은 1번째
const target = response.data.documents[0];
if (!target) {
throw new Error("지역 좌표를 찾지 못했습니다.");
}
const lon = Number(target.x);
const lat = Number(target.y);
if (!Number.isFinite(lat) || !Number.isFinite(lon)) {
throw new Error("유효하지 않은 좌표 응답입니다.");
}
return { lat, lon };
};
#4. 사용
이제 정의한 fetch 메서드들을 적절한 위치에서 호출하면 된다.
메인 화면에서 현재 지역명을 바로 필요로 하기 때문에 Geolocation으로 얻은 좌표로 행정구역정보 변환 fetch를 호출한다.
좌표로 행정구역정보 변환 fetch 호출
export default function MainPage() {
...
const [currentRegionName, setCurrentRegionName] = useState("");
const [currentRegionError, setCurrentRegionError] = useState("");
useEffect(() => {
...
let ignore = false;
const loadCurrentRegionName = async () => {
try {
// Geolocation으로 위/경도 조회
const userLocation = await getUserLocation();
// 좌표로 행정구역정보 변환 fetch 호출
const regionName = await fetchRegionNameFromCoord(userLocation);
if (!ignore) {
// 행정구역정보 조회 성공(200)
setCurrentRegionName(regionName);
setCurrentRegionError("");
}
} catch (fetchError) {
// 행정구역정보 조회 실패(400, 500, ...)
if (!ignore) {
console.warn("현재 위치의 지역명을 불러오지 못했습니다.", fetchError);
setCurrentRegionName("");
setCurrentRegionError("현재 위치를 확인하지 못했습니다. 검색으로 지역을 선택해 주세요.");
}
}
};
void loadCurrentRegionName();
return () => {
ignore = true;
};
}, [displayDistrict]);
return (
/** 적절한 형태로 출력 **/
<div>{currentRegionName}</div>
)
}
지역명 검색은 필요하면 호출하기 때문에 별도의 메서드에서 호출한다.
주소로 좌표 변환 fetch 호출
/**
* 지역명으로 위/경도 반환
* - 캐싱 데이터 우선 반환
* @param region
* @returns
*/
const getLatLonByRegion = async (region: string): Promise<LatLon> => {
const cacheKey = getGeoCacheKey(region);
const cached = getStorageCache<LatLon>(cacheKey, GEO_CACHE_TTL);
if (cached) {
return cached;
}
// fetch 호출
const latLon = await fetchLatLonByRegion(region);
setStorageCache(cacheKey, latLon);
return latLon;
};
이로써 현재 사용자의 위/경도, 지역명, 검색된 지역의 위/경도를 모두 얻을 수 있게 됐다.
지역명과 위/경도가 강하게 결합해 관리하기 비효율적인 구조의 JSON도 정리할 수 있었다.
JSON 정리 결과 예시
[
"서울특별시 종로구 청운동",
"서울특별시 종로구 세종로",
"...",
...
]
#5. 생성된 데이터로 기상청 API 날씨 정보 가져오기
기상청 API는 무료로 사용할 수 있는 API이고 사용자 위치를 파라미터에 담아서 보내면 다양한 정보를 얻을 수 있다.
대신에 위치 정보는 위/경도 대신에 지도에서 5x5km의 격자무늬를 표시한 수치예보 모델 격자 좌표를 사용한다.
이는 한 지점을 예보하는 것이 아니라 일정 면적을 대표하는 예보를 만들기 위함이라고 한다.
※ 별도 제공하는 계산식으로 parsing 필요
이해를 돕기 위해 아래의 그림을 준비했다.(Notion 최고)
흐름은 사용자 현재 위치 정보로 날씨 정보 가져오는 흐름이다.

다음은 검색으로 지역명을 입력받아 위/경도를 조회하고 날씨 정보를 가져오는 흐름이다.

#6. 결과
Github Repo: https://github.com/J-mung/My_Weather_Bot
GitHub - J-mung/My_Weather_Bot: 기상청 API를 활용한 날씨 예보 애플리케이션
기상청 API를 활용한 날씨 예보 애플리케이션. Contribute to J-mung/My_Weather_Bot development by creating an account on GitHub.
github.com



정리
이번 작업을 통해
- 위치 기반 서비스 구현에서 발생하는 좌표 <-> 지역명 변환 문제
- 외부 지도 API를 호 라용 한 데이터 관리 개선
- API 책임 분리 설계
등을 경험할 수 있었다.
특히 정적인 데이터 매핑 방식에서 API 기반 동적 조회 구조로 전환한 점이
가장 큰 개선 포인트 갔다.
'Web' 카테고리의 다른 글
| [Tailwind] inline style처럼 보이는 Tailwind를 cn, cva로 구조화 하기 (0) | 2026.03.11 |
|---|---|
| [Next.js] 스트리밍(Streaming)과 스켈레톤(Skeleton) - loading.txs, Suspense (0) | 2026.02.20 |
| [Next.js] App Router에서 error.tsx를 쓰다가 try/catch로 돌아온 이유 (0) | 2026.02.16 |
| [Next.js] App Router 데이터 fetching 구조와 에러 핸들링 (0) | 2026.02.16 |
| [React] useRef()로 DOM 참조하기 (0) | 2026.02.13 |