메인페이지에서 사용자들이 돌보미 리스트를 검색할 때, 원하는 주소에 등록된 돌보미 리스트를 검색하는 기능을 구현하기 위해 Daum Postcode API를 사용하게 되었다.
그러나 Daum Postcode API에서 반환되는 데이터는 위도, 경도 값을 포함하지 않기 때문에 위도 경도 데이터를 구하기 위해서 추가로 Kakao Local API를 함께 사용하였다.
메인 페이지에서 검색된 돌보미 리스트를 보는 방식은 기본적으로 리스트 형태이지만, 리스트 보기와 지도 보기 버튼을 통해 리스트 표기 방식 변경이 가능하다. 지도 보기 버튼을 클릭했을 때, 검색된 돌보미들의 위치와 정보를 지도상에 표기해주기 위해 위도 경도 데이터로 마커 추가가 가능한 Kakao Maps API를 사용했다.
Daum Postcode API를 간단하게 임베드해서 사용할 수 있는 'react-daum-postcode' 라이브러리를 설치해서 사용했다.
// SearchAddress.js (Homne.js의 하위 컴포넌트)
const SearchAddress = ({ setAddressInfo, setIframeDisplay }) => {
const handleClose = (data) => {
// 검색결과를 선택하여 화면이 닫혔을 경우 iframeDisplay값 false로 변경
if(data === 'COMPLETE_CLOSE'){
setIframeDisplay(false);
}
}
const handleComplete = (data) => {
const searchTxt = data.address; // 검색한 주소
const config = { headers: {Authorization : `KakaoAK ${process.env.REACT_APP_KAKAO_RESTAPI}`}}; // 헤더 설정
const url = 'https://dapi.kakao.com/v2/local/search/address.json?query='+searchTxt; // REST API url에 data.address값 전송
axios.get(url, config).then(function(result) { // API호출
if(result.data !== undefined || result.data !== null){
if(result.data.documents[0].x && result.data.documents[0].y) {
// Kakao Local API로 검색한 주소 정보 및 위도, 경도값 저장
setAddressInfo({
address_name: result.data.documents[0].address.address_name,
region_2depth_name: result.data.documents[0].address.region_2depth_name,
x: result.data.documents[0].x,
y: result.data.documents[0].y,
})
}
}
})
}
return (
<DaumPostcodeEmbed
onComplete={handleComplete}
onClose={handleClose}
style={{ width: "100%"}}
useBannerLink={false}
/>
);
};
export default SearchAddress;
onComplete : 검색 결과를 선택했을 때 발생하는 이벤트로, 여기서 검색 결과를 활용해 Kakao Local API로 위도, 경도 값을 받아오도록 설정했다.
onClose : 사용자가 검색 결과를 선택하거나 검색창 브라우저 닫기 버튼을 클릭해 우편번호 검색 서비스가 닫혔을 때 발생하는 이벤트이다.

Home.js 파일 안에 우편번호 검색 서비스 컴포넌트를 import한 뒤, 주소 검색창을 클릭했을 때 iframeDisplay 라는 state의 값이 true가 되어 우편번호 검색 서비스가 보여지도록 컴포넌트를 구성했다.
우편 번호 검색 서비스 아래의 닫기 버튼에 onClick 이벤트를 추가하고 setIframeDisplay(false) 를 통해 state값을 다시 false로 바꿔 우편번호 검색 컴포넌트가 숨겨지도록 처리했는데, 사용자가 검색결과를 선택하여 자동으로 우편번호 검색 서비스가 닫힐 때에는 iframeDisplay의 값을 바꾸지 않으면 계속해서 true로 남아있기 때문에 혹시 모를 에러를 방지하기 위해 iframeDisplay의 값을 false로 바꾸는 내용을 onClose에 추가하였다.
onClose에서 반환되는 변수의 상태값은 FORCE_CLOSE, COMPLETE_CLOSE 두가지이다.
검색결과를 완료했을 경우 반환되는 값이 'COMPLETE_CLOSE'이므로, 이 경우에 setIframeDisplay(false) 코드를 추가하여 처리했다.
// Home.js (SearchAddress.js의 부모 컴포넌트)
useEffect(()=>{
// 주소, 날짜 모두 선택했을 경우 검색 실행
if(dates.length && addressInfo){
queryClient.invalidateQueries('sitter_list');
setQueriesData({searchDate: dates, region_2depth_name: addressInfo.region_2depth_name, x: addressInfo.x, y: addressInfo.y, category});
setSearched(true); // searched 값이 true일 경우 돌보미 검색하는 API 요청 실행
}
}, [dates, addressInfo])
부모 컴포넌트에서는 useEffect의 의존성 배열에 dates와 addressInfo의 값이 변하는 것을 인지하여 dates(검색하고자 하는 날짜 데이터), addressInfo(검색하고자 하는 주소 데이터)가 모두 빈값이 아닐 경우 검색을 실행하도록 하였다.
카카오 Map API 또한 'react-kakao-maps-sdk'를 설치하여 사용하였다.
const MapContainer = ({ centerElement, showOnly, items, _height, setSitterCardShow }) => {
return (
<>
<Map
center={{ lat: centerElem.y, lng: centerElem.x }}
style={{ width: "100%", height: _height ? _height : "360px" }}
onZoomChanged={(map) => setLevel(map.getLevel())}
draggable={showOnly ? false : true}
level={level}
disableDoubleClickZoom={true}
>
{showOnly ? (
<Circle
center={{
lat: centerElem.y,
lng: centerElem.x,
}}
radius={50}
strokeWeight={2} // 선의 두께
strokeColor={"#FC9215"} // 선의 색
strokeOpacity={1} // 선의 불투명도, 1에서 0 사이의 값
strokeStyle={"normal"} // 선의 스타일
fillColor={"#FC9215"} // 채우기 색
fillOpacity={0.4} // 채우기 불투명도
/>
) : (
<>
<ZoomControl />
<MarkerClusterer
averageCenter={true} // 클러스터에 포함된 마커들의 평균 위치를 클러스터 마커 위치로 설정
minLevel={6} // 클러스터 할 최소 지도 레벨
level={3}
disableClickZoom={true}
>
{positions.map((pos, idx) => (
<CustomOverlayMap
key={`pos_${idx}`}
position={{
lat: pos.y,
lng: pos.x,
}}
image={{
src: marker,
size: {
width: 40,
height: 47,
}, // 마커이미지의 크기입니다
options: {
offset: {
x: 20,
y: -47,
}, // 마커이미지의 옵션입니다. 마커의 좌표와 일치시킬 이미지 안에서의 좌표를 설정합니다.
},
style: {textAlign: 'center', display: 'flex', justifyContent: 'center'}
}}
>
<div style={{textAlign: 'center', transform: 'translate(0, -47px)'}}>
<div style={{display: 'flex', alignItems: 'center', height: '40px', padding: '0 15px', background: '#fff', borderRadius: '20px', border: '1px solid #FC9215', boxSizing: 'border-box'}} onClick={()=>markerClickEvent(idx, pos.index)}>
<strong style={{fontWeight: 700}}>{pos.sitterName}</strong>
<span style={{fontSize: '14px'}}><img src={star} alt="star" style={{display: 'inline-block', width: '13px', height: '13px', verticalAlign: 'middle', margin: '-3px 1px 0 5px'}}/>{pos.averageStar}</span>
</div>
<img src={marker} alt="star" style={{width: '40px', height: '47px', margin: '2px 0 0'}}/>
</div>
</CustomOverlayMap>
))}
</MarkerClusterer>
</>
)}
</Map>
</>
);
};
export default MapContainer;