이 글의 초안은 원래 지난 5월 초에 작성했는데, 바쁜 일들 처리하고, 메르스가 오고, 여차저차 하다보니 어느덧 6월 말이 되어버렸네요. 시의성을 위해서는 맛집 대신 다른 내용의 지도를 내보내는게 좋을 것 같지만, 이 또한 나름의 의미가 있는듯 해서 그대로 올립니다.

맛집1과 같이 지리적 위치가 중요한 데이터는 지도 상에 표시했을 때 훨씬 보기가 쉽다. (ex: 이성빈, 봉천동 맛집 대폭발공개) 비단 맛집 뿐 아니라 병원이나, 여행 일정등도 지도라는 시각화 방식을 사용하면 훨씬 효과적으로 정보를 전달할 수 있다.

게다가 지도 상에 점을 찍는 것은 어렵지 않다. 가장 손쉽게는 MS 파워포인트를 이용할 수 있고, 디자이너분들께서 능숙하게 다루는 어도비 포토샵이나 일러스트레이터도 있지만 그 밖에도 아주 손쉽게 사용할 수 있는, 지도에 특화된 서비스도 많다 2:

또는 지도 특화 서비스는 아니지만 포스퀘어 같은 맛집 서비스를 이용해서 나만의 지도를 큐레이션할 수도 있다. 이들은 모두 클릭 몇 번만 하면 화면 너머 블랙박스에서 돌아가는 마술로 지도 위에 점을 멋있게 뿌려준다는 특성을 가지고 있다.

그런데 맛집 지도를 웹 기반으로, 인터렉티브하게 만들 수는 없을까? 그 동안 마이크 보스톡(Mike Bostock)느님이 미국 지도를 이용해서 다양하고 멋진 시각화를 수도 없이 많이 보여줬는데, 우리 지역의 지도로는 그렇게 만들 수 없나?

물론 할 수 있다. 여기서 총 4단계에 걸쳐 다음의 웹 기반 서울 맛집 지도를 그리는 방법에 대해 알아보자.

인터랙티브 버젼은 너비 768px 이상의 큰 화면에서만 보실 수 있습니다.

0단계: 들어가기 전

웹 기반 인터렉티브 지도를 그리기 위해서는 몇 가지 기반 기술과 재료가 필요하다. 일단 HTML, CSS, 자바스크립트에 대한 사전지식이 조금 있어야하고, 아래의 두 가지가 적어도 무슨 일을 하는 녀석들인지 알아야 한다.

  • D3.js: 자바스크립트 라이브러리로서, 데이터 기반으로 DOM을 편리하게 조작할 수 있게 해준다. 앞서 언급한 마이크 보스톡은 이 라이브러리를 능수능란, 자유자재로 다루기로 유명하다.
  • TopoJSON: 지리공간적 데이터를 담는 포맷의 일종인데, GeoJSON의 보다 훨씬 효율적3인 버젼이라고 할 수 있다.

재료로는 1) 지도 데이터와 2) 맛집 목록이 필요하다.
먼저 지도 데이터는 서울시 셰이프파일을 TopoJSON으로 변환한 파일을 썼고, 맛집 목록은 위키트리 김도담 기자님의 식신로드 서울지역 만점식당 20선을 이용했다. 이 리스트를 선택한 이유는 맛집의 이름 뿐 아니라 주소까지 나와 있어서, 지오매핑(geomapping)이 아주 편하기 때문이다. (물론 식신로드 만점식당을 정주행해보고 싶다는 개인적인 소망도 크게 작용했다.)

1단계: 맛집의 위경도 구하기

주소가 있으면, 맛집의 위도와 경도를 아주 쉽게 구할 수 있다. 나는 다음과 같이 quick ‘n dirty하게 네이버의 지도 API를 이용했다:

crawl.pylink
#! /usr/bin/python3
# -*- coding: utf-8 -*-
from lxml import html
import requests
APIKEY = '' # 이 곳에 네이버 API 키를 입력 (http://developer.naver.com/wiki/pages/OpenAPI)
MAPAPI = 'http://openapi.map.naver.com/api/geocode.php?key=%s&encoding=utf-8&coord=LatLng&query=%s'
def get_latlon(query):
root = html.parse(MAPAPI % (APIKEY, query))
lon, lat = root.xpath('//point/x/text()')[0], root.xpath('//point/y/text()')[0]
return (lat, lon)
def prep(item):
n, name = item[0].split(' ', 1)
lat, lon = get_latlon(item[3])
return {
'num': n, 'name': name,
'lat': lat, 'lon': lon,
'description': item[1],
'phone': item[2],
'addr': item[3]
}
# get data from article
r = requests.get('http://m.wikitree.co.kr/main/news_view.php?id=217101')
root = html.document_fromstring(r.text)
string = '\n'.join(root.xpath('//div[@id="ct_size"]/div//text()'))
items = []
for i in range(1, 21):
tmp = string.split('%s.' % i, 1)
string = tmp[1]
items.append([j.strip() for j in tmp[0].split('\n') if j and j!='\xa0'])
data = [prep(i[:4]) for i in items[1:]]
# save data to file
with open('places.csv', 'w') as f:
f.write('name,lat,lon\n')
for d in data:
f.write('%(name)s,%(lat)s,%(lon)s\n' % d)

찍으려는 좌표가 몇 개 안 되거나 찍고자하는 점의 주소를 구하지 못한 경우에는 수작업으로 위경도 정보를 얻는 방법도 있다. 일종의 꼼수지만, 다음과 같이 구글 지도에 상호명을 입력하면 대략적인 위경도 정보가 URL에 뜬다:

최종적인 맛집 데이터는 다음과 같이 식당 이름(name), 위도(lat, latitude), 경도(lon, longitude)만 최소한으로 담고 있으면 된다. (물론 필요에 따라 변수를 추가하면, 점의 색이나 모양 등을 그에 맞게 바꿀 수 있다.)

places.csvlink
name,lat,lon
다성일식,37.5569016,126.9329799
봉산집,37.5344248,126.9750082
창고43,37.5189582,126.9307458
돕감자탕,37.5414873,127.0692836
대보명가,37.6456867,127.0071573
해뜨는집,37.5900691,127.0084044
아이 해브어 드림,37.4986175,127.0278876
아현동 간장게장,37.5546000,126.9561000
왕소금구이,37.5204088,127.0361107
라틀리에 모니크,37.5261816,127.0448766
비스트로 딩고,37.5210777,127.0198099
줄리에뜨,37.4947204,127.0009384
충주집,37.5862178,127.0345490
영화루,37.5801384,126.9690401
일품헌,37.4845029,127.0391935
립스테이크,37.5955682,126.9640695
오가와,37.5720260,126.9743351
까사디노아,37.5620845,126.9235561
충무로 주꾸미 불고기,37.5617693,126.9921966

서울 지역에 대한 데이터이기 때문에 위도는 대부분 37도 부근, 경도는 127도 부근에 위치하고 있다. 내 데이터가 부정확한 값을 담고 있으면 지도도 제대로 그릴 수 없다.

2단계: 지도를 펼쳐보자

맛집 데이터를 구해두었으니, 맛집을 지도에 뿌리기 전에 먼저 지도를 그려보자.

index.html이라는 이름으로 HTML 파일을 하나 생성하고 아래를 복사해 넣자. 앞에서 HTML에 대한 사전지식이 있어야 한다고 했지만 우리가 사용할 HTML은 이게 전부고, 여기에 CSS와 자바스크립트만 조금(?) 채워 넣으면 된다.

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<style>
/* CSS는 여기에 */
</style>
</head>
<body>
<div id="chart"></div>
<script src="http://d3js.org/d3.v3.min.js"></script>
<script src="http://d3js.org/topojson.v1.min.js"></script>
<script>
// 자바스크립트는 여기에
</script>
</body>
</html>

10번째 줄에서 id가 chart인 div를 생성했는데, 자바스크립트를 이용해서 이곳에 우리의 지도를 넣을 것이다.

웹페이지에 “옷을 입히는 부분”인 CSS는 뒤에서 다시 살펴보고, 일단 자바스크립트부터 넣어보자. 11, 12번째 줄에서 각각 d3.v3.min.js와 topojson.v1.min.js를 불러오고 있다. 전자는 d3.js를 사용하기 위해 필요하고, 후자는 지도 데이터를 렌더링하는데 사용된다. 지금부터 우리가 작성하는 코드는 “자바스크립트는 여기에”라고 되어 있는 부분에 입력하면 된다.

먼저 d3를 이용해서 10번째 줄의 div 안에 크기가 800x600 픽셀인 svg를 생성해보자. 이 div의 id가 chart라는 성질을 이용했다.

var width = 800,
height = 600;
var svg = d3.select("#chart").append("svg")
.attr("width", width)
.attr("height", height);

다음으로 svg 안에 지도 레이어 map과 맛집 레이어 places를 만들자.

var map = svg.append("g").attr("id", "map"),
places = svg.append("g").attr("id", "places");

지도를 그릴 때 다양한 투영법에 대해 알면 좋다. 여기서는 메르카토르 투영법을 이용했고, 지도는 앞에서 언급한 서울시 셰이프파일을 TopoJSON으로 변환한 파일을 사용해서4 map 레이어에 각 지역구에 대한 path와 지역명을 표시한 text 요소를 생성했다.

var projection = d3.geo.mercator()
.center([126.9895, 37.5651])
.scale(100000)
.translate([width/2, height/2]);
var path = d3.geo.path().projection(projection);
d3.json("seoul_municipalities_topo_simple.json", function(error, data) {
var features = topojson.feature(data, data.objects.seoul_municipalities_geo).features;
map.selectAll("path")
.data(features)
.enter().append("path")
.attr("class", function(d) { console.log(); return "municipality c" + d.properties.code })
.attr("d", path);
map.selectAll("text")
.data(features)
.enter().append("text")
.attr("transform", function(d) { return "translate(" + path.centroid(d) + ")"; })
.attr("dy", ".35em")
.attr("class", "municipality-label")
.text(function(d) { return d.properties.name; })
});

이제 터미널에서 파이썬3를 이용해 로컬 웹 서버를 돌리자.

python -m http.server 8888      # for Python2, `python -m SimpleHTTPServer`

위 명령을 입력한 후 명령이 종료되지 않는 것은 버그가 아니라 우리의 로컬 웹 서버가 정상적으로 돌아가고 있다는 의미니까 기다리고 있거나 놀라지 말자. 웹 브라우저를 열어서 http://localhost:8888에 들어가보자. 엇, 우리가 아는 서울시 모양을 볼 수 있다! 우측 상단에 “…구”라는 글자가 작게 보이는 것으로 봐서, 앞으로 CSS만 잘 입히면 지역명도 잘 출력될 듯하다.

3단계: 지도에 점을 찍자

이제 1단계에서 만든 맛집 위경도를 데이터 places.csv를 이용해 맛집을 지도에 뿌려보자. (이 역시 “자바스크립트는 여기에” 부분에 계속 추가하면 된다.) 각 맛집에 대해 places 레이어에 circle과 text를 생성한 것을 볼 수 있다.

d3.csv("places.csv", function(data) {
places.selectAll("circle")
.data(data)
.enter().append("circle")
.attr("cx", function(d) { return projection([d.lon, d.lat])[0]; })
.attr("cy", function(d) { return projection([d.lon, d.lat])[1]; })
.attr("r", 10);
places.selectAll("text")
.data(data)
.enter().append("text")
.attr("x", function(d) { return projection([d.lon, d.lat])[0]; })
.attr("y", function(d) { return projection([d.lon, d.lat])[1] + 8; })
.text(function(d) { return d.name });
});

4단계: 지도에 옷을 입히자

맛집의 위치까지 점을 찍었지만 아직까지는 우리 지도는 전체적으로 까맣기만 한데, “CSS는 여기에”라고 되어 있는 부분에 각 element에 대한 색을 입히면 멋진 지도가 완성된다.

나는 다음과 같은 값들을 줘서 지도를 완성했다.

svg circle {
fill: orange;
opacity: .5;
stroke: white;
}
svg circle:hover {
fill: red;
stroke: #333;
}
svg text {
pointer-events: none;
}
svg .municipality {
fill: #efefef;
stroke: #fff;
}
svg .municipality-label {
fill: #bbb;
font-size: 12px;
font-weight: 300;
text-anchor: middle;
}
svg #map text {
color: #333;
font-size: 10px;
text-anchor: middle;
}
svg #places text {
color: #777;
font: 10px sans-serif;
text-anchor: start;
}

끝이다! 코드 전체는 여기에서 볼 수 있고, 완성된 지도는 이 링크에서 볼 수 있다. 앞에서 적용한 CSS 때문에, 마우스를 점에 올려놓으면(i.e., hover action을 주면) 점이 붉은 색으로 바뀐다.

지도 하나를 그렸을 뿐이지만 이렇게 “내 코드”가 되고나면 재현성(reproducibility)나 다른 영역에 대한 적용성(applicability) 등을 거머쥘 수 있게 되는 것은 큰 매력이다. 즉, 같은 서울시 지도를 이용해서 다른 데이터를 표시하는 것은 데이터만 바꿔 끼우면 되니까 너무도 간단한 일이 된다. 뿐만 아니라 미세한 동작이나 세밀한 디자인 하나하나를 커스터마이징하는 재미도 있다. 이 지도를 응용해서 나는 서울시 스시야 기행도 그렸는데, 같은 지도를 이용하고 같은 방식으로 시각화를 했지만, 1) 가격대에 따라 점마다 색을 다르게 하고, 2) 색의 범례를 추가하고, 3) 점마다 링크를 추가하고, 4) 맛집의 이름이 겹치지 않도록 forced layout을 적용하는 등의 기능을 추가했다. 전국 지도를 이용해서 같은 방식으로 그린 지도로는 롯데리아 보로노이가 있다. 여기에는 보로노이 다이어그램을 추가적으로 입혔다.

마치면서 잠깐 광고 한 마디! 곧 (2015년 7월 초?!) 제가 번역한 “D3를 이용한 시각적 스토리텔링 (리치 킹 저, 박은정/김한결 번역, 인사이트 출판사)”이 출판됩니다. 이 책에는 아주아주 쉽고 상세한 설명이 담겨 있어 웹 기반 시각화 만들기에 대한 탄탄한 기초를 마련할 수 있으니 혹시 이 글의 설명이 너무 어렵게 느껴졌다면 책을 보시는 것도 추천드립니다. 더불어 이 책에는 서울시에서 한 걸음 나가 국내 지도를 활용하고, 점 찍는 것에서 한 걸음 나가 코로플래스를 그리는 법5에 관한 설명도 부록으로 담았으니 많은 관심 가져주세요 :D

Updated 2015-07-08. 이 포스트는 슬로우뉴스에도 동시 게재 되었습니다.

  1. “맛집”이라는 단어는 누가 만든걸까? 영어로 번역하면 “tasty restaurant” 정도이려나? 다른 나라 말로 번역하기도 참 어려운, 아주 맛깔나는 표현인듯.

  2. 이들을 사용하면 데이터 시트를 직접 로딩할 수 있고 HTML 기반이기 때문에, 파워포인트나 포토샵 등을 쓸 때에 비해 scalability, interactivity와 같은 장점을 얻을 수 있다.

  3. 마이크 보스톡느님에 의하면 TopoJSON은 일반적으로 GeoJSON에 비해 약 80%의 용량 절감이 있다고 한다.

  4. 우리 나라 전국 단위 지도 TopoJSON 파일은 여기서 구할 수 있음.

  5. 최근 본 인상적인 코로플래스의 예: The Best and Worst Places to Grow Up: How Your Area Compares