
개요
개인 프로젝트의 마이페이지에서 경로 검색 기능을 개발하던 중, Tmap 지오코딩 API 호출 시 400 Bad Request 에러가 발생했습니다.
POI 검색 결과를 그대로 API에 전달하면서 특수문자 인코딩 문제와 POI명/주소 혼용 문제가 원인이었습니다. 본 글에서는 이 문제의 원인 분석과 UX를 고려한 해결 과정을 정리하고자 합니다.
🚨 문제 상황
사용자가 출발지와 도착지를 입력하고 경로를 검색했을 때, 티맵 지오코딩 API 호출 시 백엔드 서버에서 아래와 같은 에러 로그와 함께 경로 탐색에 실패하는 현상이 발생했습니다.

org.springframework.web.reactive.function.client.WebClientResponseException$BadRequest:
400 Bad Request from GET https://apis.openapi.sk.com/tmap/geo/fullAddrGeo
- 발생 지점
- 티맵 주소 기반 지오코딩 API 호출 시
- 검색 키워드
- 출발지(송파구 잠실동), 목적지(강남역[2호선])
🔍 원인 분석
1. 특수문자 인코딩 문제
- 티맵 지오코딩 API는 URL 파라미터로 주소를 전달받는데, 대괄호([, ]) 같은 특수문자가
제대로 인코딩되지 않으면 서버가 요청을 거부합니다.- 즉, 장소명에 포함된 [ , ]와 같은 특수문자가 URL 파라미터로 전달되는 과정에서
인코딩 처리가 규격에 맞지 않아 티맵 API 서버에서 잘못된 요청(Bad Request)으로 처리된 것 이었습니다.
- 즉, 장소명에 포함된 [ , ]와 같은 특수문자가 URL 파라미터로 전달되는 과정에서
2. API 성격에 맞지 않는 파라미터 전달
- 티맵의 fullAddrGeo API는 정제된 법정 주소나 도로명 주소를 기반으로 좌표를 변환합니다.
- 그러나 사용자가 선택한 강남역[2호선]은 주소가 아닌 장소명(POI) 데이터입니다. ➡️ 인코딩 실패
SK open API
puzzle 장소 혼잡도 실시간 장소 혼잡도
openapi.sk.com
위 SK open API docs를 보면 다음과 같이 나와있습니다.
[SK Open API 공식 가이드 발췌]
- 기능 설명: "지번 주소 또는 새주소(도로명 주소) 전체를 텍스트로 입력해 이를 분석하여 좌표로 변환
- "Request 필수 파라미터 (fullAddr): "지번 주소 또는 도로명 주소 전체를 지정" (예: 서울시 중구 을지로)
주의 사항: "주소 입력 시 UTF-8 기반의 URL 인코딩 처리 필수"
1. 입력값의 제한
가이드에 명시된 대로 해당 API는 주소(Address)를 입력받도록 설계되어 있습니다. 하지만 제가 전송한 데이터는 강남역[2호선]과 같은 장소명(POI)이었으며, 이는 API의 명확한 사용 목적에서 벗어난 파라미터였습니다.
2. 인코딩 규격 위반
가이드에서는 UTF-8 기반 URL 인코딩을 필수 조건으로 내걸고 있습니다.
특수문자가 포함된 장소명을 별도의 정제나 인코딩 없이 전송할 경우, API 서버는 이를 유효하지 않은 요청(Malformed Request)으로 간주하여 400 Bad Request를 반환하게 됩니다.
결론: POI명 vs 실제 주소
근본적인 문제는 "강남역[2호선]"이 주소가 아니라 POI(Point of Interest) 이름이라는 점입니다. 따라서 결국 데이터 흐름의 구조적인 문제를 해결해야 함을 확인했습니다.
- ❌ 잘못된 흐름: 강남역[2호선] ➡️ 지오코딩 API ➡️ 400 에러
- ✅ 올바른 흐름: POI 검색 ➡️ 실제 주소 획득 ➡️ 지오코딩 API ➡️ 성공
💡 해결 방법
1 단계 : POI 검색 결과에서 전체 주소 활용
POI 검색 API의 응답에는 장소명과 함께 실제 주소 정보가 포함되어 있습니다.

POI 검색 API는 장소명(name)과 함께 해당 장소의 정확한 주소 정보를 함께 제공합니다.
name은 사용자가 읽기 쉬운 형태이지만, 특수문자([2호선])가 포함되어 있고 모호할 수 있습니다. 반면 fullAddress는 시/도부터 상세 주소까지 포함된 완전한 형태로, 지오코딩 API가 정확하게 처리할 수 있는 형식입니다.
수정 전

POI 목록에서 장소를 선택하면 name 필드(예: "강남역[2호선]")를 그대로 입력창에 저장하고 있었습니다.
이 값을 그대로 지오코딩 API에 전달하면 특수문자로 인한 URL 인코딩 문제와 함께, "강남역"이라는 모호한 정보만으로는 정확한 좌표를 얻기 어려워 400 에러가 발생했습니다.
수정 후

name 대신 fullAddress를 사용하도록 변경했습니다.
이제 "서울특별시 강남구 역삼동"과 같이 특수문자가 없고 행정구역 정보가 모두 포함된 정확한 주소가 지오코딩 API로 전달되어, 정상적으로 좌표 변환이 이루어집니다.
수정 후 화면

위 POI 리스트에서 "강남역[2호선]"을 선택하면 아래와 같이 자연스럽게 fullAddress로 바뀌는 것을 볼 수 있습니다.
이후 경로를 검색하면 정상적으로 경로가 나오게됩니다.
🚨 새로운 문제 : UX 저하
문제는 해결되었지만, 새로운 이슈가 발생했습니다.
사용자가 기대하는 것
- 입력창에 "강남역" 표시
실제로 보이는 것
- 입력창에 "서울특별시 강남구 역삼동" 표시
사용자 경험이 크게 저하되었습니다.
최종 해결책 : Label과 Value 분리
프론트엔드에서 표시용 데이터(Label)와 실제 사용 데이터(Value)를 분리하여 관리하는 방식으로 개선했습니다.
1. 상태 구조 개선

기존에는 단순히 문자열 하나만 저장했기 때문에, "사용자에게 보여줄 값"과 "API에 전달할 값"을 동시에 관리할 수 없었습니다.
상태를 객체 형태로 변경하여 name(화면 표시용)과 address(API 전송용)를 별도로 저장할 수 있도록 개선했습니다.
2. POI 선택 시 두 값 모두 저장

사용자가 POI 목록에서 장소를 선택하면, 두 가지 정보를 동시에 저장합니다.
name에는 "강남역[2호선]"처럼 사용자가 선택한 장소명을 저장하여 입력창에 표시하고, address에는 "서울특별시 강남구 역삼동 "처럼 정확한 주소를 저장하여 나중에 API 호출 시 사용합니다.
이를 통해 UX는 유지하면서 기술적 정확성도 확보할 수 있습니다.
3. UI와 API 호출 분리

입력창(<input>)의 value는 startInfo.name을 사용하여 사용자에게 친숙한 장소명("강남역[2호선]")을 보여줍니다.
반면, 검색 버튼을 클릭했을 때는 startInfo.address를 전달하여 백엔드가 정확한 주소로 지오코딩을 수행할 수 있도록 합니다.
이렇게 화면에 보이는 값과 실제 처리되는 값을 분리함으로써, 사용자 경험과 기술적 요구사항을 모두 충족시킬 수 있습니다.
4. 사용자가 직접 입력한 경우 처리

또한 사용자가 POI 목록에서 선택하지 않고 직접 "강남역"이라고 입력한 후 바로 검색 버튼을 누를 수 있습니다.
이 경우 address는 비어있고 name만 존재하게 됩니다. || 연산자를 사용하여 address가 있으면 우선 사용하고, 없으면 name을 대신 사용하도록 fallback 처리를 추가했습니다.
이를 통해 POI 선택과 직접 입력 두 가지 입력 방식을 모두 지원할 수 있습니다.
최종 수정 후 화면

위 POI 리스트에서 "강남역[2호선]"을 선택하면 화면에는 사용자 친화적인 장소명이 그대로 표시되고, 백엔드로는 정확한 fullAddress가 전달되어 정상적으로 지오코딩이 수행됩니다.
핵심 교훈
1. 외부 API 연동 시 데이터 검증 필수
- 외부 API는 예상치 못한 입력에 민감합니다. 특수문자, 인코딩, 데이터 형식 등을 철저히 검증해야 합니다.
2. POI명 ≠ 주소
- POI명: 사용자가 인식하기 쉬운 장소 이름 ("강남역", "스타벅스")
- 주소: 시스템이 처리할 수 있는 정확한 위치 정보
둘의 차이를 명확히 이해하고 적절히 변환해야 합니다.
'Trouble Shooting' 카테고리의 다른 글
| [트러블슈팅] Next.js + Zustand 새로고침 시 로그인 상태 유실 문제 해결하기 (onRehydrateStorage ) (0) | 2026.02.10 |
|---|---|
| [트러블 슈팅] TMAP API 다중 경로 조회 : Mono.zip으로 동시 요청 처리하기 (0) | 2026.02.06 |
| [트러블 슈팅] 할인 계산 로직 개선: 쿠폰 할인 적용 오류 수정 (0) | 2024.10.19 |
| [트러블 슈팅] 사용자 직접 취소 시 다중 결과 반환 트러블 슈팅 (0) | 2024.10.11 |
| [트러블 슈팅] BeanCreationException 빈 충돌 해결을 위한 @ConditionalOnProperty 활용 (0) | 2024.10.11 |