3D 건물 모델링 마이크로프론트엔드
Cesium 기반 3D GIS 플랫폼 위에서 동작하는 건물 생성·층 분할·텍스처 매핑 마이크로 앱의 아키텍처와 구현을 깊이 있게 다룹니다.
React 18Webpack 5 UMDQiankunCesium 1.115ReduxFME Server3D TilesNginx Gateway
민감 정보 안내
본 문서의 IP 주소, 내부 서비스명, 조직 식별자는 <span class="blur">마스킹 처리</span> 되어 있습니다. 실제 운영 환경 값은 공개하지 않습니다.
목차
- 프로젝트 개요
- 모노레포 & 마이크로프론트엔드 아키텍처
- Webpack 멀티 엔트리 & UMD 번들링
- Qiankun 생명주기 통합
- 핵심 기능: 3D 건물 모델링 파이프라인
- Cesium 렌더링 & 지형 샘플링
- 백엔드 API & FME 변환 체인
- 환경별 빌드 전략
- Nginx 리버스 프록시 & 배포
- Architecture vs Architecture2
- 기술적 인사이트 & 설계 결정
1. 프로젝트 개요
이 프로젝트는 3D GIS 플랫폼의 마이크로프론트엔드(Micro-Frontend) 모듈로, 사용자가 지도 위에서 건물 Footprint를 그리고, 층별로 분할하며, 텍스처를 입힌 뒤 3D Tiles 형태로 변환·등록하는 End-to-End 워크플로우를 제공합니다.
2마이크로 앱 엔트리
UMD번들 출력 포맷
8081Dev Server Port
5174좌표계 (EPSG)
해결하는 문제
문제접근 방식
| 대형 3D GIS 앱의 독립 배포 | Qiankun 기반 MF + Webpack UMD |
| SHP → CityGML → 3D Tiles 변환 | FME Server 파이프라인 연동 |
| 지형 위 정확한 건물 배치 | Cesium sampleTerrainMostDetailed |
| 층별 텍스처 매핑 | ImageMaterialProperty + CustomDataSource |
| 다국어 UI | react-intl (ko / en / zh) |
2. 모노레포 & 마이크로프론트엔드 아키텍처
프로젝트는 Yarn Workspaces + Turbo 기반 모노레포의 projects/building3D 워크스페이스입니다. 공유 패키지를 통해 UI·서비스·유틸리티를 재사용합니다.
monorepo/
├── packages/
│ ├── uicomponents/ # @component/ui — 공통 UI (MyModelPanel, Table, MyButton)
│ ├── servicecomponents/ # @component/service — Texture3D 등 3D 서비스 컴포넌트
│ └── utils/ # @packages/utils — Draw, download, removeTileset
├── projects/
│ └── building3D/ # ★ 본 마이크로 앱
│ ├── src/pages/
│ │ ├── Architecture/ # v1 — 기본 건물 모델링
│ │ └── Architecture2/ # v2 — 3D Tiles·레이어 등록 확장
│ ├── webpack.config.js
│ └── dist/
├── default.conf # Nginx 게이트웨이 설정
├── Dockerfile
└── turbo.json
시스템 아키텍처
flowchart TB
subgraph Host["메인 프레임워크 (Qiankun Host)"]
Shell["3D Map Shell App"]
end
subgraph MF["building3D Micro-Frontend"]
Entry["index.tsx<br/>bootstrap / mount / unmount"]
App["app.tsx"]
Arch["Architecture 컴포넌트"]
Floor["FloorDivision"]
Upload["UploadData"]
Layer["LayerRegis (v2)"]
end
subgraph Shared["공유 레이어"]
UI["@component/ui"]
SVC["@component/service"]
UTIL["@packages/utils"]
end
subgraph Render["3D 렌더링"]
Cesium["Cesium Viewer<br/>(window.cviewer)"]
Tiles["3D Tileset"]
Entities["CustomDataSource / Entities"]
end
subgraph Backend["백엔드 서비스"]
DTAPI["/dtapi/rhino-3dbld"]
FME["/fmeserver — FME Transform"]
FileSvr["File Server / 3D Buildings Tiler"]
GeoSvr["GeoServer"]
end
Shell -->|"loadMicroApp()"| Entry
Entry --> App --> Arch & Floor & Upload & Layer
App --> UI & SVC & UTIL
Arch & Floor --> Cesium
Cesium --> Tiles & Entities
Arch --> DTAPI
Floor --> FME
FME --> FileSvr --> Tiles
Layer --> DTAPI
3. Webpack 멀티 엔트리 & UMD 번들링
마이크로프론트엔드의 핵심은 독립 번들 + 런타임 로드입니다. Webpack 설정은 src/pages/*/index.tsx 패턴을 glob으로 탐색하여 자동 멀티 엔트리를 생성합니다.
엔트리 자동 탐색
const ENTRY_ROOT_DIR = "./src/pages";
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; // Architecture, Architecture2
});
UMD 출력 — Qiankun 호환
output: {
path: path.resolve(__dirname, "dist"),
filename: 'static/[name].[chunkhash].js',
library: `[name]`, // 전역 변수명 = 엔트리명
libraryTarget: 'umd' // Universal Module Definition
}
설정목적
| libraryTarget: 'umd' | Host 앱이 <script> 또는 import()로 로드 가능 |
| library: [name] | Architecture, Architecture2 각각 독립 전역 |
| chunkhash | 캐시 무효화 & CDN 배포 |
| Access-Control-Allow-Origin: * | Dev Server CORS — 크로스 오리진 MF 로드 |
Externals — 번들 크기 최적화
Production / Portal 빌드 시 무거운 라이브러리를 Host가 제공하는 CDN 전역 변수로 외부화합니다.
externals: {
"@cesiumgs/cesium-analytics": "Cesium",
react: 'React',
'react-dom': 'ReactDOM',
moment: 'moment',
'@turf/turf': 'turf',
echarts: 'echarts',
proj4: 'proj4',
wellknown: 'wellknown',
}
Cesium 단일 번들은 수 MB에 달합니다. External 처리로 MF 번들을 수백 KB 수준으로 유지할 수 있습니다.
CSS Modules & Ant Design 프리픽스
// CSS Modules — 컴포넌트 스코프 격리
localIdentName: `building3D-[name]-[local]-[hash:base64:20]`
// Ant Design — MF 간 스타일 충돌 방지
modifyVars: { '@ant-prefix': 'building3D' }
4. Qiankun 생명주기 통합
각 페이지 엔트리(Architecture/index.tsx)는 Qiankun이 요구하는 3개의 필수 export를 구현합니다.
export async function bootstrap() {
// 앱 초기화 (1회)
}
export async function mount(props: any) {
// Host로부터 user, container, lang, onCloseMicroApp 등 수신
render(props);
}
export async function unmount(props: any) {
root.unmount(); // React 18 createRoot 정리
}
Host ↔ MF Props 계약
Prop타입역할
| user | { userid, ... } | API 호출 시 사용자 식별 |
| container | HTMLElement | Shadow DOM / Portal 내 마운트 지점 |
| lang | string | react-intl 로케일 (ko_KR / en_US / zh_CN) |
| onCloseMicroApp | () => void | MF 종료 콜백 — Cesium Entity 정리 후 호출 |
마운트 DOM 구조
<!-- Architecture/index.html -->
<div id="microapp-building3D-Architecture"></div>
<!-- Architecture2/index.html -->
<div id="microapp-building3D-Architecture2"></div>
React 18 createRoot는 Host가 제공한 container 내부의 #microapp-building3D-* 엘리먼트를 타겟으로 합니다.
5. 핵심 기능: 3D 건물 모델링 파이프라인
5.1 전체 워크플로우
sequenceDiagram
actor User
participant UI as Architecture UI
participant Cesium as Cesium Viewer
participant API as DT API
participant FME as FME Server
participant Tiler as 3D Buildings Tiler
User->>UI: 건물 목록 조회
UI->>API: GET /dtapi/rhino-3dbld/list/{page}/{size}
API-->>UI: 건물 목록 (status: 0~3)
User->>UI: 신규 건물 생성
UI->>UI: FloorDivision — Footprint 그리기
User->>Cesium: Draw Tool로 Polygon/Wall 그리기
Cesium->>Cesium: sampleTerrainMostDetailed (지형 고도)
User->>UI: 층별 높이·텍스처 설정
UI->>API: POST /dtapi/rhino-3dbld (GeoJSON 저장)
User->>UI: SHP 업로드 (v2)
UI->>FME: SHP → CityGML (EPSG:5174)
FME-->>UI: Job ID
UI->>FME: SHP → 3D Tiles
FME-->>UI: 변환 완료
UI->>Tiler: GML → 3D Tiles Tiling
Tiler-->>UI: tileset.json URL
User->>UI: 레이어 등록 (v2)
UI->>API: POST /dtapi/rhino-3dbld/layer
5.2 Architecture — 건물 CRUD
메인 패널에서 건물 목록을 페이지네이션으로 관리합니다.
APIMethod설명
| /dtapi/rhino-3dbld/list/{pageNum}/{pageSize} | GET | 목록 조회 (카테고리·건물명 필터) |
| /dtapi/rhino-3dbld | POST | 신규 3D 건물 저장 |
| /dtapi/rhino-3dbld/{id} | GET | 단건 조회 |
| /dtapi/rhino-3dbld | PUT | 수정 |
| /dtapi/rhino-3dbld/delete | POST | 삭제 |
| /dtapi/rhino-3dbld/thumbnail | PATCH | 썸네일 갱신 |
| /dtapi/rhino-3dbld/upload/geojson | POST | GeoJSON 업로드 |
| /dtapi/rhino-3dbld/upload | POST | 3D 데이터 업로드 |
| /dtapi/rhino-3dbld/file | POST/DELETE | SHP ZIP 업·삭제 |
| /dtapi/rhino-3dbld/layer | POST | 레이어 등록 |
상태 코드 (status 필드)
값UI 표시의미
| 0 | 대기중 | 변환 Job 대기 |
| 1 | 진행중 | FME / Tiler 처리 중 |
| 2 | 성공 | 3D Tiles 생성 완료 |
| 3 | 실패 | 변환 오류 |
5.3 FloorDivision — 층 분할 & 3D 프리뷰
가장 기술 밀도가 높은 컴포넌트입니다.
- Draw Tool (@packages/utils/Draw)로 Cesium 위 Polygon/Wall 그리기
- Turf.js — Footprint 기하 연산
- 지형 샘플링 — 각 꼭짓점의 실제 고도 획득
- CustomDataSource — 층별 Entity (Wall / Polygon) 생성
- ImageMaterialProperty — 층별 텍스처 실시간 프리뷰
// 지형 고도 샘플링 → 건물 바닥면 정렬
let result = await sampleTerrainMostDetailed(
view?.terrainProvider as TerrainProvider,
polygon // Cartographic[]
);
// 층별 Wall Entity 생성
item.wall.material = new ImageMaterialProperty({
image: AppConfig.fileUrl + obj.imgUrl,
repeat: new Cartesian3(10, 2)
});
5.4 UploadData — 썸네일 생성
Ant Design Dragger 컴포넌트로 썸네일 이미지를 업로드하고, change3DBuilding API로 메타데이터를 갱신합니다.
const formData = new FormData();
formData.append('multipartFile', file);
formData.append('userAccount', user.userid);
formData.append('path', changeNewData[0].bldgFilePath);
await upload3DData(formData);
5.5 Texture3D — 텍스처 선택 UI
@component/service의 Texture3D 컴포넌트를 통해 텍스처 라이브러리에서 이미지를 선택하면 imgClickCallback으로 Cesium Entity Material이 실시간 교체됩니다.
6. Cesium 렌더링 & 지형 샘플링
CustomDataSource 패턴
dataSource = new CustomDataSource('wall');
view?.dataSources.add(dataSource);
// MF 종료 시 정리 (Architecture2)
view.dataSources.remove(dataSource);
view.entities.remove(entity);
view.scene.primitives.remove(tileSet);
GeoJSON → 3D Entity 변환
저장된 GeoJSON을 불러올 때 combineData() 함수가 FeatureCollection을 순회하며:
- WGS84 좌표 → Cartographic 변환
- sampleTerrainMostDetailed로 지형 고도 반영
- PolygonHierarchy + Extruded Height로 3D Wall/Polygon Entity 생성
Draw Action 관리
draw = new Draw(view);
// DrawType.Polygon, DrawEventType.Complete 등
// action 완료 시 GeoJSON Feature 생성 → Redux store 저장
7. 백엔드 API & FME 변환 체인
FME Server 변환 파이프라인
SHP 파일을 EPSG:5174 (Korea 2000 / Central Belt) 좌표계 기준으로 변환합니다.
flowchart LR
SHP["SHP (ZIP)"] --> FME1["FME: SHP → CityGML"]
SHP --> FME2["FME: SHP → 3D Tiles"]
FME1 --> GML["CityGML"]
GML --> Tiler["3D Buildings Tiler"]
Tiler --> Tiles["tileset.json + b3dm"]
FME2 --> Tiles
변환 유형FME Workspace (마스킹)출력
| 내부 도로 | SHP_to_CityGML_internal_***.fmw | CityGML (5174) |
| 실내 이동 경로 | SHP_to_CityGML_indoor_***.fmw | CityGML (5174) |
| 입체 도로 | SHP_to_CityGML_dimension_***.fmw | CityGML (5174) |
| 3D Tiles (도로명) | SHP_to_3DTiles_roadname_***.fmw | 3D Tiles |
| 3D Tiles (입체) | SHP_to_3DTiles_dimension_***.fmw | 3D Tiles |
HTTP Request 인터셉터
umi-request 기반의 공통 HTTP 클라이언트는 다음을 자동 처리합니다.
// FME Server 인증
if (url.startsWith('/fmeserver')) {
options.headers['Authorization'] = 'fmetoken token=' + AppConfig.fmetoken;
}
// 사용자 정보 자동 주입
Object.assign(options.data, {
userId: userInfo.account,
userAccount: userInfo.account
});
// JWT 토큰
options.headers['accessToken'] = accessToken;
options.headers['refreshToken'] = refreshToken;
보안 참고: FME Token, API URL 등은 AppConfig에서 환경별로 분기됩니다. 블로그 목적상 토큰 값은 공개하지 않으며, 운영 환경에서는 반드시 환경 변수 / Secret Manager로 관리해야 합니다.
8. 환경별 빌드 전략
Webpack은 process.env.appConfigGbn을 DefinePlugin으로 주입하여 런타임 환경 분기를 구현합니다.
빌드 명령appConfigGbn용도
| yarn start | "" (development) | 로컬 개발 |
| yarn build | "production" | Production |
| yarn build:lx-portal | "lx-portal" | Portal 환경 |
| yarn start:lx-portal | "lx-portal" | Portal 로컬 |
AppConfig 분기 예시 (민감 정보 마스킹)
if (process.env.appConfigGbn === "lx-portal") {
tempAppConfig = {
fileUrl: '/wastomcat/uav-model/',
uploadUrl: 'https://<span class="blur">internal-portal-host</span>/server/other/fileInfo/readSHP',
apiUrl: 'https://<span class="blur">internal-portal-host</span>/server',
downLoadUrl: 'https://<span class="blur">internal-portal-host</span>',
fmetoken: '<span class="blur">***REDACTED***</span>',
};
} else {
tempAppConfig = {
fileUrl: './wastomcat/uav-model',
uploadUrl: 'http://<span class="blur">10.x.x.x</span>:8087/server/other/fileInfo/readSHP',
apiUrl: 'http://<span class="blur">10.x.x.x</span>:8087/server',
downLoadUrl: 'http://<span class="blur">10.x.x.x</span>:8087',
fmetoken: '<span class="blur">***REDACTED***</span>',
};
}
Portal 환경에서는 Request Interceptor가 URL 앞에 .을 붙여 상대 경로 프록시를 활성화합니다.
if (process.env.appConfigGbn === "lx-portal" || "op-portal") {
if (url.startsWith('/')) {
url = '.' + url; // Nginx 프록시 경유
}
}
9. Nginx 리버스 프록시 & 배포
Docker 이미지
FROM <span class="blur">internal-registry</span>/nginx_unprivileged:1.0
COPY projects/building3D/dist /usr/share/nginx/html/building3D
COPY default.conf /etc/nginx/conf.d/
EXPOSE 80
Nginx 게이트웨이 역할
default.conf는 단일 진입점(Single Entry Point) 으로 수십 개의 백엔드 서비스를 프록시합니다.
Location프록시 대상 (마스킹)역할
| /dtapi | OCP Route / Back-end Pod | 3D 건물 DT API |
| /fmeserver/ | 10.x.x.x:8050 | FME 변환 서버 |
| /server | 10.x.x.x:8000 | 공통 Admin API |
| /geosvr | 10.x.x.x:20080 | GeoServer |
| /fileServer/api/cesium/3d-buildings-tiler | File Server | 3D Tiles 생성 |
| /share-files | /dt_nas/SHARE | NAS 공유 파일 |
| /vworld_api/ | api.vworld.kr | 브이월드 API |
| /microapp-*/ | 10.x.x.x:8080 | 타 MF 앱 프록시 |
Kubernetes 배포
# deploy/all-in-one/devops-*-web-master.yaml (마스킹)
spec:
containers:
- image: __IMAGE__
name: <span class="blur">web-master</span>
ports:
- containerPort: 8081
10. Architecture vs Architecture2
동일 도메인의 두 버전이 공존하며, Architecture2가 확장 기능을 포함합니다.
기능Architecture (v1)Architecture2 (v2)
| 건물 CRUD | ✅ | ✅ |
| FloorDivision (층 분할) | ✅ | ✅ |
| Texture3D (텍스처) | ✅ | ✅ |
| UploadData (썸네일) | ✅ | ✅ |
| SHP → 3D Tiles 파이프라인 | ❌ | ✅ |
| LayerRegis (레이어 등록) | ❌ | ✅ |
| Tileset / Entity 정리 (unmount) | 기본 | ✅ 고급 cleanup |
| deleteShpZip / deleteTilesFolder | ❌ | ✅ |
| Custom ModelPanel | ❌ | ✅ |
Architecture2의 handleClose()는 MF 종료 시 Cesium Scene을 완전히 정리합니다.
const handleClose = () => {
if (entity) window.cviewer.entities.remove(entity);
if (polygon) window.cviewer.entities.remove(polygon);
if (dataSource) window.cviewer.dataSources.remove(dataSource);
if (geoJson) window.cviewer.dataSources.remove(geoJson);
if (tileSet) window.cviewer.scene.primitives.remove(tileSet);
if (shpUrl) deleteShpZip({ id: shpUrl });
if (tilePath) deleteTilesFolder({ inpath: tilePath });
onCloseMicroApp();
};
11. 기술적 인사이트 & 설계 결정
왜 Webpack UMD인가?
대안채택하지 않은 이유
| Module Federation | Host와 강한 Webpack 버전 종속 |
| iframe | Cesium Viewer 공유 불가, 성능 저하 |
| Single SPA | Cesium 생태계와 React 18 호환 이슈 |
| UMD + Qiankun | ✅ Host CDN 공유, 독립 배포, Cesium 글로벌 공유 |
Redux 단순화 패턴
복잡한 Redux Toolkit 대신 단일 SET_STATE 액션으로 모든 상태를 병합합니다.
case SET_STATE:
return { ...state, ...action.payload };
MF 규모에서는 Boilerplate 최소화가 유지보수성에 유리합니다.
Cesium External 전략
Host 앱이 Cesium을 **글로벌 window.Cesium**으로 로드하면, MF는 @cesiumgs/cesium-analytics를 import하되 번들에는 포함되지 않습니다. window.cviewer는 Host가 생성한 Viewer 인스턴스를 MF가 공유합니다.
다국어 (i18n)
// locales/ko_KR.ts, en_US.ts, zh_CN.ts
<IntlProvider messages={message} locale={locale}>
{intl.formatMessage({ id: 'FloorDivision.name9' })}
</IntlProvider>
Host로부터 lang prop을 받아 로케일을 동적으로 전환합니다.
기술 스택 요약
레이어기술버전
| UI Framework | React | 18.2 |
| UI Library | Ant Design | 4.24.7 |
| State | Redux + react-redux | 5.0 / 9.1 |
| 3D Engine | Cesium Analytics | 1.115.0 |
| Bundler | Webpack | 5.75 |
| MF Framework | Qiankun (via Umi plugin) | 2.x |
| HTTP Client | umi-request | 1.4 |
| GIS Utils | Turf.js, proj4, wellknown | 6.5 / 2.7 / 0.5 |
| Charts | ECharts, Ant Design Charts | 5.4 / 1.3 |
| i18n | react-intl | 6.2 |
| Build Orchestration | Turbo + Yarn Workspaces | 1.9 / 1.22 |
| Web Server | Nginx (unprivileged) | 1.0 |
| Container | Docker + Kubernetes | — |
| PWA | Workbox (GenerateSW) | 6.5 |
로컬 개발 가이드
# 모노레포 루트 — 의존성 설치
yarn install
# building3D 개발 서버 (port 8081)
cd projects/building3D
yarn start
# Production 빌드
yarn build
# Portal 환경 빌드
yarn build:lx-portal
Host 앱 연동 팁: 단독 실행 시 Cesium Viewer(window.cviewer)가 없으므로 일부 3D 기능은 동작하지 않습니다. Qiankun Host 앱 위에서 loadMicroApp으로 로드하거나, Dev 환경에서 Mock Viewer를 주입해야 합니다.
마치며
이 프로젝트는 3D GIS 플랫폼의 마이크로프론트엔드 모듈로서, Webpack UMD + Qiankun 패턴으로 독립 배포 가능성을 확보하면서, Cesium 기반 실시간 3D 프리뷰와 FME Server 기반 대용량 공간 데이터 변환 파이프라인을 하나의 UX로 통합했습니다.
특히 지형 샘플링 기반 건물 배치, 층별 ImageMaterialProperty 텍스처 매핑, SHP → CityGML → 3D Tiles End-to-End 변환은 공간정보 프론트엔드에서 재현 가능한 패턴으로, 유사 프로젝트의 아키텍처 레퍼런스로 활용할 수 있습니다.
'Frontend' 카테고리의 다른 글
| [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 |
| [Frontend] JavaScript 정복하기 (0) | 2026.07.05 |