React

[React/TS] 주소 검색 기능 구현하기 / 주소를 위도 경도로 변환하기 / 카카오맵 API 타입 정의

solfa 2024. 7. 30. 18:07

주소 검색 라이브러리 추천

 

유명한 주소 검색 라이브러리들은 다음과 같다.

  1. Google Places API: 구글의 위치 검색 API를 사용하여 자동 완성 주소 검색 기능을 구현할 수 있습니다.
  2. Daum Postcode API: 한국에서 널리 사용되는 주소 검색 API로, 다음(카카오)의 주소 검색 기능을 제공합니다.
  3. 네이버 지도 API: 네이버의 지도 API를 사용하여 주소 검색 기능을 구현할 수 있습니다.
  4. 사용자 정의 주소 목록: 사용자가 직접 주소 목록을 선택하거나 입력하도록 할 수 있습니다.

장점과 단점을 정리해보자면,

 

Google Places API

장점

  • 글로벌 적용 가능: 전 세계 어디서나 사용할 수 있는 주소 검색 기능을 제공한다.
  • 자동 완성 기능: 사용자가 주소를 입력할 때 자동 완성 기능이 작동하여 빠르고 정확한 주소 입력을 도와준다.
  • 지도 통합: Google Maps와 자연스럽게 통합되어 지도 상에서 위치를 확인할 수 있다.
  • 풍부한 데이터: 다양한 장소 정보와 함께 사용할 수 있어 주소 외에도 다양한 장소 정보를 제공할 수 있다.

단점

  • 사용 요금: 일정 수준 이상 사용하면 과금이 발생하므로 비용 관리가 필요하다.
  • 개발 복잡성: API 키 관리 및 설정 등이 필요하다.

Daum Postcode API

장점

  • 한국에 특화: 한국의 주소 체계를 정확하게 반영하여 사용자가 쉽게 주소를 검색할 수 있다.
  • 무료 사용: 대부분의 기능을 무료로 사용할 수 있어 비용 부담이 적다.
  • 간편한 통합: 간단한 설정으로 웹사이트나 앱에 쉽게 통합할 수 있다.
  • 최신 주소 데이터: 지속적으로 업데이트되는 최신 주소 데이터를 제공받을 수 있다.

단점

  • 지역 한정: 한국 이외의 주소 검색에는 사용할 수 없다.
  • 기능 제한: 글로벌 서비스에 비해 제공하는 기능이 제한적일 수 있다.

네이버 지도 API

장점

  • 한국에 특화: 한국의 주소 체계를 정확하게 반영하며, 네이버 지도와 통합되어 위치 검색이 용이하다.
  • 다양한 기능: 지도 검색, 길찾기, 거리 계산 등 다양한 기능을 제공한다.
  • 무료 사용: 기본적인 기능을 무료로 사용할 수 있으며, 일정 수준까지는 무료로 사용할 수 있다.
  • 친숙한 인터페이스: 한국 사용자들에게 익숙한 네이버 지도 인터페이스를 제공한다.

단점:

  • 지역 한정: 한국 이외의 주소 검색에는 사용할 수 없다.
  • API 제한: 요청 횟수나 사용량에 제한이 있을 수 있다.

 

현재 개발중인 프로젝트에서 메인 지도 기능을 구글 맵으로 구현하고 있긴 하지만 국내 서비스이기도 하고 시중에서 주소 입력을 할 때 다음의 우편 번호 검색 서비스를 많이 봐와서 더 익숙한 다음 우편번호 서비스를 사용하기로 했다. 간편로그인도 카카오로 진행하니까 UI적 통일도 쪼금... 되지 않을까 싶은 마음이다 핳


문제상황 및 구현 할 부분

 

주소를 입력하는 기능을 구현해야 한다. 프론트에서 주소를 입력받고 주소와 도로명 주소, 위도와 경도를 서버측으로 보내줘야 한다.

주소 검색을 누르면 주소 검색 창이 뜨는 방식이다.

고민이 있다면 다른 주소 검색 창들을 보면 우편주소, 상세주소 적는 칸이 있는데 그런 부분이 디자인에 없어서 어떻게 처리할지 고민이다.

개발 하면서 더 고민해보자! 긔긔


기능 구현하기

단순한 주소 + 위도, 경도를 알아내야 하기 때문에 두 개의 api를 이용해야 한다.

https://postcode.map.daum.net/guide

 

Daum 우편번호 서비스

우편번호 검색과 도로명 주소 입력 기능을 너무 간단하게 적용할 수 있는 방법. Daum 우편번호 서비스를 이용해보세요. 어느 사이트에서나 무료로 제약없이 사용 가능하답니다.

postcode.map.daum.net

여기에서 1차로 주소를 알아내고

 

https://apis.map.kakao.com/

 

kakao API의 assressSearch로 위도와 경도를 알아낸다.

 

뭔가... 번거롭다 생각이 들지만 일단 해보자!


기능 구현하기 - 주소 가져오기 

웹앱 형식이기 때문에 팝업보다는 iframe 형식이 낫다고 판단했다.

iframe은 웹 안에 또 웹페이지를 삽입하는 형식이다!

 

모바일웹에서는 팝업을 띄우는게 부담스러울 수도 있으니, 아래 코드와 같이 특정 element에 크기를 지정하여 iframe으로 끼워넣는 방식을 이용할 수도 있습니다.

 

라고 카카오도 말하고 있다.

 

또한 예제코드는 바닐라 JS형식으로 나와있기 때문에 react 기반에서 쓰기 쉽게 만들어준 패키지를 사용할 예정이다.

사용한 모듈은 다음과 같다.

https://www.npmjs.com/package/react-daum-postcode

 

react-daum-postcode

Daum Postcode service for React. Latest version: 3.1.3, last published: a year ago. Start using react-daum-postcode in your project by running `npm i react-daum-postcode`. There are 22 other projects in the npm registry using react-daum-postcode.

www.npmjs.com

 

1. 패키지를 설치해준다.

npm install react-daum-postcode

 

설치한 후 

import DaumPostcode from 'react-daum-postcode'

 

이런식으로 불러와서 사용하면 된다!

 

2. 코드를 작성한다.

import { useState } from 'react'
import DaumPostcode from 'react-daum-postcode'
import {
  ModalBackground,
  ModalContent,
  StyledAddressButton,
  StyledAddressInput,
  StyledRowDiv,
} from './AddressSearch.style'

interface AddressSearchProps {
  address: string
  setAddress: (address: string) => void
}

interface DaumPostcodeData {
  address: string
  roadAddress: string
}

export const AddressSearch = ({ address, setAddress }: AddressSearchProps) => {
  const [isModalOpen, setIsModalOpen] = useState(false)
  const [postcodeKey, setPostcodeKey] = useState(0)

  const handleComplete = (data: DaumPostcodeData) => {
    console.log('주소:', data.address)
    console.log('도로명 주소:', data.roadAddress)
    setAddress(data.address)
    setIsModalOpen(false)
    // getCoordinates(data.roadAddress)
  }
  const openModal = () => {
    setIsModalOpen(true)
    setPostcodeKey((prevKey) => prevKey + 1)
  }

  const closeModal = () => {
    setIsModalOpen(false)
  }

  const closeModalOnBackgroundClick = (e: React.MouseEvent) => {
    if (e.target === e.currentTarget) {
      setIsModalOpen(false)
    }
  }

  return (
    <>
      <StyledRowDiv>
        <StyledAddressInput
          type="text"
          placeholder="기본 주소"
          value={address}
          readOnly
        />
        <StyledAddressButton onClick={openModal}>주소 검색</StyledAddressButton>
      </StyledRowDiv>
      {isModalOpen && (
        <ModalBackground
          $isOpen={isModalOpen}
          onClick={closeModalOnBackgroundClick}
        >
          <ModalContent>
            <img
              src="//t1.daumcdn.net/postcode/resource/images/close.png"
              alt="닫기"
              style={{
                cursor: 'pointer',
                position: 'absolute',
                top: 0,
                right: 0,
              }}
              onClick={closeModal}
            />
            <DaumPostcode key={postcodeKey} onComplete={handleComplete} />
          </ModalContent>
        </ModalBackground>
      )}
    </>
  )
}

 

이 부분이 주소를 가져오는 역할을 한다!

            <DaumPostcode key={postcodeKey} onComplete={handleComplete} />

 

위에서 설치한 라이브러리에서 onComplete method가 있는데 이는 사용자가 주소를 선택했을때 발생하는 event값으로 다음과 같은 정보를 받아올 수 있다! 

 

반환되는 데이터

postcode: ""
postcode1: ""
postcode2: ""
postcodeSeq: ""
zonecode: "07027"
address: "서울 동작구 사당로 50"
addressEnglish: "50, Sadang-ro, Dongjak-gu, Seoul, Korea"
addressType: "R"
bcode: "1159010200"
bname: "상도동"
bnameEnglish: "Sangdo-dong"
bname1: ""
bname1English: ""
bname2: "상도동"
bname2English: "Sangdo-dong"
sido: "서울"
sidoEnglish: "Seoul"
sigungu: "동작구"
sigunguEnglish: "Dongjak-gu"
sigunguCode: "11590"
userLanguageType: "K"
query: "숭실대학교 정보과학관"
buildingName: "숭실대학교 정보과학관"
buildingCode: "1159010200105090000020102"
apartment: "N"
jibunAddress: "서울 동작구 상도동 509"
jibunAddressEnglish: "509, Sangdo-dong, Dongjak-gu, Seoul, Korea"
roadAddress: "서울 동작구 사당로 50"
roadAddressEnglish: "50, Sadang-ro, Dongjak-gu, Seoul, Korea"
autoRoadAddress: ""
autoRoadAddressEnglish: ""
autoJibunAddress: ""
autoJibunAddressEnglish: ""
userSelectedType: "R"
noSelected: "N"
hname: ""
roadnameCode: "3119004"
roadname: "사당로"
roadnameEnglish: "Sadang-ro"

 

 

따라서 oncomplete에 함수 하나를 만들어 연결한 뒤 그 함수에서 정보를 가져올 수 있다! 함수 안에서 콘솔을 찍어보면 원하는 값이 정확하게 나온다.

 

타입 설명

interface AddressSearchProps {
  address: string
  setAddress: (address: string) => void
}

 

  • address: 현재 선택된 주소
  • setAddress: 주소 상태를 업데이트하는 함수
interface DaumPostcodeData {
  address: string
  roadAddress: string
}

 

  • address: 전체 주소
  • roadAddress: 도로명 주소

-> 난 두 개만 필요해서 두 개만 타입 지정했다! 더 필요한 게 있다면 추가로 타입 지정하면 될 듯

 

코드 설명

const [isModalOpen, setIsModalOpen] = useState(false)
const [postcodeKey, setPostcodeKey] = useState(0)
  • iframe으로 삽입하려고 했는데 안예뻐서 그냥 모달 구현으로 변경했다.
  • DaumPostcode 컴포넌트를 새로 고침할 때 사용되는 키 -> 위에서 import한 DaumPostcode 컴포넌트를 새로고침할 때 사용하는 상태이다. 주소를 선택한 후 다시 주소를 선택하려고 DaumPostcode 컴포넌트를 열었을 때 오류가 나서 컴포넌트를 열 때 마다 새로고침을 하도록 했다.

handleComplete 함수

const handleComplete = (data: DaumPostcodeData) => {
  console.log('주소:', data.address)
  console.log('도로명 주소:', data.roadAddress)
  setAddress(data.address)
  setIsModalOpen(false)
}
  • DaumPostcodeData 타입의 data를 인자로 받는다. 도로와 도로명 주소!
  • setAddress 함수를 호출하여 주소 상태를 업데이트 + 모달 창을 닫는다.

openModal 함수

const openModal = () => {
  setIsModalOpen(true)
  setPostcodeKey((prevKey) => prevKey + 1)
}

 

  • 모달 창을 열고 postcodeKey를 증가시킨다. 그럼 0에서 1이 된다!

closeModal 함수

const closeModal = () => {
  setIsModalOpen(false)
}

 

  • 모달 창을 닫는다.

closeModalOnBackgroundClick 함수

const closeModalOnBackgroundClick = (e: React.MouseEvent) => {
  if (e.target === e.currentTarget) {
    setIsModalOpen(false)
  }
}

 

  • 모달 창 배경을 클릭했을 때 모달 창을 닫는다. 모달 밖 클릭시 모달 닫히는 기능!
 
 

코드 실행 결과

다음과 같이 콘솔이 잘 찍히는 것을 확인할 수 있다!

 

이제 이 코드를 위도와 경도로 변환할 차례,,,


기능 구현하기 - 주소를 위도와 경도로 변환하기

카카오 map api에는 주소를 위도와 경도로 반환해주는 기능도 있다!

https://developers.kakao.com/product/map

 

Kakao Developers

카카오 API를 활용하여 다양한 어플리케이션을 개발해보세요. 카카오 로그인, 메시지 보내기, 친구 API, 인공지능 API 등을 제공합니다.

developers.kakao.com

제공 기능
로컬 > 좌표계 변환: x, y 값과 입출력 좌표계를 지정하여 변환된 좌표값을 제공합니다.
로컬 > 키워드로 장소 검색: 질의어에 매칭된 장소 검색 결과를 지정된 정렬 기준에 따라 제공합니다.

 

우선 Kakao 지도 API의 타입을 정의해줘야 한다! 자바스크립트일때는 안해봤는데 참 번거롭구만 ;;

 

1. Kakao 지도 API 타입 정의

src 하위에 global.d.ts로 타입을 추가로 정의해준다.

declare global {
  interface Window {
    kakao: any
  }
}

 

대부분 kakao: any 해주면 잘 작동되지만! 현재 프로젝트의 eslint 설정이 강력해서 any를 못 쓰게 해뒀다 핳 (이건 좀 바꿔야 할 듯)

그래서 더 자세하게 타입 정의를 해보고자 한다!

 

https://apis.map.kakao.com/web/documentation/

docs에 보면 매우 자세하게 타입이 나와있다

이걸 참고해서 타입 코드를 작성해보쟈

(노가다) 

declare namespace kakao.maps {
  namespace services {
    class Geocoder {
      addressSearch(
        address: string,
        callback: (result: GeocoderResult[], status: Status) => void,
      ): void
    }

    interface GeocoderResult {
      address_name: string
      y: string
      x: string
    }

    enum Status {
      OK = 'OK',
      ZERO_RESULT = 'ZERO_RESULT',
      ERROR = 'ERROR',
    }
  }

  class LatLng {
    constructor(lat: number, lng: number)
    getLat(): number
    getLng(): number
  }
}

 

설명을 자세히 해보자면

namespace kakao.maps

 

  • Kakao 지도 API의 네임스페이스이다. 모든 Kakao 지도 관련 클래스와 타입을 이 네임스페이스 안에 정의할 예정!
namespace services
  • services는 Kakao 지도 API의 서비스 관련 기능을 담고 있는 네임스페이스! 여기에는 주소 검색을 위한 Geocoder 클래스를 정의할 예정
class Geocode
  • 주소를 위도와 경도로 변환하기 위한 클래스!
declare namespace kakao.maps.services {
  class Geocoder {
    addressSearch(
      address: string,
      callback: (result: GeocoderResult[], status: Status) => void,
    ): void
  }
}
  • addressSearch 메서드는 주소 문자열과 콜백 함수를 인자로 받는다.
  • 콜백 함수는 주소 검색 결과(GeocoderResult 배열)와 상태(Status)를 인자로 받는다.
  • addressSearch 메서드의 시그니처는 Kakao 지도 API의 docs를 참조하여 정의할 수 있어여
interface GeocoderResult
  • Geocoder 클래스의 addressSearch 메서드가 반환하는 결과 배열의 요소 타입이다.
declare namespace kakao.maps.services {
  interface GeocoderResult {
    address_name: string
    y: string
    x: string
  }
}
  • address_name: 검색된 주소의 전체 이름 (위에서 가져온 주소를 여기에 넣어줄거임)
  • y: 위도 값
  • x: 경도 값

 

enum Status
  • addressSearch 메서드의 두 번째 인자로 반환되는 상태 코드이다.
declare namespace kakao.maps.services {
  enum Status {
    OK = 'OK',
    ZERO_RESULT = 'ZERO_RESULT',
    ERROR = 'ERROR',
  }
}
  • OK: 검색이 성공적으로 완료된 상태
  • ZERO_RESULT: 검색 결과가 없는 상태
  • ERROR: 검색 중 오류가 발생한 상태
class LatLng
  • 위도와 경도를 표현하는 클래스!
declare namespace kakao.maps {
  class LatLng {
    constructor(lat: number, lng: number)
    getLat(): number
    getLng(): number
  }
}
  • 생성자: 위도와 경도를 숫자 타입으로 받아 객체를 생성한다.
  • getLat: 위도를 반환하는 메서드
  • getLng: 경도를 반환하는 메서드

사람들이 만들어둔 걸 타입 지정만 하고 갖다 쓰기만 하면 된다!

참... 다들 똑똑하다는 걸 다시 느꼈다.

이런데도 내가 코딩을 해야할까..............????????? ㅜㅜㅜ

 

2. 나머지 코드 작성하기

위에 코드에 위도 경도를 계산하는 함수를 추가한다.

const getCoordinates = (roadAddress: string) => {
    if (window.kakao) {
      const geocoder = new kakao.maps.services.Geocoder()

      geocoder.addressSearch(
        roadAddress,
        function (
          result: kakao.maps.services.GeocoderResult[],
          status: kakao.maps.services.Status,
        ) {
          if (status === kakao.maps.services.Status.OK) {
            const coords = new kakao.maps.LatLng(
              Number(result[0].y),
              Number(result[0].x),
            )
            console.log('위도:', coords.getLat())
            console.log('경도:', coords.getLng())
          } else {
            console.error('Geocoder failed due to:', status)
          }
        },
      )
    }

 

 

이 함수는 도로명 주소를 받아서 해당 주소의 위도와 경도를 얻어오는 역할을 한다.

  • window.kakao 객체가 존재하는지 확인하여 Kakao 지도 API가 로드되었는지 확인한다. 에러 방지라 안해도 실행은 된다!
  • kakao.maps.services.Geocoder 인스턴스를 생성하여 geocoder 변수에 할당한다.
  • geocoder.addressSearch 메서드를 사용해 주소 검색을 수행한다.
  • 검색이 성공적일 때(status === kakao.maps.services.Status.OK) 결과 배열에서 첫 번째 결과의 위도(y)와 경도(x) 값을 사용하여 LatLng 객체를 생성한다.
  • LatLng 객체에서 getLat와 getLng 메서드를 호출하여 위도와 경도를 콘솔에 출력한다.
  const handleComplete = (data: DaumPostcodeData) => {
		...
    getCoordinates(data.roadAddress)
  }

 

 

이제 저 함수를 위에서 만든 handleComplete에 도로명주소와 함께 불러주면 된다!

 

코드 실행 결과

 

굿굿 이제 서버로 잘 보내주면 됨

 

728x90