[119monitoring #4] 카카오맵 API 마커, 오버레이 생성하기

이원진·2023년 8월 14일
0

119monitoring

목록 보기
4/6
post-thumbnail

목차


  1. 서론

  2. axios 통신

  3. 마커로 병원 위치 표시하기

  4. 오버레이로 병원 정보 출력하기

  5. 메모



0. 서론


이번 글에서는 카카오맵 API를 사용해 백엔드에서 받아온 병원 위치를 마커로 표시하고, 오버레이로 정보를 출력하는 기능을 구현해보겠습니다.



1. axios 통신


우선 백엔드에서 프런트엔드로 병원 정보를 가져와야 합니다. React에서는 API와 통신하기 위해 fetch API, axios 등의 HTTP Client 라이브러리를 주로 사용합니다. fetch는 Javascript 내장 라이브러리이기 때문에 별도의 설치가 필요 없다는 장점이 있지만, JSON으로 변환하는 별도의 로직을 구현해야 하고, 지원하지 않는 브라우저가 있다는 단점도 있습니다. 반면 axios는 별도로 설치해야하고 fetch에 비해 느리지만, 보안성과 브라우저 호환성이 더 좋기 때문에 이번 프로젝트에서는 axios를 사용했습니다.

HospitalList.js

import React, { useState, useEffect } from 'react';
import axios from 'axios';
import MapComponent from './MapComponent';

function HospitalList() {
    const [hospitals, setHospitals] = useState([]);
    console.log("entered1")

    useEffect(() => {
        // 백엔드 API에서 병원 목록 데이터 fetch
        axios.get('http://localhost:8000/api/hospitals/')
            .then(response => {
                setHospitals(response.data);
            })
            .catch(error => {
                console.error('데이터를 가져오는 데 실패했습니다:', error);
            });
    }, []);

    return (
        <div>
            <MapComponent hospitals={hospitals} />
        </div>
    );
}

위 코드와 같이 React Hook(useState)에서 axios 통신으로 백엔드의 데이터를 가져와서 hospitals라는 상수에 저장합니다. 이후에 hospitals를 매개변수(prop)로 사용해 MapComponent를 호출합니다.

axios.get('/api/hospitals/')과 같이 사용할 경우, 백엔드의 포트 번호 8000이 아닌 react 서버의 포트 3000을 가리키기 때문에 위 사진과 같이 HTTP 404 에러가 발생합니다. 따라서, axios.get('http://localhost:8000/api/hospitals/‘)과 같이 URL을 전부 명시해서 사용했습니다.

CORS(Cross-Origin Resource Sharing)

위의 코드를 사용해 axios로 백엔드에 정보를 요청하면, 아래 사진과 같이 CORS 정책으로 인해 요청이 거부되었다는 에러 메시지가 출력됩니다.

CORS를 한국말로 직역하면 "교차 출처 자원 공유"라는 뜻인데, 말 그대로 여러 개의 출처에서 자원을 공유하기 때문에 발생하는 문제입니다. 여기서 출처(origin)은 아래 사진과 같이 URL에서 프로토콜, 호스트, 그리고 포트 번호까지의 조합을 의미합니다.

현재 프로젝트에서 백엔드(서버 사이드)의 포트는 8000번이고, 프런트엔드(클라이언트 사이드)의 포트는 3000번으로 둘의 포트가 다르기 때문에 다른 출처로 취급되어 브라우저에서 해당 요청을 거부한 것입니다. Django 측에서 CORS 관련 설정을 추가해 해당 문제를 해결해보겠습니다.



우선, poetry add django-cors-headers 명령어로 라이브러리를 설치합니다. 그 후, 아래 코드와 같이 settings.py의 INSTALLED_APPS에 해당 라이브러리를 추가합니다.

INSTALLED_APPS = [
    ...
    'corsheaders',
    ...
]

다음으로는 아래와 같이 settings.py의 MIDDLEWARE에 corsheader와 관련된 미들웨어를 추가합니다. 목록 중 최상단에 써야한다는 의견도 있고, django.middleware.common.CommonMiddleware 이후에 써야한다는 의견도 있었는데 아래와 같이 최하단에 써도 잘 동작하는 것을 확인했습니다.

MIDDLEWARE = [
    ...
    'corsheaders.middleware.CorsMiddleware',
]

마지막으로 settings.py에 아래와 같이 모든 origin에 대해 요청을 허가하는 설정을 추가합니다. 해당 설정은 개발 환경에서는 아래와 같이 단순하게 정의해도 무관하지만, 프로덕션 환경에서는 특정 origin에 대한 요청만 허가하도록 수정해야합니다.

# Production 환경에서 수정
CORS_ALLOW_ALL_ORIGINS = True

위의 설정들을 추가하면 CORS 에러가 해결되어 프런트엔드에서 백엔드로 성공적으로 요청을 보낼 수 있습니다.


App.js

import React from 'react';
import HospitalList  from './components/HospitalList';

function App() {
  return (
      <div className="App">
        <HospitalList />
      </div>
  );
}

export default App;

npm start로 서버 실행 시 실행되는 App.js에서 HospitalList를 호출하면, HospitalList에서 백엔드의 데이터를 불러와서 MapComponent에 전달해 지도에 병원 위치를 출력하는 구조입니다.



2. 마커로 병원 위치 표시하기


카카오맵 API 명세서 - 마커 생성하기 페이지를 보면, 지도에 마커를 표시하는 샘플 코드를 확인할 수 있습니다. 이를 프로젝트 상황에 맞게 수정해서 사용했습니다.

...
const MapComponent = ({ hospitals = [] }) => {
    useEffect(() => {
    ...

      script.onload = () => {
          ...
          
          const map = new window.kakao.maps.Map(container, options);

          // 병원 위치 마커로 표시
          hospitals.forEach(hospital => {
              const position = new window.kakao.maps.LatLng(hospital.wgs_84_lat, hospital.wgs_84_lon)

              const marker = new window.kakao.maps.Marker({position: position});
              marker.setMap(map);

              ...
              });

          });

          ...
      };
    }, [hospitals]);

    return <div id="kakao-map" style={{ width: '100%', height: '400px' }} />;
};

export default MapComponent;

각 병원에 대한 정보를 담은 hospitals 리스트를 prop으로 전달받으면, foreach문을 사용해 각 병원의 위치를 카카오맵에서 사용하는 형태로 변환한 뒤, 마커의 position으로 설정했습니다. 그 후 marker.setMap() 메서드를 사용해 표시할 맵을 설정하면, 아래 사진과 같이 지도에 총 522개의 병원 위치가 마커로 잘 표시되는 것을 확인할 수 있습니다.



3. 오버레이로 병원 정보 출력하기


단순히 마커로 병원 위치만 표시할 경우 해당 병원의 정확한 위치와 정보를 확인하기 어려울 수 있습니다. 따라서 카카오맵 API에서 제공하는 커스텀 오버레이를 사용해 마커를 클릭할 경우 병원에 대한 정보를 새로운 창으로 출력하고, 닫기 버튼을 누를 경우 창을 닫는 기능을 추가해보겠습니다. 해당 기능에 대한 샘플 코드는 카카오맵 API - 닫기가 가능한 커스텀 오버레이에서 확인할 수 있습니다.

...

const MapComponent = ({ hospitals = [], selectedHospital }) => {
    useEffect(() => {
        ...

        script.onload = () => {
            window.kakao.maps.load(() => {
            ...

            // 병원 위치 마커로 표시
            hospitals.forEach(hospital => {
                ...

                // 마커 클릭 시 오버레이로 병원 정보 표시
                const content = `<div class="custom-overlay">` +
                '    <div class="info">' + 
                '       <div class="header">' + 
                '           <div class="title">' + 
                `               ${hospital.duty_name}` + 
                `               ${hospital.center_type === "0" ? "(응급)" : "(외상)"}` +
                '           </div>' + 
                `            <button class="close" title="닫기">X</button>` + 
                '        </div>' +
                '        <div class="body">' +
                '            <div class="desc">' + 
                `                <div class="address">${hospital.duty_addr}</div>` + 
                `                <div class="representitive-tel">대표: ${hospital.duty_tel1}</div>` + 
                `                <div class="er-tel">응급실: ${hospital.duty_tel3}</div>` +
                '            </div>' + 
                '        </div>' + 
                '    </div>' +
                '</div>';

                const overlay = new window.kakao.maps.CustomOverlay({
                    position: position,
                    content: content,
                    yAnchor: 1.35
                });

                // 오버레이 닫기 기능 추가
                window.kakao.maps.event.addListener(marker, 'click', function(){
                    overlay.setMap(map);

                    const tempDiv = document.createElement('div');
                    tempDiv.innerHTML = content;

                    const closeBtn = tempDiv.querySelector('.close');
                    closeBtn.addEventListener('click', () => {
                        overlay.setMap(null);
                    });

                    overlay.setContent(tempDiv);
                });
            });

            ...
            });
        };
    }, [hospitals]);

    return <div id="kakao-map" />;
};

export default MapComponent;

각 마커에 대해 오버레이를 생성하기 위해 foreach문 안에서 content를 정의한 뒤, 카카오맵의 커스텀 오버레이를 생성했습니다. 그 후, 마커를 클릭하면 오버레이가 켜지고 닫기 버튼을 누르면 오버레이가 꺼지도록 event listener를 등록했습니다. 닫기 기능의 경우, 명세서에서는 closeOverlay()라는 함수를 정의한 뒤 content 내에서 button의 onclick으로 등록했지만, 저의 경우 foreach문 내부에서 오버레이를 생성하기 때문에 foreach문 외부에 있는 closeOverlay() 함수를 인식하지 못했습니다.

overlay.getContent() 메서드를 사용하면 content는 알 수 있지만, element가 아닌 문자열 형식으로 반환하기 때문에 버튼 element를 찾는 것이 어려웠습니다. 따라서, tempDiv라는 임시 div를 생성해 원본 content를 복사하고, tempDiv에서 ".close"라는 이름을 갖는 element(button)를 찾아서 닫는 이벤트를 등록한 뒤 오버레이의 content를 tempDiv로 덮어씌우는 방법으로 해결했습니다.

content 내부에서 템플릿 리터럴을 사용해 hospital.center_type의 값이 0이면 "(응급)"을, 그 외의 값이면 "(외상)"을 출력했는데, hospital.center_type === "0" 대신 hospital.center_type === 0을 사용했더니 항상 "(외상)"만 출력되었습니다. 이는 Javascript의 ==은 Equal Operator이고, ===은 Strict Equal Operator 즉, 엄격한 Equal Operator로 타입이 다를 경우 다른 값으로 취급하기 때문에 발생한 문제였습니다. hospital.center_type == 0과 같이 ==을 사용하면 타입이 다르더라도 의도대로 동작하지만, Javascript에서는 == 대신 ===을 사용할 것을 권장하기 때문에 위와 같이 코드를 작성했습니다.

서버를 실행해 마커를 클릭하면 아래 사진과 같이 해당 병원의 정보가 오버레이로 출력되고, 닫기 버튼을 누르면 오버레이가 잘 닫히는 것을 확인할 수 있습니다.



4. 메모


  • .toml, package.json과 같이 패키지 관리 파일의 경우, hash값 등의 정보를 포함하기 때문에 Github에 올리지 않는 것이 좋음

    • 가상환경(venv)를 같이 올리는 것이 아니라면 어차피 clone해도 실행하지 못하며, 용량만 잡아먹음

  • 기존에는 프로젝트의 데이터 파이프라인 관련 코드와 웹 구현 코드를 하나의 Repo에 같이 저장했는데, 관리의 용이성을 위해 별도의 Repo로 분리함

    • 이 과정에서 브랜치를 삭제한 뒤 git init 다시하고 remote 제거해도 git 연결이 초기화되지 않음

      • 로컬 폴더를 새로 생성해 파일 복사한 뒤 새로운 repo를 remote으로 설정해서 해결

      • 복사하는 과정에서 venv가 깨져서 PYTHONPATH를 인식하지 못해 venv 다시 잡고 poetry update package로 패키지 다시 설치함

    • git-filter-branch라는 기능을 사용하면 Repo를 쉽게 분리할 수 있음

0개의 댓글