[실전 프로젝트 회고] Daum Postcode API + Kakao Local API를 활용한 위치 기반 검색 기능 구현 / Kakao Maps API 활용하여 지도상에 데이터 보여주기

김하연·2022년 8월 8일

🔗 실전 프로젝트 전체 내용 보러가기


✅ Daum Postcode API + Kakao Local API 도입 이유

메인페이지에서 사용자들이 돌보미 리스트를 검색할 때, 원하는 주소에 등록된 돌보미 리스트를 검색하는 기능을 구현하기 위해 Daum Postcode API를 사용하게 되었다.
그러나 Daum Postcode API에서 반환되는 데이터는 위도, 경도 값을 포함하지 않기 때문에 위도 경도 데이터를 구하기 위해서 추가로 Kakao Local API를 함께 사용하였다.

✅ Kakao Maps 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로 위도, 경도 값을 받아오도록 설정했다.

  1. 검색 결과에 해당하는 데이터 중에서 address의 값을 searchTxt라는 변수에 저장한다.
  2. Kakao Local REST API를 사용하기 위해 header에 인증키 관련 설정을 추가한다.
  3. REST API url로 쿼리스트링에 검색하고자 하는 데이터, 즉 우리 프로젝트에서는 data.address 값을 가지고 있는 searchTxt라는 변수를 전송한다.
  4. API 요청을 통하여 얻은 결과 데이터에서 필요한 정보를 addressInfo라는 state값에 저장하여 부모 컴포넌트(검색 화면)에 적용한다.

🔗 Kakao Developers 공식 문서 참고


onClose : 사용자가 검색 결과를 선택하거나 검색창 브라우저 닫기 버튼을 클릭해 우편번호 검색 서비스가 닫혔을 때 발생하는 이벤트이다.

Home.js 파일 안에 우편번호 검색 서비스 컴포넌트를 import한 뒤, 주소 검색창을 클릭했을 때 iframeDisplay 라는 state의 값이 true가 되어 우편번호 검색 서비스가 보여지도록 컴포넌트를 구성했다.

우편 번호 검색 서비스 아래의 닫기 버튼에 onClick 이벤트를 추가하고 setIframeDisplay(false) 를 통해 state값을 다시 false로 바꿔 우편번호 검색 컴포넌트가 숨겨지도록 처리했는데, 사용자가 검색결과를 선택하여 자동으로 우편번호 검색 서비스가 닫힐 때에는 iframeDisplay의 값을 바꾸지 않으면 계속해서 true로 남아있기 때문에 혹시 모를 에러를 방지하기 위해 iframeDisplay의 값을 false로 바꾸는 내용을 onClose에 추가하였다.

onClose에서 반환되는 변수의 상태값은 FORCE_CLOSE, COMPLETE_CLOSE 두가지이다.

  • FORCE CLOSE : 브라우저 닫기 버튼을 통해 팝업창을 닫았을 경우
  • COMPLETE_CLOSE : 결과값을 선택하여 팝업창이 닫혔을 경우

검색결과를 완료했을 경우 반환되는 값이 'COMPLETE_CLOSE'이므로, 이 경우에 setIframeDisplay(false) 코드를 추가하여 처리했다.

🔗 Daum Postcode API 공식 문서 참고


// 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(검색하고자 하는 주소 데이터)가 모두 빈값이 아닐 경우 검색을 실행하도록 하였다.


✅ Kakao Map에 돌보미 리스트 데이터 반영하고 마커 띄우기

카카오 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;

0개의 댓글