프론트엔드

Cesium 마우스 이벤트 처리

한 줄 요약

Cesium의 명령형(imperative) 이벤트 API를 React의 선언형(declarative) 컴포넌트 모델 위에서 운용하기 위한 아키텍처 설계. 세 가지 핵심 패턴 — 컴포넌트 라이프사이클 기반 이벤트 관리, Hook-per-Entity 추상화, 커스텀 이벤트 버스 — 으로 복잡한 3D 인터랙션을 구조적으로 해결했다.

근본 문제: 선언형과 명령형의 충돌

Cesium.js와 React는 정반대의 프로그래밍 모델을 따른다.

React는 선언형(declarative)이다. "화면에 무엇이 보여야 하는가"를 기술하면, React가 DOM을 알아서 업데이트한다. 컴포넌트가 마운트되면 나타나고, 언마운트되면 사라진다. 개발자가 직접 DOM을 조작하지 않는다.

Cesium의 ScreenSpaceEventHandler는 명령형(imperative)이다. 이벤트를 직접 등록하고, 사용이 끝나면 직접 해제해야 한다. 해제를 빠뜨리면 이벤트 핸들러가 잔류하여 메모리 누수와 이벤트 충돌을 일으킨다.

측정 도구 7종과 이슈 CRUD를 구현하려면, 이 두 모델 사이에 브릿지가 필요했다. 이 브릿지의 설계가 이 노드의 핵심이다.

패턴 1: React 컴포넌트 라이프사이클 = 이벤트 생명주기

이 문제를 아키텍처 레벨에서 해결한 핵심 아이디어는, 각 도구를 독립된 React 컴포넌트로 구현하여, 컴포넌트의 마운트/언마운트가 곧 이벤트의 등록/해제가 되도록 설계한 것이다.

도구 활성화 → 컴포넌트 마운트 → 이벤트 핸들러 등록
도구 비활성화 → 컴포넌트 언마운트 → 이벤트 핸들러 해제 + 임시 엔티티 정리

이 구조에서 도구 전환 시의 이벤트 교체 문제가 자동으로 해결된다. 예를 들어 사용자가 "거리 측정" 도중에 "면적 측정"을 누르면:

  1. 거리 측정 컴포넌트가 언마운트된다 → 거리 측정의 이벤트 핸들러가 자동 해제되고, 그리던 임시 점·선·라벨이 정리된다
  2. 면적 측정 컴포넌트가 마운트된다 → 면적 측정의 이벤트 핸들러가 자동 등록된다

개발자가 "이전 도구의 이벤트를 해제하고 → 임시 엔티티를 제거하고 → 새 도구의 이벤트를 등록하고..."를 명시적으로 관리할 필요가 없다. React의 컴포넌트 라이프사이클이 이 순서를 보장한다.

중앙에서 활성 도구 상태(active tool state)를 하나만 관리하면, 어떤 도구가 활성화되면 다른 도구 컴포넌트는 자동으로 언마운트된다. 동시에 두 도구가 활성화되는 것이 구조적으로 불가능하다.

이것은 Cesium의 명령형 이벤트 관리를 React의 선언형 모델 안으로 흡수한 것이다. "이벤트를 언제 등록하고 언제 해제할 것인가"라는 명령형 문제를, "어떤 컴포넌트가 화면에 있는가"라는 선언형 문제로 변환한 것이다.

패턴 2: Hook-per-Entity 추상화

각 도구 컴포넌트가 Cesium의 저수준 Entity API를 직접 다루면, 코드가 복잡해지고 도구 간 중복이 발생한다. 이 문제를 엔티티 유형별 커스텀 훅으로 해결했다.

추상화 구조

3D 공간에 필요한 동적 엔티티(점, 선, 라벨, 사각형 등)를 각각 독립된 커스텀 훅으로 캡슐화했다. 각 훅이 내부적으로 Cesium 엔티티의 생성·업데이트·삭제를 관리한다.

도구 컴포넌트 (DistanceTool, AreaTool, ...)
  └── 커스텀 훅 조합
        ├── useDynamicPoint     → 커서 따라다니는 점
        ├── useDynamicLine      → 실시간 미리보기 선 + 거리 라벨
        └── useMeasureLines     → 확정된 측정선 목록

도구 컴포넌트는 Cesium의 Entity API를 직접 호출하지 않는다. 대신 훅이 제공하는 고수준 인터페이스를 사용한다:

  • "이 3D 좌표에 점을 놓아라" → setPosition(cartesian3)
  • "선의 끝점을 이 위치로 업데이트하라" → setEndPosition(start, end)
  • "이 도구의 모든 임시 엔티티를 제거하라" → remove()

이 추상화가 주는 이점

새 도구 추가가 쉬워진다. 새 측정 도구를 만들 때 Cesium의 Entity 생성·삭제 로직을 다시 작성할 필요 없이, 기존 훅을 조합하기만 하면 된다. 예를 들어 "수직·수평 거리" 도구는 동적 선 훅을 3개(대각선, 수직, 수평) 조합하여 구현했다.

정리 로직이 일관된다. 도구가 언마운트될 때 각 훅의 remove()만 호출하면 해당 훅이 관리하던 모든 엔티티가 Cesium 씬에서 제거된다. 어떤 엔티티를 빠뜨릴 위험이 줄어든다.

실시간 피드백의 구현이 표준화된다. 모든 도구가 마우스 이동 시 "점을 업데이트하고, 선의 끝점을 업데이트하고, 렌더를 요청"하는 동일한 패턴을 따른다. 이 패턴이 훅 레벨에서 통일되어 있으므로, 도구 간 동작의 일관성이 보장된다.

패턴 3: 커스텀 이벤트 버스 — React 상태 트리 외부 통신

대부분의 도구 간 통신은 React의 상태 관리(useState, Context, TanStack Query)로 처리할 수 있다. 그러나 Profile(단면도) 도구에서 이 범위를 벗어나는 문제가 발생했다.

문제: 두 개의 독립된 Cesium 뷰어

Profile은 메인 3D 뷰어 하단에 별도의 Cesium 뷰어를 열어 단면도를 보여준다. 이 두 뷰어 사이에 실시간 동기화가 필요하다:

  • 단면도 뷰어에서 마우스를 올린 위치 → 메인 뷰어에서 해당 위치를 강조
  • 메인 뷰어에서 "단면도 열기" 이벤트 → 단면도 뷰어가 반응
  • 레이어 토글(이슈 보기/숨기기) → 두 뷰어 모두에 반영

이 통신은 React의 상태 트리만으로는 처리가 어렵다. 두 뷰어가 React 트리에서 멀리 떨어진 위치에 있을 수 있고, Cesium의 내부 이벤트(카메라 이동, 타일 로드 등)는 React 상태 시스템 밖에서 발생한다.

해결: 싱글톤 이벤트 매니저 (Pub/Sub)

이 문제를 싱글톤 이벤트 매니저로 해결했다. 하나의 전역 인스턴스가 이벤트 채널을 관리하고, 어떤 컴포넌트든 이벤트를 발행(publish)하거나 구독(subscribe)할 수 있다.

발행 측:  "단면도를 열어라" 이벤트 발행
  → 이벤트 매니저가 구독자에게 전달
  → 구독 측: 단면도 뷰어가 열림

이 패턴의 핵심은 발행자와 구독자가 서로를 몰라도 된다는 것이다. 메인 뷰어의 버튼 컴포넌트는 "단면도를 열어라"라는 이벤트만 발행하고, 단면도 뷰어 컴포넌트는 그 이벤트를 구독하여 반응한다. 둘 사이에 직접적인 의존관계가 없으므로, 한쪽을 수정해도 다른 쪽에 영향이 없다.

이 이벤트 버스는 React의 상태 관리와 보완적으로 사용된다. 일반적인 UI 상태는 React로, Cesium 레이어 간의 이벤트 통신은 이벤트 버스로 처리한다. 두 시스템의 역할이 명확히 분리되어 있다.

이 경험에서 추출한 원칙

  1. 명령형 API를 선언형 모델 안에 흡수하라. "이벤트를 언제 등록/해제할 것인가"를 직접 관리하면 실수가 생긴다. 컴포넌트 라이프사이클에 바인딩하면, React가 순서와 정리를 보장한다. 이것은 Cesium뿐 아니라 D3, Canvas, WebGL 같은 명령형 라이브러리를 React에서 사용할 때 일반적으로 적용 가능한 패턴이다.

  2. 추상화의 단위는 "도구"가 아니라 "엔티티"다. 도구별로 코드를 작성하면 중복이 발생하고, 도구 간 동작이 불일치한다. 엔티티(점, 선, 라벨) 단위로 추상화하면 도구는 "훅의 조합"이 되어 일관성과 재사용성을 동시에 얻는다.

  3. React 상태 관리의 한계를 인식하고, 보완적 시스템을 설계하라. React의 상태 트리는 UI 계층을 따르지만, 3D 렌더링 엔진의 이벤트는 UI 계층을 따르지 않는다. 이 갭을 메우기 위해 Pub/Sub 같은 별도 통신 채널이 필요할 수 있으며, 두 시스템의 역할 경계를 명확히 정의해야 한다.