Frontend

[Frontend] 3D 디지털 트윈 공간분석 기능 구현 (feat. MSA)

teddy bear 2026. 7. 6. 23:01
반응형

🌍 3D 디지털 트윈 공간분석 마이크로 프론트엔드

Cesium · React · Webpack MPA · Qiankun 기반의 지리공간 분석 플랫폼

 

React TypeScript Cesium Webpack Ant Design Turf.js

 

프로젝트 코드명: [platform]-svc3d-front-dt-general
역할: 3D 지도 위에서 동작하는 공간분석·시뮬레이션 마이크로 앱 모음
배포 형태: UMD 번들 → 메인 3D 플랫폼(Qiankun)에 동적 로드


📋 목차

  1. 프로젝트 개요
  2. 아키텍처 한눈에 보기
  3. 빌드 시스템: Webpack MPA
  4. 마이크로 프론트엔드: Qiankun 통합
  5. 3D 렌더링 레이어: Cesium Analytics
  6. 공통 모듈 설계
  7. 분석 모듈 카탈로그
  8. 공간 알고리즘 구현
  9. API · 환경변수 · 프록시
  10. 상태 관리 패턴
  11. 국제화(i18n)
  12. 개발·빌드·배포
  13. 기술적 도전과 설계 결정
  14. 마무리

🎯 프로젝트 개요

이 프로젝트는 3D 디지털 트윈 플랫폼 위에서 동작하는 공간분석(Spatial Analysis) 마이크로 애플리케이션 모음입니다. 단일 SPA가 아니라, 페이지 단위로 독립 번들을 생성하는 Webpack MPA(Multi-Page Application) 구조를 채택했습니다.

각 마이크로 앱은 메인 3D 뷰어(window.cviewer)를 공유하며, Cesium Globe 위에 GeoJSON·WKT·Shapefile 기반 레이어를 올리고, 백엔드 GIS 엔진과 통신해 분석 결과를 시각화합니다.

핵심 기술 스택

영역기술버전 / 비고

UI Framework React 18.2 (createRoot)
Language TypeScript 4.9
Bundler Webpack 5.x, MPA + UMD
3D Engine @cesiumgs/cesium-analytics 1.115 (External)
UI Library Ant Design 4.24.7
State Redux + React Context 모듈별 혼합
Geo Library Turf.js, proj4, wellknown 좌표·지오메트리 처리
HTTP umi-request (axios 기반) Interceptor + Error Handler
i18n react-intl ko_KR / zh_CN
Micro FE Qiankun bootstrap / mount / unmount
Chart ECharts, @ant-design/charts 결과 시각화
Heatmap cesium-heatmap-es6 3D 히트맵
DevOps Husky, lint-staged, ESLint, Prettier 코드 품질

🏗 아키텍처 한눈에 보기

graph TB
    subgraph MainPlatform["메인 3D 플랫폼 (Qiankun Host)"]
        Viewer["window.cviewer<br/>(Cesium Viewer)"]
        Shell["Shell / Router / Auth"]
    end

    subgraph MicroApps["dt-general 마이크로 앱 (UMD Bundles)"]
        direction TB
        Geo["geospatialTransformation<br/>좌표변환·지오코딩"]
        Spatial["buffer · clip · dissolve<br/>cluster · proximity · hotspot"]
        Sim["floodAnalysis · blackIce<br/>SolarAnalysis · undercurrentAnalysis"]
        Infra["roadinfra · roadLightAnalysis<br/>viewAnalysis · splitScreen"]
    end

    subgraph Backend["백엔드 서비스 (프록시 경유)"]
        Zeta["Zeta GIS Engine<br/>(/apiGen)"]
        GeoAPI["Addr/Geo API<br/>(/apiGeo)"]
        KRRIS["도로대장 API<br/>(/krris)"]
        Tile["3D Tile Server"]
        Py["Python Analysis<br/>(/pytsvr)"]
    end

    Shell -->|loadMicroApp| MicroApps
    MicroApps -->|공유 Viewer| Viewer
    MicroApps -->|REST POST/GET| Backend
    Viewer -->|WMTS/Tiles| Tile

디렉터리 구조

src/
├── common/                  # 전 모듈 공통 컴포넌트·유틸·상수
│   ├── components/          # AnalysisTable, FileUploadButton, Layout ...
│   ├── utils/
│   │   ├── draw/            # Cesium Entity / GeoJsonDataSource 렌더링
│   │   ├── convert/         # WKT, EPSG:5186 좌표 변환
│   │   └── requestData/     # 분석 API 공통 요청 파이프라인
│   ├── constant/            # DataSource 이름, 행정구역 코드, 옵션
│   └── types/               # GeometryType, TableDataSource 등
├── pages/                   # ★ 마이크로 앱 진입점 (Webpack Entry)
│   ├── buffer/
│   ├── hotspot/
│   ├── floodAnalysis/
│   ├── roadinfra/
│   └── ... (20+ modules)
├── locales/                 # ko_KR, zh_CN
├── services/                # 공통 API
└── utils/
    └── request.ts           # HTTP 클라이언트

⚙️ 빌드 시스템: Webpack MPA

가장 특징적인 설계는 Glob 기반 자동 Entry Discovery입니다. src/pages/*/index.tsx 패턴을 스캔해 각 폴더명을 Entry Key로 등록합니다.

// webpack.config.js (핵심 로직 요약)
const ENTRY_ROOT_DIR = "./src/pages";
let entries = {};
let matches = glob.sync(`${ENTRY_ROOT_DIR}/*/index.tsx`);
matches.map(f => {
  let name = f.match(/.\/src\/pages\/(\S*)\/index.tsx/)[1];
  entries[name] = f;
});

빌드 산출물

설정값의미

output.library [name] UMD 모듈명 = 페이지명
output.libraryTarget umd Qiankun / Script Tag 로드
output.filename static/[name].[chunkhash].js 캐시 버스팅
externals (prod) React, Cesium, Turf, ECharts ... CDN/Host 주입

Production Externals 전략은 번들 크기를 대폭 줄입니다. Cesium 단일 라이브러리만 수 MB이므로, Host 앱에서 전역(window.Cesium)으로 한 번만 로드하고 각 마이크로 앱은 참조만 합니다.

// Production externals 예시
externals: {
  "@cesiumgs/cesium-analytics": "Cesium",
  react: 'React',
  'react-dom': 'ReactDOM',
  '@turf/turf': 'turf',
  echarts: 'echarts',
  proj4: 'proj4',
}

CSS Modules + Ant Design Prefix

Ant Design의 CSS 클래스 충돌을 방지하기 위해 @ant-prefix를 패키지명(demo)으로 커스터마이징합니다.

modifyVars: {
  '@ant-prefix': pkg.name  // ant-demo-button, ant-demo-modal ...
}

🔌 마이크로 프론트엔드: Qiankun 통합

모든 페이지 Entry는 동일한 Qiankun Lifecycle Contract를 export합니다.

// pages/*/index.tsx 공통 패턴
function render(props: any) {
  const { container, lang, onCloseMicroApp, user } = props;
  const { message, antLocale, locale } = loadLocale(lang);
  root = createRoot(
    container
      ? container.querySelector('#microapp-demo-roadinfra')
      : document.querySelector('#microapp-demo-roadinfra')
  );
  root.render(
    <Provider store={_store}>
      <IntlProvider messages={message} locale={locale}>
        <ConfigProvider locale={antLocale} prefixCls={pkg.name}>
          <App onCloseMicroApp={onCloseMicroApp} user={user} />
        </ConfigProvider>
      </IntlProvider>
    </Provider>
  );
}

if (!window.__POWERED_BY_QIANKUN__) {
  render({});  // 독립 실행 (로컬 개발)
}

export async function bootstrap() { /* ... */ }
export async function mount(props: any) { render(props); }
export async function unmount(props: any) { root?.unmount(); }

Host ↔ Micro App 데이터 흐름

Prop용도

container Shadow DOM / Portal 마운트 대상
lang ko_KR / zh_CN locale
onCloseMicroApp 분석 패널 닫기 콜백
user 사용자 정보 (분석 이력·권한)
window.cviewer 전역 Cesium Viewer (Host 주입)

💡 설계 포인트: 마이크로 앱은 자체 Viewer를 생성하지 않습니다. Host가 제공하는 window.cviewer를 공유해 GPU·메모리 중복을 방지하고, 앱 전환 시 카메라·타일 상태를 유지합니다.


🌐 3D 렌더링 레이어: Cesium Analytics

GeoJSON → Cesium Entity 파이프라인

공통 유틸 drawFeatureCollection은 Turf GeoJSON FeatureCollection을 GeometryType별로 분기해 Cesium Entity/DataSource에 매핑합니다.

// src/common/utils/draw/drawFeatureCollection.ts
switch (featureType) {
  case GeometryType.Point:
  case GeometryType.MultiPoint:
    drawPoint(featureCollection, color, outlineColor, fileUploadDataSource, isResult, pointSize);
    break;
  case GeometryType.LineString:
  case GeometryType.MultiLineString:
    drawLineString(featureCollection, multiLineStringColor, fileUploadDataSource);
    break;
  case GeometryType.Polygon:
  case GeometryType.MultiPolygon:
    await drawGeoJsonDataSource(featureCollection, color, outlineColor, fileUploadDataSource);
}

Custom WMTS Imagery Provider

인증 헤더가 필요한 타일 서버를 위해 Cesium.UrlTemplateImageryProvider를 확장합니다.

class CustomUrlTemplateImagery extends Cesium.UrlTemplateImageryProvider {
  constructor(props: CustomWMTSProps) {
    props.wmtsProps.url = new Cesium.Resource({
      url: `${props.wmtsProps.url}/${props.templateUrl}`,
      headers: props.headers,  // Authorization, x-lpid, x-uid ...
    });
    super(props.wmtsProps);
  }
}

Draw Tool (@packages/utils)

분석 영역 지정, 포인트 배치, Polyline 그리기 등은 공통 Draw 클래스로 처리합니다.

const draw = new Draw(window.cviewer);
const dataSource = new Cesium.CustomDataSource('drawarea');
window.cviewer.dataSources.add(dataSource);

const action = draw.create(DrawType.Polyline, { useDefaultEntity: false });
action.on(DrawEventType.VertexAdd, (event) => { /* 좌표 수집 */ });

🧩 공통 모듈 설계

Analysis Pipeline (DataAnalyzer)

Shapefile 업로드 → 필터링 → WKT 변환 → 분석 API POST → GeoJSON 결과 → Cesium 렌더링까지의 파이프라인이 DataAnalyzer 클래스로 추상화되어 있습니다.

class DataAnalyzer {
  async postAnalysis(endpoint: string, state: any, isFilterChecked: boolean, isUseWKT?: boolean) {
    // 1. 선택적 속성 필터 (/anals/flter)
    // 2. Request Body 생성 (WKT / GeoJSON)
    // 3. 분석 엔드포인트 POST
    const bodyData = this.createRequestBody(state, filteredTargetName, isUseWKT, filteredCmprName);
    return await this.requestHandler.requestData(endpoint, 'POST', bodyData);
  }
}

공통 UI 컴포넌트

컴포넌트역할

MyModelPanel @component/ui — 플로팅 분석 패널
FileUploadButton SHP/ZIP 업로드 → GeoJSON 파싱
AnalysisTable 분석 이력·결과 테이블
DistrictSelectors 시·도 / 시·군·구 / 읍·면·동 행정구역 필터
FilterContainer 그리기·행정구역·속성 필터 통합
HotspotLegend / ClusterLegend 분석 결과 범례

좌표계 변환

한국 공간정보 표준 좌표계를 proj4로 정의·변환합니다.

proj4.defs("EPSG:5186", "+proj=tmerc +lat_0=38 +lon_0=127 +k=1 +x_0=200000 +y_0=600000 +ellps=GRS80 +units=m +no_defs");
proj4.defs('EPSG:5179', '+proj=tmerc +lat_0=38 +lon_0=127.5 +k=0.9996 +x_0=1000000 +y_0=2000000 +ellps=GRS80 +units=m +no_defs');

📦 분석 모듈 카탈로그

🗺 GIS 벡터 분석

모듈설명핵심 기술

buffer 점 레이어 기준 버퍼 영역 생성 Turf buffer, WKT POST
clip 클립 레이어 경계로 입력 레이어 자르기 Polygon intersection
dissolve 속성 기준 폴리곤 병합 Backend dissolve API
cluster K-Means 군집 분석 클러스터별 Color 매핑
proximity 최근접점(Nearest Neighbor) 선형 연결 Turf nearestPoint
hotspot Getis-Ord Gi* / Local Moran's I 프론트엔드 Gi 직접 구현*
density 커널 밀도 추정 Heatmap 시각화
spaceDivision 공간 분할(Voronoi 등) Polygon generation
layerOverlapping 레이어 중첩 분석 Overlay 연산
layerMerged 레이어 병합(Merge) Field mapping

🌊 시뮬레이션 · 환경 분석

모듈설명핵심 기술

floodAnalysis 홍수 침수 시뮬레이션 Water level animation, Turf intersection, EPSG:5186
blackIce 블랙아이스(결빙) 위험 분석 Cesium Entity, 기상 데이터 연동
SolarAnalysis 일조 분석 globe.enableLighting, Shadow volume
undercurrentAnalysis 지하수·함양 분석 CustomDataSource 다중 레이어
viewAnalysis 가시권(Viewshed) 분석 3D Tileset + Draw point

🛣 도로 · 인프라

모듈설명핵심 기술

roadinfra 도로대장·3D 모델 라이브러리 KRRIS API, GLB 모델 배치
roadLightAnalysis 가로등 주간 분석 Cesium Model Entity
roadLightAnalysisShadow 가로등 야간·그림자 분석 Cylinder light volume, GLB silhouette

🔧 유틸리티

모듈설명

geospatialTransformation 좌표변환, 역지오코딩, 레이어 발행
splitScreen 듀얼 뷰어 분할 비교
downloadTest 뷰어 캡처·다운로드 테스트

🧮 공간 알고리즘 구현

Getis-Ord Gi* (Hotspot Analysis)

Hotspot 모듈은 Getis-Ord Gi 통계량을 프론트엔드에서 직접 계산합니다. Turf.js의 distance 함수로 이웃 관계를 판별하고, z-score를 산출합니다.

// src/pages/hotspot/utils/calculateGiStar.ts
points.features.forEach((point, i) => {
  const neighbors = points.features.filter((otherPoint, j) => {
    if (i !== j) {
      const calculatedDistance = distance(point, otherPoint, { units: distanceUnit });
      return calculatedDistance <= distanceThreshold;
    }
    return false;
  });

  const wij = neighbors.length + 1;
  const wijSum = neighbors.map(n => n.properties[variable])
    .reduce((a, b) => a + b, point.properties[variable]);

  const numerator = wijSum - mean * wij;
  const denominator = s * Math.sqrt((n * wij - Math.pow(wij, 2)) / (n - 1));
  point.properties.giStar = denominator === 0 ? 0 : numerator / denominator;
});

계산된 Gi* 값은 assignGiBin으로 구간화되어 Cesium Point Color에 매핑됩니다.

홍수 시뮬레이션 (floodAnalysis)

  1. 사용자가 폴리곤 영역 Draw
  2. 행정구역(시·군·구·읍면동) 선택
  3. Safemap / Zeta API로 침수 데이터 조회
  4. Water Level Slider → Polygon Hierarchy 애니메이션
  5. Turf intersect로 도로·건물 교차 영역 계산

야간 가로등 시각화 (roadLightAnalysisShadow)

Cesium Cylinder + Model(GLB) 조합으로 조명 범위를 3D 표현합니다.

const nightLightOptions = {
  STREET: { lightRadius: 6, lightColor: Color.YELLOW.withAlpha(0.3) },
  SECURITY: { lightRadius: 4, lightColor: Color.YELLOW.withAlpha(0.3), silhouetteColor: Color.RED.withAlpha(0.3) },
};

// Cylinder = 조명 범위, Model = 가로등 3D 메시
entity.cylinder = {
  length: 4,
  bottomRadius: brightness ?? nightLightOptions[lightType].lightRadius,
  material: nightLightOptions[lightType].lightColor,
  heightReference: HeightReference.CLAMP_TO_GROUND,
};

🔗 API · 환경변수 · 프록시

환경변수 (.env)

변수용도

REACT_APP_IS_PROD prod/dev 분기
REACT_APP_PROD_ZETA_API_URL Zeta GIS 분석 API
REACT_APP_DEV_ZETA_API_URL 개발 환경 API
REACT_APP_PROD_KRRIS_API_KEY 도로대장 API Key
REACT_APP_PROD_SERVICE_API_URL 3D 모델 라이브러리 API
REACT_APP_PROD_TILE_API_URL 3D Tileset URL
REACT_APP_SAFEMAP_API_KEY Safemap 침수 API Key

⚠️ 민감 정보 주의: API Key, 내부 IP, 서비스명은 .env와 Nginx 설정에서 관리하며, 소스코드·문서에 직접 노출하지 않습니다.

Nginx Reverse Proxy (개발 환경 예시)

내부 IP 및 서비스명은 마스킹 처리했습니다.

# GIS 분석 엔진
location /apiGen {
    proxy_pass http://***.***.***.***:8000/[platform]-svc3d-back-dt-ge-be/;
}

# 도로대장 API
location /krris {
    proxy_pass https://***.***.***.***:8080/;
}

# 좌표·주소 변환
location /apiGeo {
    proxy_pass http://***.***.***.***:8000/addrgeo/;
}

# Python 분석 서버
location /pytsvr {
    proxy_pass http://***.***.***.***:8190$tmp;
}

# 3D 타일 프록시
location /tile8091 {
    proxy_pass http://***.***.***.***:8000/tile8091/$tmp;
}

Road Infra API 예시

// 환경별 Base URL 분기
const isProd = process.env.REACT_APP_IS_PROD === 'true';
let baseUrl = isProd
  ? process.env.REACT_APP_PROD_ZETA_API_URL
  : process.env.REACT_APP_DEV_ZETA_API_URL;

// 코드 목록 병렬 조회
export async function getCodeList() {
  const promises = [
    request(`${baseUrl}/api/roadinfra/mcos`),   // 관리기관
    request(`${baseUrl}/api/roadinfra/lines`),   // 도로노선
    request(`${baseUrl}/api/roadinfra/sects`),   // 도로구간
    request(`${baseUrl}/api/roadinfra/roads`),   // 공간정보 항목
  ];
  return Promise.all(promises);
}

// 도로대장 인프라 조회 (POST)
export async function fetchKrrisApi(param) {
  return request(`${baseUrl}/api/roadinfra/roads/infra`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      roadNo: param.selectedLine,
      sect: param.selectedSect,
      layerId: param.selectedGeo,
      scrtkyCn: krrisKey,  // 환경변수에서 주입
    }),
  });
}

🗂 상태 관리 패턴

모듈마다 두 가지 패턴이 혼재합니다.

Pattern A: Redux (classical)

store.ts → reducers → actions → connect(mapStateToProps)

  • 사용 모듈: floodAnalysis, SolarAnalysis, viewAnalysis, roadinfra, blackIce ...
  • 적합: 복잡한 Draw Tool 상태, 다중 DataSource, 비동기 분석 플로우

Pattern B: React Context + useReducer

contexts/AppProvider.tsx → useAppState() hook

  • 사용 모듈: buffer, clip, hotspot, cluster, proximity ...
  • 적합: Form 중심 분석 UI, 공통 AnalysisTable 패턴
// buffer/App.tsx — Context 패턴 + 공통 Layout
const { updateState } = useAppState();

useEffect(() => {
  return () => {
    window.cviewer.dataSources.removeAll();
    window.cviewer.entities.removeAll();
  };
}, []);

🧹 Unmount Cleanup: 모든 모듈은 unmount 시 Cesium Entity/DataSource를 정리해 메모리 누수와 레이어 잔존을 방지합니다.


🌏 국제화(i18n)

react-intl + Ant Design Locale을 Host로부터 받은 lang prop으로 동적 로드합니다.

function loadLocale(lang: string) {
  switch (lang) {
    case 'ko_KR': return { locale: 'ko-KR', message: koKR, antLocale: ko_KR };
    case 'zh_CN': return { locale: 'zh-CN', message: zhCN, antLocale: zh_CN };
    default:      return { locale: 'ko-KR', message: koKR, antLocale: ko_KR };
  }
}

🚀 개발·빌드·배포

# 로컬 개발 서버 (HMR)
npm run start          # webpack serve

# Production 빌드
npm run build:prod     # dist/{moduleName}.html + static/*.js

# 코드 품질
npm run lint           # ESLint --fix
# pre-commit: lint-staged → prettier

빌드 산출물 구조

dist/
├── buffer.html
├── hotspot.html
├── floodAnalysis.html
├── roadinfra.html
├── ...
└── static/
    ├── buffer.[hash].js
    ├── hotspot.[hash].js
    └── ...

각 HTML은 해당 마이크로 앱 UMD 번들만 로드하며, Production에서는 React·Cesium 등 External CDN 스크립트를 Host HTML에서 선행 로드합니다.

PWA (Production)

workbox-webpack-plugin의 GenerateSW로 Service Worker를 생성해 정적 자산 캐싱을 지원합니다.


💡 기술적 도전과 설계 결정

1. MPA vs SPA

MPA (채택)SPA

번들 크기 모듈별 분리,按需 로드 단일 대형 번들
배포 모듈 독립 배포 가능 전체 재배포
Qiankun UMD per module = 자연스러운 fit 추가 splitting 필요

2. Cesium External 전략

Cesium WASM + Worker + Assets는 Host에서 1회 로드. 마이크로 앱은 @cesiumgs/cesium-analytics를 import하되 webpack externals로 번들에서 제외.

3. 공유 Viewer vs 독립 Viewer

공유 window.cviewer 채택으로:

  • ✅ GPU Context 단일화
  • ✅ 카메라·타일 캐시 공유
  • ⚠️ Unmount 시 Entity cleanup 필수 (모듈 간 간섭 방지)

4. 프론트엔드 Gi* vs 백엔드 분석

Hotspot의 Gi*는 클라이언트 계산으로 구현. 소규모 Point 데이터셋에서 네트워크 RTT 없이 즉시 결과 표출. 대용량은 백엔드 /pytsvr 위임.

5. Ant Design CSS Isolation

Micro FE 환경에서 Ant Design CSS 충돌은 @ant-prefix 커스터마이징 + CSS Modules(demo-[name]-[local]-[hash])로 해결.


🏁 마무리

이 프로젝트는 3D 디지털 트윈이라는 특수한 런타임 환경 위에서 20개 이상의 GIS 분석 도구를 마이크로 프론트엔드로 제공하는 프론트엔드 플랫폼입니다.

핵심 설계 철학을 정리하면:

  1. Webpack MPA + UMD — 모듈 독립성과 Qiankun 호환
  2. Cesium 공유 Viewer — 3D 성능 최적화
  3. Common Layer 추상화 — Draw, Convert, DataAnalyzer 재사용
  4. Turf.js + proj4 — 클라이언트 GIS 연산
  5. 환경변수 기반 API 분기 — dev/prod 안전한 설정 관리

공간분석, 시뮬레이션, 도로 인프라까지 아우르는 이 아키텍처는 **"하나의 3D 지도 위에서 수십 가지 분석을 플러그인처럼 실행"**하는 디지털 트윈 UX를 구현합니다.


 

Built with React · TypeScript · Cesium · Turf.js · Webpack · Qiankun

 

※ 본 문서의 내부 IP, API Key, 서비스명은 보안을 위해 마스킹 처리되었습니다.