🌍 3D 디지털 트윈 공간분석 마이크로 프론트엔드
Cesium · React · Webpack MPA · Qiankun 기반의 지리공간 분석 플랫폼
React TypeScript Cesium Webpack Ant Design Turf.js
프로젝트 코드명: [platform]-svc3d-front-dt-general
역할: 3D 지도 위에서 동작하는 공간분석·시뮬레이션 마이크로 앱 모음
배포 형태: UMD 번들 → 메인 3D 플랫폼(Qiankun)에 동적 로드
📋 목차
- 프로젝트 개요
- 아키텍처 한눈에 보기
- 빌드 시스템: Webpack MPA
- 마이크로 프론트엔드: Qiankun 통합
- 3D 렌더링 레이어: Cesium Analytics
- 공통 모듈 설계
- 분석 모듈 카탈로그
- 공간 알고리즘 구현
- API · 환경변수 · 프록시
- 상태 관리 패턴
- 국제화(i18n)
- 개발·빌드·배포
- 기술적 도전과 설계 결정
- 마무리
🎯 프로젝트 개요
이 프로젝트는 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)
- 사용자가 폴리곤 영역 Draw
- 행정구역(시·군·구·읍면동) 선택
- Safemap / Zeta API로 침수 데이터 조회
- Water Level Slider → Polygon Hierarchy 애니메이션
- 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 분석 도구를 마이크로 프론트엔드로 제공하는 프론트엔드 플랫폼입니다.
핵심 설계 철학을 정리하면:
- Webpack MPA + UMD — 모듈 독립성과 Qiankun 호환
- Cesium 공유 Viewer — 3D 성능 최적화
- Common Layer 추상화 — Draw, Convert, DataAnalyzer 재사용
- Turf.js + proj4 — 클라이언트 GIS 연산
- 환경변수 기반 API 분기 — dev/prod 안전한 설정 관리
공간분석, 시뮬레이션, 도로 인프라까지 아우르는 이 아키텍처는 **"하나의 3D 지도 위에서 수십 가지 분석을 플러그인처럼 실행"**하는 디지털 트윈 UX를 구현합니다.
Built with React · TypeScript · Cesium · Turf.js · Webpack · Qiankun
※ 본 문서의 내부 IP, API Key, 서비스명은 보안을 위해 마스킹 처리되었습니다.
'Frontend' 카테고리의 다른 글
| [Frontend] Webpack vs Vite 번들링 원리부터 이해하기 (1) | 2026.07.07 |
|---|---|
| [Frontend] React가 Virtual DOM을 사용하는 이유(성능 측정) (0) | 2026.07.07 |
| [Frontend] 3D 건물 모델링 기능 구현 (feat. MSA) (0) | 2026.07.06 |
| [Frontend] 3D 지형 종/횡단면 분석 기능 구현 (feat. MSA) (0) | 2026.07.05 |
| [Frontend] 3D 시뮬레이션 기능 구현 (feat. MSA) (1) | 2026.07.05 |