먹고 기도하고 코딩하라

카카오 지도 API로 엑셀 파일의 시설물 위치 지도에 마커 찍기 본문

개발일지

카카오 지도 API로 엑셀 파일의 시설물 위치 지도에 마커 찍기

2G Dev 2020. 9. 23. 22:17
728x90
728x90

코드 repo 주소 (README.md에 적힌대로 사용하면 정상 사용 가능)

 

 

dev-dain/Map-The-Seoul

이 길.. 왜 본 거 같지? Contribute to dev-dain/Map-The-Seoul development by creating an account on GitHub.

github.com

 

저번 여름방학에 대학생 구청 아르바이트에 지원했다가 운 좋게 당첨되어 1달 일하게 됐다.

내가 맡은 일은 기존에 시설물의 위치를 정리해둔 엑셀 파일을 보고 시설물의 위치를 지도에서 찾아 로드뷰로 확인하고 위도와 경도를 찍어 새로운 엑셀 파일 데이터를 만드는 일이었다.

첫 주는 긴장돼서 바짝 했는데 생각보다 일이 빠르게 끝났다. 손은 눈보다 빠르다(?)

 

 

개발 이유

작업을 진행하다보니 불편한 점이 두 가지 있었다.

첫 번째로, 구글 지도와 카카오 지도를 번갈아 보면서 작업을 해야 했다. 이유는 카카오 지도는 위치를 찍어도 바로 위도와 경도를 보여주지 않기 때문이었다. 그래서 일단 카카오 지도로 로드뷰를 싹 보고 나서 구글 지도로 똑같은 위치를 찾아 구글 지도에서 위도와 경도를 찍어야 하는 번거로움이 있었다.

두 번째로, 대체로 도로를 따라 일렬로 시설물이 배치되어 있긴 했지만 가끔 다른 동을 갔다가 다시 작업하던 동으로 돌아왔을 때 왠지 본 것 같은 길이 계속 나왔다. 고개를 갸웃거리면서 뭔가 이건 아닌 것 같은데... 싶으면서도 일단 작업을 했다.

뭔가 이렇게 하면 대충 일하는 것 같아서 마음이 상당히 불편했다.

 

 

착수 - 1. 엑셀 파일 JSON으로 파싱

알바를 시작한 지 3일째 되는 날 집에 돌아와서 프로그램을 짜기 시작했다.

목적은 오로지 생산성 향상이었다. 빠르게 개발해서 얼른 쓰는 게 중요했다. 그래서 노드와 npm 모듈 몇 개 제외하고는 딱히 사용한 것도 없었고, 코드도 복잡하지 않다. 컴퓨터에 노드만 설치하고 깃허브 repo를 그대로 받아서 로컬호스트에서 노드 프로세스를 돌리기만 하면 바로 사용할 수 있게 만들었다.

 

엑셀 파일의 가로행(행과 열 구분은 맨날 헷갈린다. 가로열인가?)에 적힌 '동명/소재지도로명주소/설치장소/위도/경도' 셀의 세로줄 데이터들을 추출해 위치-위도-경도가 하나의 레코드를 이루도록 데이터를 구성하는 코드를 짰다.

엑셀 파일을 다루는 건 처음 해봤기 때문에 xlsx 라는 모듈을 설치해서 사용해봤다. 전부 function으로 만들어서 module.exports를 했는데 이유는 다른 js 파일에서 바로 require해서 쓰기 때문이다.

//excel-read.js
function parseExcel() {
  const xlsx = require('xlsx');
  const fs = require('fs');
  const excel = xlsx.readFile('map.xlsx');
  const sheetName = excel.SheetNames[0];
  const firstSheet = excel.Sheets[sheetName];
  const jsonData = xlsx.utils.sheet_to_json(firstSheet, { defval : "" });

  const latlng = jsonData.map((data, index) => {
    return { 
      dong: data['동명'],
      index: index + 2,
      //실제 엑셀 파일에서는 A1 이런 식으로 1번부터 시작하는 데다가
      //1번은 '동명', '위도' 등의 정보 셀이므로 2번부터 시작하기 위해 +2함
      roadName: data['소재지도로명주소'],
      name: data['설치장소'],
      lat: data['위도'], 
      lng: data['경도']
    }
  });

  fs.writeFile('./public/latlng.json', JSON.stringify(latlng), 'utf8', () => {
    console.log('Done');  
  });
}

module.exports = parseExcel;
동명 소재지도로명주소 설치장소 위도 경도
삼양동 삼양로538-6 라멘집 앞 36.512359 126.351385
삼양동 삼양로550 샤브샤브집 앞 36.523520 126.424616
삼양동 삼양로530 국밥집 앞 36.549428 126.326816
삼양동 삼양로541 카페 앞 36.538256 126.392360

 

뭐 이런 식의 데이터가 있다면 latlng.json에는 다음과 같은 내용이 담기게 된다. 참고로 위 데이터는 내가 수집한 데이터는 아니고 주소는 다 내가 자주 가는 학교 앞 식당들이다.

//laglng.json
[{
  'dong': '삼양동',
  'index': 2,
  'roadName': '삼양로538-6',
  'name': '라멘집 앞',
  'lat': 36.512359,
  'lng': 126.351385
}, 
{
  'dong': '삼양동',
  'index': 3,
  'roadName': '삼양로550',
  'name': '샤브샤브집 앞',
  'lat': 36.523520,
  'lng': 126.424616
}, ...
]

 

 

그리고 처음에 작업할 때는 엑셀 시트 하나씩만 작업했지만, 엑셀 시트가 늘어나면서 그 시트 정보를 전부 하나로 통합하고 싶다는 생각에 여러 시트 전용 파일을 또 만들었다.

//multi-excel-read.js
const xlsx = require('xlsx');
const fs = require('fs');
const excel = xlsx.readFile('map.xlsx');
const sheetName = excel.SheetNames;
const sheets = [];
const jsonData = [];
let latlng = [];

sheetName.map(sheet => sheets.push(excel.Sheets[sheet]));

sheets.map(sheet => 
  jsonData.push(xlsx.utils.sheet_to_json(sheet, { defval : "" })));

jsonData.forEach((arr, dong) => {
  arr.forEach((data, index) => {
    latlng.push({ 
      dong: sheetName[dong],
      index: index + 2,
      roadName: data['소재지도로명주소'],
      name: data['설치장소'],
      lat: data['위도'], 
      lng: data['경도']
    });
  });
});

fs.writeFile('./public/multi-latlng.json', JSON.stringify(latlng), 'utf8', () => {
  console.log('Done');  
});

이건 module.exports를 하지는 않았다. 필요할 때마다 그때그때 실행할 수 있도록 했다. 시트가 2개 이상이었기 때문에 일단 sheets에 엑셀 시트를 하나씩 전부 push한 다음, jsonData에 그 시트 json 데이터를 하나씩 push한다.

그런 다음 jsonData에 forEach를 돌린다. 지금 jsonData의 요소 개수는 시트 개수와 똑같고, 내용은 해당 시트를 json 형식으로 바꾼 데이터이다. arr에도 forEach를 돌리는데, arr을 forEach하고 그 안에서 latlng.push를 하면 위의 excel-read.js에서 jsonData.map을 한 것과 같은 효과를 낸다.

 

 

2. JSON 데이터로 지도에 마커 찍기

이렇게 해서 엑셀 파일에서 원하는 데이터만 골라서 json 파일로 만드는 것까진 완료했고, 그 다음엔 서버 올려서 웹페이지에 접속하면 이 json 파일의 위도, 경도를 지도에 정확히 찍어서 마커를 만드는 작업을 해야 했다.

바빠서 코드를 좀 방치하다 보니 만든 지 1달이 넘은 지금까지도 리팩토링을 못 했는데(아마 마음먹고 하지 않는 이상 하기 힘들 것 같다) 중요한 코드 뭉치만 올리고 전체 코드는 링크로 걸겠다.

 

const fetchData = async () => {
  /* excel-read를 실행한다면 fetch() 안을 './latlng.json'으로 변경해야 함*/
  const response = await fetch('./latlng.json');
  const latlng = await response.json();
  //...

 

먼저, fetch API로 latlng.json 데이터를 가져오는 작업을 해야 했다. 그런데 latlng.json의 크기가 크면 fetch가 완료되기 전에 다음 행들이 실행되기 때문에 async-await로 처리를 해줘야 했다.

 

  const positions = [];

  latlng.map(l => {
    positions.push({
      dong: l.dong,
      index: l.index,
      roadName: l.roadName,
      name: l.name,
      latlng: new kakao.maps.LatLng(l.lat, l.lng)
    });
  });
  

 

그런 다음, positions라는 빈 배열을 만들어 엑셀 파일에서 했던 것처럼 하나하나 객체로 만들어 준다.

 

  // 마커 이미지의 이미지 주소입니다
  const imageSrc = "https://t1.daumcdn.net/localimg/localimages/07/mapapidoc/markerStar.png";

  positions.map(position => {

    // 마커 이미지의 이미지 크기 입니다
    const imageSize = new kakao.maps.Size(24, 35);

    // 마커 이미지를 생성합니다    
    const markerImage = new kakao.maps.MarkerImage(imageSrc, imageSize);

    // 마커를 생성합니다
    const marker = new kakao.maps.Marker({
      map: map, // 마커를 표시할 지도
      position: position.latlng, // 마커를 표시할 위치
      title: `${position.dong} :: (${position.index})`,
      // 마커의 타이틀, 마커에 마우스를 올리면 타이틀이 표시됩니다
      image: markerImage, // 마커 이미지 
      opacity: 0.7,
      clickable: true // 마커를 클릭했을 때 지도의 클릭 이벤트가 발생하지 않도록 설정합니다
    });
    kakao.maps.event.addListener(marker, 'mouseover', () => {
      marker.setOpacity(1.0);
    });
    kakao.maps.event.addListener(marker, 'mouseout', () => {
      marker.setOpacity(0.7);
    });
    // 마커를 지도에 표시합니다.
    marker.setMap(map);
    //...

이제 positions 배열의 요소 하나하나는 마커로 만들어질 준비가 된 객체이다. 여기에 map을 돌리는데... 어 배열 만들 거 아닌데 왜 map으로 했을까? forEach로 해도 될 거 같은데 그 때는 약간 map에 미쳐 있었던 거 같다.

const marker = new kakao.maps.Marker({객체})를 해서 마커를 만들고, marker.setMap(map)을 해서 실제 지도에 띄운다. 

kakao.maps.event.addListener를 적절히 설정하면 특정 이벤트가 발생할 때마다 실행할 동작들을 지정할 수 있다. 내 경우 mouseover가 되면 마커가 완전 불투명이 되고, 마우스가 나가 있을 때는 30% 투명도를 가지도록 설정했다.

 

 // 마커에 클릭이벤트를 등록합니다
    kakao.maps.event.addListener(marker, 'click', function () {
      // infowindow.open(map, marker);
      let message = `${marker.getTitle()} ${position.roadName} <br> ${position.name}<br><br>`;
      message += `위도 : ${marker.getPosition().getLat().toFixed(6)} `;
      message += `경도 : ${marker.getPosition().getLng().toFixed(6)}`;

      const resultDiv = document.getElementById('result');
      resultDiv.innerHTML = message;
    });
    kakao.maps.event.addListener(marker, 'rightclick', function () {
      let url = `${marker.getTitle()} ${position.roadName},`;
      url += `${marker.getPosition().getLat().toFixed(6)},`;
      url += `${marker.getPosition().getLng().toFixed(6)}`;
      window.open(`https://map.kakao.com/link/map/${url}`, '_blank');
    });

그런 다음 마커에 클릭 이벤트도 등록해줬다.

그냥 클릭했을 때 #result 박스에 클릭한 장소의 이름과 도로명, 위도와 경도를 소숫점 6자리까지 띄우도록 만들었다. 우클릭했을 때는 해당 장소의 위도와 경도를 파라미터로 가지는 카카오 맵을 열도록 했다.

 

const mapClick = mouseEvent => {
  // 클릭한 위도, 경도 정보를 가져옵니다 
  const kakaoL = mouseEvent.latLng;

  // 마커 위치를 클릭한 위치로 옮깁니다
  newMarker.setPosition(kakaoL);

  const message = `위도 : ${kakaoL.getLat().toFixed(6)} 경도 : ${kakaoL.getLng().toFixed(6)}`;

  const resultDiv = document.getElementById('result');
  resultDiv.innerHTML = message;
  return kakaoL;
}

kakao.maps.event.addListener(map, 'click', mapClick);

kakao.maps.event.addListener(map, 'rightclick', function (mouseEvent) {
  // 클릭한 위도, 경도 정보를 가져옵니다 
  const kakaoL = mapClick(mouseEvent);
  const lat = kakaoL.getLat().toFixed(6);
  const lng = kakaoL.getLng().toFixed(6);
  let url = `위도 : ${lat} 경도 : ${lng},`;
  url += `${lat},${lng}`;
  window.open(`https://map.kakao.com/link/map/${url}`, '_blank');
});

마커가 없는 그냥 지도의 아무 곳이나 클릭해도 비슷하게 결과가 나올 수 있도록 했다.

이 경우에는 동과 도로명 등을 알 수 없으므로 위도와 경도만 출력하게 했다.

우클릭했을 때는 역시 마찬가지로 해당 위치의 위도와 경로를 파라미터로 한 카카오 맵이 새 창으로 뜨도록 했다.

 

코드는 좀 지저분하다. 리팩토링을 언제 하게 될지는 모르겠다. 일단 구현부터 하는 게 목적이었고 그 목적만은 정말 충실히 따랐다(이게 습관이 되면 안 되는데).

어쨌거나 결과는 완전 성공이었다. 같은 시간에 위치 80개 딸걸 140개를 따는 등 생산성이 최소 1.5배가량 눈에 띄게 향상됐다. 이건 그냥 단순 작업이 반복돼서 능률이 오른 수준으로 설명할 수 없었다.

한국 지도 API를 사용하고 싶다면 카카오 지도 API가 좋은 선택일 거라는 생각이 든다. 네이버에서도 API를 제공하는지는 모르겠다. 내가 카카오 API를 사용하기로 한 이유는 작업을 카카오-구글 맵으로 했기 때문이지 네이버 지도가 별로라거나 하는 이유가 절대 아니다. 문서에 사용법이 굉장히 상세히 기술되어 있으므로 사용하기 편리하다.

 

 

사용례

이것 말고는 사진이나 캡쳐를 따로 찍어두지는 않았다. 이것도 양식만 맞춰서 내가 테스트 데이터를 임의로 만들어 찍은 것이다.

 

기능은 다음과 같았다.

  • 엑셀 시트의 레코드를 전부 지도에 찍음 (같이 알바한 분의 작업과 합쳐봤는데 거의 1700개 가량 나와서 지도를 줄여봤을 때 정말 장관이었다)
  • 지도의 아무 부분(마커를 포함)을 클릭하면 위도와 경도를 출력함 (구글 맵에서 찾아봐야 하는 문제 해결)
  • 지도의 아무 부분(마커를 포함)을 우클릭하면 해당 위도와 경로를 파라미터로 한 카카오 지도 창을 새로 열기함
  • 로드뷰 보기와 평범한 지도 모드 지원
  • 창 사이즈를 줄였을 때 언제나 브라우저 width의 100%가 되도록 하고, 위아래 스크롤이 생기지 않도록 크기 조정
  • 주소를 검색해 해당하는 위치를 지도에 찍도록 함 (마커 이미지가 다름)

 

이외에도 추가할 기능들이 몇 가지가 있었으나 이 정도 기능으로 충분하다는 생각에 개발을 더 진행하지는 않았다.

범용성이 조금 떨어지는 것 같아서 몇 가지 개선할 사항들을 나중에 시간 날 때 추가해볼까 생각 중이다.

그러나 그 '나중'은 언제 올지... 아마 종강 이후에 하겠지? 올해 했던 것들을 몰아서 리팩토링할 예정이다.

적어도 올 초중반의 나보다 지금의 내가 나을 테니까 ㅎㅎ

 

 

728x90
반응형
4 Comments
  • 프로필사진 질문 2021.05.16 19:48 안녕하세요 구글링하다가 들어왔습니다.
    올려놓으신 코드는 어떤 언어로 작성된건가요?
  • 프로필사진 2G Dev 2021.05.16 19:48 신고 자바스크립트입니다.
  • 프로필사진 박준호 2021.09.07 17:29 안녕하세요 제가 찾고 있는 기능과 너무 흡사해서 그런데 혹시 가능하시다면 파일 공유 좀 해주실 수 있을까 해서 댓글 남기게 되었습니다...
    엑셀시트에 있는 주소들을 끌어다 놓기만 하면 바로 카카오맵에 해당 주소에 대한 주소 마커가 뜨게 하고 싶습니다. 가능하시다면 파일이나 솔직한 조언 부탁드립니다.
  • 프로필사진 2G Dev 2021.09.07 18:06 신고 안녕하세요?
    https://github.com/dev-dain/Map-The-Seoul
    여기에 코드를 저장해뒀습니다. exe 실행 파일은 아니구요. 컴퓨터에 nodeJS가 설치되어 있고 엑셀이 양식대로 준비되어 있다면 사용하실 수 있습니다.
    다만 제가 웹앱을 만든지 거의 1년이 되어 정확히 어떤 방식으로 동작하는지는 기억이 가물가물하네요 ^^; 참고가 되시길 바랍니다.
댓글쓰기 폼