SoyoonEN

04

mapix

캔버스 지도용 인터랙션 툴킷

Role
설계·개발 단독
Period
2026.06—2026.10
Stack
  • TypeScript
  • Mapbox GL
  • mapbox-gl-draw
  • GeoJSON / KML / Shapefile
  • tsup
Links

Overview

mapix는 활발히 관리되지 않는 드로잉·비교 라이브러리를 제품에 적용하면서 시작했습니다. 기본 기능은 있었지만 TypeScript 타입도 충분하지 않아, 제품 요구에 맞춰 직접 다듬기 시작했습니다.

관심 지역 드로잉과 두 시점 비교를 안정화한 뒤 공간 파일 파싱까지 묶어 자체 라이브러리로 정리했고, MIT로 공개했습니다. 제품 화면이 없는 패키지라 API가 곧 인터페이스입니다.

Problem

기존 드로잉·비교 라이브러리는 제품에 필요한 검증, 제약, 타입 안전성을 충분히 제공하지 못했습니다. 필요한 기능을 앱마다 덧붙이기 시작하자 지도 코드가 복사본마다 갈라졌고, 한쪽에서 고친 버그가 다른 쪽에는 남았습니다.

문제는 코드를 한곳에 모으는 것만이 아니라, 특정 앱 사정을 전제하지 않는 타입 있는 라이브러리 API로 다시 설계하는 일이었습니다.

Constraint

  1. 쓰는 쪽 사정을 알 수 없다

    내부 코드는 우리 앱의 스타일과 레이아웃을 전제해도 됩니다. 패키지는 안 됩니다. 스타일을 주입하지 않은 앱에서도 경고가 보여야 하고, 패널이 화면 절반을 덮은 레이아웃에서도 슬라이더가 쓸 만해야 합니다.

  2. 포인터만 있는 게 아니다

    비교 슬라이더는 드래그로 조작하는 컴포넌트입니다. 드래그할 수 없는 사람에게도 같은 기능이 열려 있어야 합니다.

  3. 남의 지도를 빌려 쓴다

    슬라이더는 소비자가 만든 지도 두 개를 받아 씁니다. 다 쓰고 나면 받았을 때 모습 그대로 돌려줘야 합니다. 드래그 도중에 제거되더라도요.

Decision

  1. 경고 레이어를 지도에 직접 등록한다

    자기교차 경고의 소스와 레이어를 패키지가 지도에 직접 올립니다. 소비자가 스타일을 한 줄도 넣지 않아도 잘못 그린 도형이 빨갛게 드러납니다. 선 두께는 소비자가 흔히 쓰는 획 위에 얹어도 테두리가 새지 않도록 한 단계 두껍게 잡았습니다.

  2. 끝점을 공유하는 건 교차가 아니다

    선분 교차를 엄격 부등호로 판정합니다. 이웃한 변은 반드시 끝점을 공유하므로, 등호를 허용하면 정상적인 폴리곤이 전부 경고에 걸립니다. 좌표는 평면으로 근사했습니다. 관심 지역 규모에서는 이 판정이 지도 곡률에 좌우되지 않습니다.

  3. 슬라이더를 키보드로 완결시킨다

    손잡이를 role="slider"로 탭 순서에 넣고 방향키·PageUp/Down·Home/End를 붙였습니다. 키를 누르고 있는 동안 이벤트가 쏟아지지 않도록, 키를 떼거나 초점이 빠질 때 완료를 한 번만 알립니다.

  4. 되돌릴 수 있게 만든다

    remove()는 지도 컨테이너가 처음 가지고 있던 인라인 스타일을 되써 넣고 붙인 리스너를 모두 뗍니다. 슬라이더에 잠깐 빌려준 지도를 다른 곳에서 다시 써도 흔적이 남지 않습니다.

Interface

Draw modes
import MapboxDraw from '@mapbox/mapbox-gl-draw';
import { DrawPolygon, DrawRectangle } from '@telepix-lab/mapix';

const draw = new MapboxDraw({
  displayControlsDefault: false,
  modes: { ...MapboxDraw.modes, draw_polygon: DrawPolygon },
});

map.addControl(draw);
draw.changeMode('draw_polygon', { maxVertices: 1000 });

기존 모드를 덮어쓰는 형태라 쓰는 쪽이 새로운 개념을 배우지 않아도 됩니다. 꼭짓점 상한은 모드마다 따로 겁니다.

Compare slider
const compare = new Compare(beforeMap, afterMap, container, {
  orientation: 'vertical',
  bounds: { min: 0.25, max: 0.95 },
  initialRatio: 0.6,
});

compare.on('slideend', (event) => {
  console.log(event.currentPosition);
});

compare.remove();

bounds는 양끝을 따로 잡습니다. 한쪽만 패널에 가린 레이아웃에서 반대쪽 여유는 그대로 둘 수 있습니다. 가운데에서 한 프레임 깜빡이지 않도록 초기 위치는 생성자에서 받습니다.

File parsing
const result = await parseFile(file);

if (result.success && result.data) {
  const vertexCount = countVertices(result.data);
  // result.correction: 구조를 고쳤다면 무엇을 고쳤는지
} else {
  // result.errorCode: 고정된 기계 코드. 문구는 쓰는 쪽에서 정한다
  console.error(result.errorCode);
}

오류를 문장이 아니라 코드로 돌려줍니다. 네 개 로케일을 쓰는 앱이 자기 문구를 붙일 수 있어야 하기 때문입니다. 업로드 정책은 패키지가 정하지 않습니다.

Engineering

  1. FileGeoJSON · KML · Shapefile을 읽는다
  2. Normalize폴리곤 컬렉션으로 맞추고 구조를 고친다
  3. Draw그리는 도중에 자기교차를 판정한다
  4. Compare두 지도의 카메라를 묶는다
  1. 마우스·터치·펜을 한 길로

    포인터 이벤트 하나로 처리하고, 누르는 순간 포인터를 캡처합니다. 커서가 손잡이를 벗어나도 드래그가 이어지고, 두 번째 손가락이 끼어들어 드래그를 가로채거나 끝낼 수 없습니다. 브라우저가 터치를 스크롤로 가져가지 않도록 touch-action도 꺼 둡니다.

  2. 크기가 바뀌면 비율을 지킨다

    컨테이너 크기가 바뀔 때 픽셀 위치가 아니라 분할 비율을 유지합니다. 탭이나 패널이 닫혀 크기가 0이 되었다 돌아오는 경우까지 같은 규칙을 따릅니다.

  3. padding까지 같이 묶는다

    두 지도의 카메라를 동기화할 때 padding도 함께 옮깁니다. padding은 투영 중심을 옮기기 때문에, 같은 center를 들고 있어도 padding이 다르면 화면에는 다른 위치로 그려집니다.

  4. 못 하는 것을 문서에 적는다

    MapLibre에서는 드로잉 모드가 동작하지 않습니다. mapbox-gl-draw 기본 테마가 쓰는 line-dasharray 리터럴을 MapLibre가 거부하기 때문입니다. 호환 표에 지원 여부와 이유를 함께 적어 두었습니다. 파일 파싱은 엔진과 무관해서 어디서나 됩니다.

Result

2026년 6월 MIT로 공개하고 9월에 0.2.0을 냈습니다.

기존 드로잉·비교 라이브러리의 빈틈을 메우려 시작한 작업은 드로잉·비교·파일 처리를 함께 다루는 지도 인터랙션 라이브러리가 됐고, SatCHAT이 이 패키지를 씁니다. 0.x인 동안 공개 API가 바뀔 수 있다는 점도 문서 맨 앞에 적어 두었습니다.

오픈소스 프로젝트입니다. 코드와 문서를 그대로 인용했습니다.