Learning
레슨 6 / 8·20분

지리 데이터와 지도

D3로 지도 시각화하기

D3는 지리 데이터(GeoJSON, TopoJSON)를 기반으로 인터랙티브 지도를 만드는 강력한 기능을 제공합니다. 투영법(projection)을 사용하여 구체 위의 좌표를 2D 평면에 매핑하고, 다양한 시각적 효과를 적용할 수 있습니다.

GeoJSON 데이터 구조

GeoJSON은 지리 공간 데이터를 표현하는 표준 JSON 형식입니다. 각 feature는 geometry(점, 선, 다각형)와 properties(속성 데이터)로 구성됩니다.

javascript
// GeoJSON 구조 예시
var geojson = {
  "type": "FeatureCollection",
  "features": [
    {
      "type": "Feature",
      "properties": {
        "name": "서울특별시",
        "population": 9700000,
        "code": "11"
      },
      "geometry": {
        "type": "Polygon",
        "coordinates": [[[126.7, 37.4], [127.2, 37.4], /* ... */]]
      }
    },
    // ... 다른 지역들
  ]
};

투영법 (Projection)

투영법은 지구(3D)의 좌표를 화면(2D)의 좌표로 변환하는 방법입니다. D3는 수십 가지 투영법을 제공하며, 목적에 따라 적절한 것을 선택합니다.

javascript
var width = 600, height = 500;

// 메르카토르 투영법 (일반적인 웹 지도)
var projection = d3.geoMercator()
  .center([127.5, 36.0])        // 대한민국 중심 좌표
  .scale(5000)                   // 확대 비율
  .translate([width / 2, height / 2]);  // 화면 중앙에 배치

// 경로 생성기: 투영된 좌표를 SVG path로 변환
var pathGenerator = d3.geoPath()
  .projection(projection);

var svg = d3.select('#map')
  .append('svg')
  .attr('width', width)
  .attr('height', height);

지도 그리기

javascript
// GeoJSON 데이터 로드 및 지도 그리기
d3.json('/data/korea.geojson').then(function(geoData) {

  // 색상 스케일 (인구 기준)
  var colorScale = d3.scaleQuantize()
    .domain([0, 10000000])
    .range(d3.schemeBlues[7]);

  // 지역 경로 그리기
  svg.selectAll('.region')
    .data(geoData.features)
    .join('path')
    .attr('class', 'region')
    .attr('d', pathGenerator)
    .attr('fill', function(d) {
      return colorScale(d.properties.population);
    })
    .attr('stroke', '#fff')
    .attr('stroke-width', 0.5)
    .on('mouseover', function(event, d) {
      d3.select(this)
        .transition()
        .duration(200)
        .attr('stroke-width', 2)
        .attr('stroke', '#333');

      tooltip.style('opacity', 1)
        .html(
          d.properties.name + '<br>' +
          '인구: ' + d.properties.population.toLocaleString() + '명'
        )
        .style('left', (event.pageX + 15) + 'px')
        .style('top', (event.pageY - 10) + 'px');
    })
    .on('mouseout', function() {
      d3.select(this)
        .transition()
        .duration(200)
        .attr('stroke-width', 0.5)
        .attr('stroke', '#fff');

      tooltip.style('opacity', 0);
    });

  // 범례 추가
  var legend = svg.append('g')
    .attr('transform', 'translate(20, 350)');

  var legendScale = d3.scaleLinear()
    .domain([0, 10000000])
    .range([0, 200]);

  var legendAxis = d3.axisBottom(legendScale)
    .ticks(5)
    .tickFormat(function(d) { return d / 10000 + '만'; });

  legend.append('g')
    .attr('transform', 'translate(0, 20)')
    .call(legendAxis);
});

주요 투영법 종류

  • d3.geoMercator() — 메르카토르 (웹 지도 표준, 고위도 왜곡)
  • d3.geoAlbers() — 알베르스 정적 원추 (미국 지도에 적합)
  • d3.geoEquirectangular() — 정거원통 (단순, 교육용)
  • d3.geoOrthographic() — 정사 투영 (지구본 느낌)
  • d3.geoNaturalEarth1() — 자연스러운 세계 지도
  • d3.geoConicEqualArea() — 정적 원추 (면적 보존)

줌과 패닝

javascript
// 줌/패닝 기능 추가
var zoom = d3.zoom()
  .scaleExtent([1, 8])     // 줌 범위: 1배 ~ 8배
  .on('zoom', function(event) {
    svg.selectAll('path')
      .attr('transform', event.transform);
  });

svg.call(zoom);

// 특정 지역으로 줌 인
function zoomToRegion(feature) {
  var bounds = pathGenerator.bounds(feature);
  var dx = bounds[1][0] - bounds[0][0];
  var dy = bounds[1][1] - bounds[0][1];
  var x = (bounds[0][0] + bounds[1][0]) / 2;
  var y = (bounds[0][1] + bounds[1][1]) / 2;
  var scale = Math.max(1, Math.min(8, 0.9 / Math.max(dx / width, dy / height)));
  var translate = [width / 2 - scale * x, height / 2 - scale * y];

  svg.transition()
    .duration(750)
    .call(zoom.transform, d3.zoomIdentity
      .translate(translate[0], translate[1])
      .scale(scale));
}

포인트 데이터 표시

javascript
// 도시 위치에 원형 마커 표시
var cities = [
  { name: '서울', lat: 37.5665, lon: 126.9780, value: 9700 },
  { name: '부산', lat: 35.1796, lon: 129.0756, value: 3400 },
  { name: '인천', lat: 37.4563, lon: 126.7052, value: 2900 },
  { name: '대구', lat: 35.8714, lon: 128.6014, value: 2400 },
  { name: '대전', lat: 36.3504, lon: 127.3845, value: 1500 },
];

var sizeScale = d3.scaleSqrt()
  .domain([0, 10000])
  .range([3, 20]);

svg.selectAll('.city')
  .data(cities)
  .join('circle')
  .attr('class', 'city')
  .attr('cx', function(d) { return projection([d.lon, d.lat])[0]; })
  .attr('cy', function(d) { return projection([d.lon, d.lat])[1]; })
  .attr('r', function(d) { return sizeScale(d.value); })
  .attr('fill', 'rgba(231, 76, 60, 0.6)')
  .attr('stroke', '#c0392b')
  .attr('stroke-width', 1);
💡

TopoJSON은 GeoJSON을 압축한 형식으로, 파일 크기가 훨씬 작습니다. topojson.feature()로 GeoJSON으로 변환하여 사용합니다. npm install topojson-client로 설치할 수 있습니다.