FLOW-3D 교량 세굴 3D 대시보드
수치해석 결과를 브라우저에서 직접 탐색하는 WebGL 기반 시각화 플랫폼.
하상 변화, 유체장, 교각 안전도를 단일 시간축으로 동기화합니다.
Three.js 0.169TypeScript StrictVite 5VitestDocker + NginxZero UI Framework
109
TypeScript 모듈
17
단위 테스트 스위트
80K
최대 유체 포인트
1
updateAtTime(t) 규약
목차
- 왜 이 프로젝트인가
- 시스템 아키텍처
- 핵심 설계: 단일 시간축 규약
- Three.js 렌더링 코어
- 데이터 레이어: 합성 · Manifest · CSV
- 시각화 모듈 딥다이브
- 대용량 CSV 스트리밍 파이프라인
- UI & 인터랙션
- 인프라 & CI/CD
- 성능 & 운영 고려사항
- 마치며
1. 왜 이 프로젝트인가
교량 세굴(Bridge Scour) 은 하상 침식으로 교각 기초가 노출될 때 발생하는 치명적 구조 위험입니다. FLOW-3D 같은 CFD(Computational Fluid Dynamics) 도구로 수 m³ 규모의 3D 유체장과 하상 변화를 계산할 수 있지만, 그 결과는 보통 수 GB CSV/바이너리 형태로 남습니다.
문제 정의 — 연구자·엔지니어는 ParaView나 Tecplot 같은 데스크톱 툴 없이도, 브라우저에서 시간에 따른 세굴 진행·유속 분포·교각 안전도를 직관적으로 확인해야 합니다.
이 프로젝트(Bridge Scour 3D Dashboard)는 그 갭을 메우는 SPA(Single Page Application) 입니다.
목표구현 방향
| 실시간 시간 애니메이션 | requestAnimationFrame + 단일 currentTime |
| FLOW-3D 실데이터 연동 | CSV 스트리밍 파서 + Manifest 바이너리 |
| 데모/교육용 폴백 | 물리 기반 합성 데이터 소스 |
| 교각 안전 모니터링 | 임계 세굴 깊이 기반 붕괴 시뮬레이션 |
| 운영 배포 | Docker 멀티스테이지 + Nginx + GitHub Actions |
2. 시스템 아키텍처
2.1 3계층 토폴로지
flowchart TB
subgraph Client["🌐 사용자 브라우저"]
SPA["Vite SPA<br/>Three.js WebGL"]
end
subgraph Edge["🔒 Edge Layer"]
NGINX["Nginx Alpine<br/>TLS · gzip · SPA fallback"]
end
subgraph Backend["⚙️ Backend Stack"]
API["REST API<br/>(Node / Python)"]
PG["PostgreSQL"]
FS["File Storage<br/>FLOW-3D 결과"]
RD["Redis (optional)"]
end
Client -->|HTTPS| NGINX
NGINX -->|"/"| SPA
NGINX -->|"/api/*"| API
API --> PG
API --> FS
API --> RD
2.2 Docker 네트워크 분리
네트워크소속 서비스외부 노출
| frontend-net | frontend, backend | :80 / :443 |
| backend-net | backend, database, redis | 없음 |
DB와 Redis는 backend-net 내부에서만 통신합니다. 운영 서버 예시: https://▓▓▓.▓▓▓.▓▓▓.▓▓▓ (내부망 전용)
2.3 프론트엔드 모듈 맵
src/
├── core/ SceneManager · RendererManager · CameraManager · LightManager · AnimationLoop
├── modules/ Terrain · TerrainWater · FluidSlicePlane · VelocityArrows · BridgeCollapse · …
├── data/ SyntheticScourSource · ManifestSource · loadScourFromCsvFiles · …
├── components/ TimeControls · FluidControls · CsvUploadPanel · SimParamPanel · …
├── utils/ streamGridCsv · parseFlow3dVariableCsv · fluidWorld · colorRamp · …
├── types/ ScourSeries · FluidSeries · SimParams · Disposable
└── main.ts bootstrap() — 모든 것을 조립하는 진입점
의존성 최소화 — React/Vue 없이 순수 DOM API + CSS 로 UI를 구성합니다. Three.js 캔버스와 UI 패널의 lifecycle을 분리하고, GPU 리소스 누수를 Disposable 패턴으로 통제합니다.
3. 핵심 설계: 단일 시간축 규약
이 프로젝트에서 가장 중요한 아키텍처 결정은 모든 시각 모듈이 updateAtTime(t: number) 하나로 동기화된다는 점입니다.
TimeControls.tick(delta)
│
▼
currentTime = t
│
├─► Terrain.updateAtTime(t)
├─► BridgeCollapse.updateAtTime(t)
├─► TerrainWater.updateAtTime(t)
├─► FluidSlicePlane.updateAtTime(t)
├─► VelocityArrows.updateAtTime(t)
├─► FluidQuantityPoints.updateAtTime(t)
└─► CellTimeSeries.setTime(t)
왜 이 규약인가?
- 모듈 추가 비용 최소화 — 새 시각 레이어는 updateAtTime + dispose 만 구현
- 데이터 소스 교체 용이 — 합성/Manifest/CSV 모두 동일한 ScourSeries / FluidSeries 타입
- 애니메이션 루프 단순화 — AnimationLoop 는 콜백 Set만 관리, 시간 로직은 TimeControls 에 위임
SimState 는 파라미터에 의존하는 모듈 묶음입니다. 사용자가 시뮬 파라미터를 변경하면 buildSimState() → swapSim() 으로 통째로 교체하고, 이전 상태의 dispose() 를 호출합니다.
interface SimState {
series: ScourSeries;
fluidSeries: FluidSeries;
terrain: Terrain;
bridgeCollapse: BridgeCollapse;
slicePlane: FluidSlicePlane;
terrainWater: TerrainWater;
arrows: VelocityArrows;
fluidPoints: FluidQuantityPoints;
dispose(): void;
}
4. Three.js 렌더링 코어
4.1 SceneManager — 메모리 누수 방어
Three.js 의 가장 흔한 누수 원인은 geometry / material 미해제입니다. SceneManager.dispose() 는 scene.traverse() 로 모든 자식의 GPU 리소스를 재귀 정리합니다.
4.2 RendererManager — WebGL 컨텍스트 복원
// powerPreference: 'high-performance'
// setPixelRatio(min(devicePixelRatio, 2)) ← Retina GPU 부담 제어
rendererManager.onContextLossEvent(() => { loop.stop(); showBanner(); });
rendererManager.onContextRestoredEvent(() => { loop.start(); hideBanner(); });
탭 전환·GPU 드라이버 리셋 시 WebGL 컨텍스트가 손실될 수 있습니다. 이를 감지해 렌더 루프를 일시 정지하고, 복원 시 자동 재개합니다.
4.3 CameraManager — 데이터 크기 적응형 프리셋
applyPreset('reset' | 'top' | 'side' | 'front', sceneRadius) 는 격자 물리 크기 (width × cellSize) 에 비례해 카메라 거리를 계산합니다. CSV 업로드 후에도 자동으로 적절한 시점이 잡힙니다.
4.4 AnimationLoop — rAF 래퍼
loop.add((delta, elapsed) => {
timeControls.tick(delta);
const t = timeControls.time;
// ... 모든 모듈 updateAtTime(t)
cameraManager.update();
renderer.render(scene, camera);
});
다중 콜백 등록/해제 함수를 반환해 모듈별 lifecycle 을 분리합니다. HMR(import.meta.hot.dispose) 시에도 dispose() 체인이 실행됩니다.
5. 데이터 레이어: 합성 · Manifest · CSV
5.1 공통 타입 — Float32Array + 인덱스 규약
모든 격자 데이터는 1차원 Float32Array 로 저장합니다.
차원인덱스 규약
| 2D Terrain | i = y × W + x |
| 3D Fluid | i = x + y × W + z × W × H |
CPU 파싱 → 메모리 → GPU DataTexture / BufferAttribute 전송 경로에서 동일한 레이아웃을 유지합니다.
5.2 SyntheticScourSource — 데모용 세굴 생성
교각 주변 세굴공을 가우시안 + 로그 곡선으로 모사합니다.
maxDepth = -1.2 × scourRate × log1p(3t)
radius = pierRadius × (1 + 0.15 × t)
베이스 지형은 채널형 가우시안 + 미세 리플로 구성하고, 프레임마다 deltaElevations(ΔZ)만 갱신합니다.
5.3 SyntheticFluidSource — 물리 기반 합성 유체
데스크톱 CFD 없이도 그럴듯한 유체장을 만들기 위해 다음 근사를 사용합니다.
물리량모델
| 속도장 | 2D potential flow + 교각 정체 |
| 후류 와류 | Strouhal 수 St ≈ 0.2 기반 사인파 진동 |
| 압력 | Bernoulli 근사 + 정체점 보정 |
| 바닥/천장 | 로그 분포 점성 감속 |
5.4 ManifestSource — 사전 변환 바이너리
GB급 CSV를 브라우저에서 매번 파싱하는 것은 비현실적입니다. scripts/build-manifest.ts 가 Node 환경에서 FLOW-3D 결과를 프레임별 Float32 .bin + manifest.json 으로 변환합니다.
FLOW-3D raw (GB)
│ scripts/build-manifest.ts
▼
storage/flow3d/processed/
├── manifest.json ← 메타 + 프레임 인덱스
├── frame_0000.bin
├── frame_0001.bin
└── …
│ fetch (concurrency=6)
▼
ManifestSource.load() → ScourSeries
createDataSource() 는 manifest URL 에 HEAD 요청 → 200이면 Manifest, 아니면 Synthetic 폴백. 화면이 항상 뜨도록 설계된 graceful degradation입니다.
5.5 CSV 직접 업로드 — 브라우저 내 파싱
CsvUploadPanel → loadScourFromCsvFiles() + parseFlow3dVariableCsv() 로 사용자 로컬 CSV를 즉시 시각화합니다. 서버 업로드 없이 클라이언트 전용 파이프라인입니다.
6. 시각화 모듈 딥다이브
6.1 Terrain — 하상 지형 + 세굴 ΔZ
- Geometry: PlaneGeometry + MeshStandardMaterial{ vertexColors: true }
- 갱신: 매 프레임 positions.setY(base + Δ × exaggeration) in-place
- 컬러: diverging ramp (청색↔베이지↔황토) — ±absMax 동적 정규화
- 쿼리: queryAtWorld(x, z) → 격자 인덱스 → CellTimeSeries SVG sparkline
// 세굴 변화만 과장, 베이스 지형 비율은 유지
terrain.setVerticalExaggeration(f); // Δelevation × f
6.2 TerrainWater — 커스텀 GLSL 수면
수평 슬라이스 대신 지형 위에 직접 투영하는 방식입니다. ShaderMaterial 로 구현합니다.
Vertex Shader — 습윤(wet) 정점에만 Gerstner-like 파형 적용:
float w1 = sin(pos.x * 1.15 + uTime * 2.2) * cos(pos.z * 0.95 + uTime * 1.7);
float w2 = sin(pos.x * 2.4 - uTime * 3.0 + pos.z * 1.6) * 0.42;
pos.y += (w1 + w2 + w3) * uWaveAmp * aWet;
Fragment Shader — DataTexture(유체량 컬러맵) + Fresnel-like 가장자리 하이라이트
- 지형 고도 + 세굴 ΔZ + 수면 높이를 동기화
- fluidWorld.ts 의 좌표 변환으로 Terrain ↔ Fluid 3D 격자 정렬
6.3 FluidSlicePlane — DataTexture 수평 단면
- Y = const 평면 → W × D RGBA8 DataTexture
- setQuantity('speed'|'pressure'|'density'|U_x/y/z) → viridis 컬러맵
- setHeight(y) → 격자 Y 인덱스 스냅 + mesh.position.y 이동
6.4 VelocityArrows — InstancedMesh
InstancedMesh × 2 (shaft: CylinderGeometry, head: ConeGeometry)
stride = 4 (다운샘플)
draw calls = 2 (수천 화살표도 2 draw call)
각 인스턴스: compose(position, quaternion, scale) + setColorAt. magnitude ≈ 0 이면 scale 0으로 숨김.
6.5 FluidQuantityPoints — 3D 점군
FLOW-3D x·y·z 좌표 CSV에 맞춰 최대 80,000점 Points 렌더링. CSV 실데이터 탐색 시 stride 자동 조정.
6.6 BridgeCollapse — 교각 붕괴 시뮬레이션
교각 주변 max(erosion) 을 샘플링 → scourDepth / criticalScourDepth = ratio
ratio상태시각
| > 0.45 | 경고 | 베이지 |
| > 0.75 | 위험 | 주황 |
| ≥ 1.0 | 붕괴 트리거 | 빨강 → 흔들림 → 기울어짐 → 덱 낙하 |
SHAKE_DURATION = 1.5s (sin 진동)
TILT_DURATION = 5.0s (smoothstep 회전 + 침강)
DECK_FALL = quadratic ease-in (중력)
onCollapse(evt) → ScourWarning 배너 + 이력 기록
6.7 Picking — Raycaster
- NDC 변환 + Raycaster.intersectObjects
- hover: 60ms throttle / click: 즉시
- hit → Terrain.queryAtWorld → 셀 시계열 패널
7. 대용량 CSV 스트리밍 파이프라인
브라우저 메인 스레드를 블로킹하지 않기 위해 스트리밍 + 협력적 멀티태스킹을 적용했습니다.
7.1 streamGridCsv — 핵심 파서
// 64줄마다 메인 스레드 양보
const YIELD_EVERY_LINES = 64;
await yieldToMain();
지원 레이아웃:
layout설명
| matrix | 각 행 = x 방향 격자 (기본) |
| columnar | A열 세로 나열 → y×width+x reshape |
구분자: 쉼표 · 탭 · 공백 · 세미콜론 · # 주석 · 빈 줄 자동 스킵
7.2 파일 분류 (loadScourFromCsvFiles)
*.csv 파일 드롭
│
├─ *terrain* → baseTerrain (고정 표고)
├─ *frame* → deltaElevations 시계열 (시간순 정렬)
└─ meta.json → width/height/cellSize/intervalSeconds
진행률 콜백: phase × bytesRead/fileSize × rowsParsed/totalRows
7.3 parseFlow3dVariableCsv — FLOW-3D 다변수
한 CSV에 여러 물리량(속도·압력·난류 등)이 블록 단위로 포함된 형식을 파싱합니다.
변수명, U-Velocity, …
물리량, …
물리량단위, m/s, …
─────
<numeric block>
FLOW3D_VARIABLE_DEFS 레지스트리로 ID 정규화 + 단위 매핑.
7.4 Abort & Progress
- AbortSignal — 사용자 취소 시 파싱 즉시 중단
- csvProgress.ts — 다중 파일 가중 진행률 집계
- CsvDataPreviewModal — 업로드 전 헤더/샘플 미리보기
8. UI & 인터랙션
8.1 디자인 시스템
CSS 커스텀 프로퍼티 기반 다크 네이비 UI:
--panel-bg: rgba(12, 28, 48, 0.82);
--accent: #7ec8e3;
--radius-panel: 10px;
좌/우 Dock 패널 + 하단 TimeControls + FPS 미터(stats.js). 스크롤바는 숨기되 휠 스크롤은 유지.
8.2 TimeControls — 시간축 진실 공급원
- 재생 중: currentTime += delta × speed
- 슬라이더 드래그: isUserScrubbing = true → 자동 진행 정지
- loop 모드: time % duration
8.3 SimParamPanel — 핫 리빌드
SIM_PARAM_META 기반 슬라이더 자동 생성. 380ms 디바운스 라이브 모드 또는 "적용" 버튼. 적용 시 이전 SimState.dispose() → 새 모듈 생성.
8.4 KeyboardShortcuts
키동작
| Space | 재생/일시정지 |
| ← / → | ±1초 (Shift: ±5초) |
| Home / End | 처음/끝 |
| R / T / F / X | 카메라 프리셋 |
| P | PNG 스크린샷 |
8.5 CellTimeSeries — SVG Sparkline
Three.js 텍스처 대신 순수 SVG <path> 로 셀별 Δelevation 시계열. 현재 시간에 노란 마커 동기 이동. DOM만 사용 → 가볍고 선명.
8.6 ColorLegend — Single Source of Truth
colorRampToCssGradient() // HTML 범례
sampleColorRamp(v, min, max, out) // Three.js 정점색
3D 렌더와 HTML 범례가 동일 컬러맵 정의를 공유합니다.
9. 인프라 & CI/CD
9.1 Docker 멀티스테이지 (Frontend)
# Stage 1: node:20-alpine → npm ci → vite build
# Stage 2: nginx:alpine → dist/ only → non-root user
9.2 Nginx
- try_files $uri /index.html — SPA fallback
- gzip + 정적 캐시 (Cache-Control: immutable for hashed assets)
- /api/* → backend proxy
- 보안 헤더: X-Frame-Options, X-Content-Type-Options, Referrer-Policy
9.3 GitHub Actions CI
jobs:
lint-test-build: # typecheck → lint → format → vitest → vite build
docker-build: # frontend image build verify (needs: lint-test-build)
PR / push to main·develop — concurrency group으로 중복 실행 취소.
9.4 환경 변수
VITE_API_BASE_URL="/api"
VITE_API_PROXY_TARGET="http://localhost:8000" # dev only
POSTGRES_PASSWORD="change_me_in_production" # ⚠️ 시크릿 매니저 주입 권장
10. 성능 & 운영 고려사항
10.1 렌더링
기법효과
| InstancedMesh (화살표) | O(N) → 2 draw calls |
| DataTexture (슬라이스/수면) | CPU fill → GPU sample |
| setPixelRatio(min(dpr, 2)) | Retina GPU 부담 50%↓ |
| In-place vertex update | GC pressure 최소화 |
10.2 파싱
기법효과
| ReadableStream + line buffer | 전체 파일 메모리 적재 방지 |
| yieldToMain() every 64 lines | UI 프리즈 방지 |
| AbortSignal | 대용량 파일 취소 가능 |
| Float32Array pre-allocation | 힙 단편화 방지 |
10.3 메모리
- Disposable 인터페이스 — 모든 Three.js 모듈 + UI 컴포넌트
- buildSimState.dispose() — 파라미터 변경 시 이전 GPU 리소스 해제
- beforeunload + HMR dispose — 개발/운영 모두 cleanup
10.4 테스트 커버리지
Vitest + jsdom: Terrain, streamGridCsv, parseFlow3dVariableCsv, loadScourFromCsvFiles, fluidWorld, csvProgress 등 17개 테스트 스위트.
11. 마치며
Bridge Scour 3D Dashboard는 "FLOW-3D CSV → 브라우저 3D" 사이의 마지막 mile 을 담당하는 시각화 엔진입니다.
설계에서 배운 것
- updateAtTime(t) 단일 규약 — 모듈 확장 비용을 구조적으로 낮춤
- 데이터 소스 추상화 — 합성/Manifest/CSV가 같은 타입으로 수렴
- Float32Array 통일 — 파싱·CPU·GPU 경로 일관성
- 스트리밍 + yield — GB CSV도 메인 스레드 프리즈 없이 처리
- Disposable lifecycle — 핫 리빌드·HMR·탭 종료 모두 안전
로드맵 (향후)
-
Backend REST API 연동 (presigned URL 기반 대용량 스트리밍)
-
Web Worker CSV 파싱 (메인 스레드 완전 분리)
-
glTF 메시 export
-
Prometheus 메트릭 + Grafana 대시보드
'Frontend' 카테고리의 다른 글
| [Frontend] npm, Yarn, pnpm 정리 (0) | 2026.07.03 |
|---|---|
| [Frontend] Redux와 Recoil의 차이 (0) | 2026.07.03 |
| [FE 기술 면접] 예상 질문 정리 (0) | 2025.02.05 |
| [Frontend] UmiJS - routes 사용 (0) | 2024.04.23 |
| [Frontend] React를 알아보자 (0) | 2024.04.12 |