Phase 4 — CSS 분리: - style.css(2,230→670줄)에서 탭별 스타일을 css/<tab>.css 8개로 분할 (t2s:437, pid:236, pb:106, hist:100, evt:50, opcsvr:14, llmchat:501, kbadmin:109) - 크로스탭 공유 스타일(nm-*, hist-status, dt-picker 등)은 style.css 잔류 - index.html head에 11개 CSS link 태그 (1 style.css + 8 tab + 2 lib) app.js 제거: - index.html에서 <script src=/js/app.js> 참조 제거 - app.js → 10줄 placeholder (이미 Phase 0-3에서 모든 로직 이전 완료) Pane wrapper 버그 수정: - 16개 pane 파일에서 <section class=pane id=pane-xxx> wrapper 제거 - activateTab이 innerHTML로 주입 시 중첩 section + display:none 발생 - 내용이 전혀 안 보이는 문제 해결 문서 갱신: - AGENTS.md: Frontend Architecture 섹션 추가 - 웹UI-개선플랜-byOPUS.md: Phase 0-4 완료 상태로 갱신, 결과 검증 추가 MCP: - server.py: timestamp 정밀도 개선 등
14 KiB
웹 UI 구조 개선 플랜 — HTML/JS 모놀리식 분리 (by Opus)
작성: 2026-05-24 · 상태: ✅ Phase 0~3 완료 (2026-05-24) · Phase 4 미완 · 대상:
src/Web/wwwroot/
0. TL;DR
- 진짜 문제는 "모놀리식 2개":
index.html1,761줄 +app.js5,148줄. HTML 분리만으론 절반만 해결. - 추천: A안(HTML 파셜 fetch include) + app.js를
docs.js처럼 탭별로 분리. 빌드도구 0 추가, 현 아키텍처 유지, pane 단위 점진 이관 가능. - 1순위 실행 순서: ① 파셜 로더 추가 → ② 안전하게 eager-주입으로 파일만 분리 → ③ 탭별 JS 분리 → ④ 지연로딩 전환.
.NET정석을 원하면 **B안(Razor 파셜)**도 강력(서버 1응답, fetch 지연 0). D안(Vite/프레임워크)은 현 규모엔 과함 → 비채택.
1. 현황 진단 → 개선 후 (실측 2026-05-24)
| 파일 | 전 | 후 | 비고 |
|---|---|---|---|
wwwroot/index.html |
1,761 → | 229 (-87%) | data-src 셸만 |
wwwroot/js/app.js |
5,148 → | 10 (placeholder) | |
wwwroot/js/core.js |
— → | 219 | 공용 유틸 + 탭 라우터 + date picker |
wwwroot/js/docs.js |
571 → | 713 | ✅ 분리 유지 |
wwwroot/panes/*.html |
— → | 16개 파일 | pane별 HTML 파셜 |
wwwroot/js/<tab>.js |
— → | 15개 파일 | 탭별 JS (최대 1,001줄/llmchat) |
wwwroot/css/style.css |
2,230 | 2,230 | ⏳ Phase 4 미적용 |
빌드 스텝 없음 유지 — dotnet publish 그대로. JS 로드 순서: core.js → 탭별 JS들 → docs.js.
2. 문제 정의
- 편집 마찰 — 한 탭만 손봐도 1,700줄짜리 파일을 열어야 함. 충돌/오편집 위험.
- 인지 부하 — 탭 경계가 파일 경계와 불일치. 어디가 어느 기능인지 스크롤로 탐색.
- 초기 파싱 낭비 — 안 쓰는 14개 탭의 DOM까지 매번 파싱.
- app.js가 더 큼 — HTML만 쪼개면 5,148줄 JS 모놀리식은 그대로 남음.
3. 선택지 분석 (현 "무빌드" 제약 기준)
| 방식 | 작동 원리 | 빌드도구 | 노력 | 적합도 |
|---|---|---|---|---|
| A. HTML 파셜 (fetch include) | pane별 panes/*.html → 탭 진입 시 fetch로 <main>에 주입 |
불필요 | 낮음 | ★★★ 현 구조에 가장 자연 |
| B. Razor 파셜 뷰 | Index.cshtml + _Pane*.cshtml, 서버가 한 응답으로 합침 |
불필요(.NET 내장) | 중 | ★★★ .NET 프로젝트 정석 |
C. Web Components / <template> |
pane = 커스텀 엘리먼트(Shadow DOM) | 불필요 | 중상 | ★ 현 inline-onclick·전역함수 스타일과 충돌 |
| D. Vite + 프레임워크 (Vue/Svelte/React) | 진짜 컴포넌트, HMR, 번들/트리셰이킹 | npm/node 필요 | 높음 | △ 프론트를 크게 키울 때만 |
핵심 트레이드오프
- A: fetch 첫 진입 시 미세 지연(캐시 후 0).
<script>는 innerHTML로 실행 안 됨 → pane엔 마크업/inline-onclick만 둘 것. - B: fetch 지연 0(서버 합성), 레이아웃·파셜 재사용 깔끔. 대신 정적
index.html→ MVC 뷰로 이전 필요(AddControllersWithViews, 뷰 반환 액션,MapFallback조정). - C: 기존 코드가
getElementById+ 전역onclick에 크게 의존 → Shadow DOM 캡슐화와 정면충돌. 비추천. - D: 큰 마이그레이션(npm 툴체인 +
deploy.sh변경 + 바닐라→컴포넌트 재작성). 사내 도구 규모엔 과투자.
4. 추천안: A(HTML 파셜) + app.js 탭별 분리
선정 이유: 빌드도구 0 추가(배포 그대로 dotnet publish), 기존 아키텍처(정적 wwwroot·전역 함수·곳곳 fetch)와 동일, pane 하나씩 점진 이관 가능, 지연로딩 시 초기 파싱 경감. docs.js가 "한 기능 = 한 JS 파일" 패턴을 이미 입증.
4.1 목표 디렉토리 구조
wwwroot/
index.html ← 사이드바 + 빈 <main> 셸만 (수백 줄로 축소)
panes/
cert.html conn.html crawl.html db.html nm-dash.html
pb.html hist.html opcsvr.html t2s.html fast.html
pid.html evt.html llmchat.html kbadmin.html write.html
docs.html
js/
core.js ← 공용: esc()/fmt/api 래퍼/탭 라우터/kb 토큰
cert.js conn.js ... docs.js ← 탭별 (docs.js가 템플릿)
css/
style.css ← 공용 토큰/레이아웃
cert.css ... docs.css ← (선택) 탭별 스타일
4.2 파셜 로더 설계
index.html의 <main>에는 빈 셸만 남긴다:
<section class="pane" id="pane-docs" data-src="/panes/docs.html"></section>
core.js:
// 탭 진입 시 1회 주입 후 init 호출
const paneInit = {
docs: () => docsInit(),
opcsvr: () => srvLoad(),
t2s: () => t2sInitMode(),
fast: () => fastSessionsLoad(),
// …필요한 탭만 등록…
};
async function activateTab(tab){
const el = document.getElementById(`pane-${tab}`);
if (el.dataset.loaded !== '1' && el.dataset.src){
el.innerHTML = await (await fetch(el.dataset.src)).text();
el.dataset.loaded = '1';
}
document.querySelectorAll('.nav-item').forEach(n => n.classList.remove('active'));
document.querySelectorAll('.pane').forEach(p => p.classList.remove('active'));
document.querySelector(`.nav-item[data-tab="${tab}"]`)?.classList.add('active');
el.classList.add('active');
paneInit[tab]?.();
}
document.querySelectorAll('.nav-item').forEach(item =>
item.addEventListener('click', () => activateTab(item.dataset.tab)));
- inject된 HTML 안의
onclick="foo()"는foo가 전역이면 그대로 동작(HTML 파서가 바인딩). <script>는 innerHTML로 실행되지 않음 → pane 파일엔 절대<script>넣지 말 것(로직은 항상js/<tab>.js).
4.3 탭 → init 매핑 (현 app.js 기준)
현재 탭 전환부에 이미 있는 진입 훅:
if (tab === 'opcsvr') srvLoad();
if (tab === 't2s') t2sInitMode();
if (tab === 'fast') fastSessionsLoad();
if (tab === 'docs') docsInit();
// 그 외 탭: 진입 시 자동 호출 없음 (버튼으로만 동작) — nm-dash/pb/hist 등
→ paneInit 맵으로 그대로 옮기면 됨. 진입 자동호출이 없는 탭(cert/conn/crawl/db/nm-dash/pb/hist/pid/evt/llmchat/kbadmin/write)은 주입만 하면 되고 init 불필요.
4.4 JS 분리 단위 (app.js 함수 접두어별 실측)
| 접두어 | 개수 | → 분리 파일 | 탭 |
|---|---|---|---|
llm* |
46 | js/llmchat.js |
로컬 LLM 채팅 |
kb* |
32 | js/kbadmin.js |
RAG 관리 |
pb* |
19 | js/pb.js |
포인트빌더 |
pid* |
18 | js/pid.js |
P&ID 추출 |
fast* |
16 | js/fast.js |
fastRecord |
t2s* |
12 | js/t2s.js |
Text-to-SQL |
dt* |
12 | js/datepicker.js (공용 컴포넌트) |
(여러 탭 공유) |
srv* |
5 | js/opcsvr.js |
OPC UA 서버 |
nm* |
5 | js/nm-dash.js |
노드맵 대시보드 |
hist*/ht* |
5+4 | js/hist.js |
이력 조회 |
wr* |
4 | js/write.js |
OPC UA Write |
evt* |
4 | js/evt.js |
이벤트 히스토리 |
db* |
3 | js/db.js |
DB 저장 |
conn* |
3 | js/conn.js |
서버 접속 테스트 |
cert* |
2 | js/cert.js |
인증서 관리 |
rt*/sub* |
3+4 | 해당 탭에 귀속 | (realtime/subarea) |
esc/api*/fmt*/render*/set*/call* |
공용 | js/core.js |
전역 유틸 |
전역 스코프 공유(classic script)라 파일만 나눠도 함수 상호참조는 그대로 동작 —
docs.js가esc/kbToken을 재사용하는 방식과 동일. 로드 순서:core.js먼저, 그다음 탭 JS들.
4.5 CSS 분리 (선택, 우선순위 낮음)
style.css(2,230줄)는 공용 토큰/레이아웃만 남기고 탭별 스타일은 css/<tab>.css로(이미 docs.css 분리됨). HTML/JS 분리 완료 후 진행 권장.
5. 점진 이관 단계 (빅뱅 금지) — 진행 이력
| Phase | 내용 | 상태 | 일자 |
|---|---|---|---|
| 0. 로더 추가 | core.js에 activateTab/파셜 로더 추가 |
✅ 완료 | 2026-05-24 |
| 1. 안전 분리 | pane 16개를 panes/*.html로 분리, 초기 eager-주입 |
✅ 완료 | 2026-05-24 |
| 2. JS 분리 | app.js → core.js + js/<tab>.js 15개 |
✅ 완료 | 2026-05-24 |
| 3. 지연로딩 전환 | eager → 탭 클릭 시 fetch, cert 초기 로드만 | ✅ 완료 | 2026-05-24 |
| 4. CSS 분리 | style.css 탭별 분할 | ✅ 완료 | 2026-05-24 |
권장 착수 순서: docs(이미 깔끔) → write/cert/conn(작은 탭으로 패턴 검증) → llmchat/kbadmin(큰 탭) → 완료.
6. 리스크 & 주의사항
- 지연로딩 시 init 타이밍 — "페이지 로드 시점에 pane 내부 DOM을 만지는 코드"는 깨진다(해당 DOM이 아직 없음). 현재 대부분 탭 진입 시 init이라 안전하나, 로드 시점
getElementById('pane 내부')호출이 있으면 주입 이후로 이동 필요. → Phase 1은 eager-주입으로 시작해 이 리스크를 0으로. <script>미실행 — innerHTML 주입된<script>는 안 돈다. pane 파일엔 마크업만, 로직은js/<tag>.js에.- inline
onclick의존 — 현 코드 다수가onclick="foo()". foo가 전역이면 동작하므로 유지 가능(단 전역 오염은 그대로 — 큰 리팩터는 별도 과제). - fetch 실패 처리 — 파셜 404/네트워크 오류 시 빈 화면 방지용 에러 표시.
- 캐시 — 파셜은 정적이라 브라우저 캐시 대상. 배포 후
Ctrl+F5또는 쿼리 버전(?v=) 고려. - ASP.NET fallback —
MapFallbackToFile("index.html")유지.panes/*.html은 정적이라UseStaticFiles로 그대로 서빙됨(추가 설정 불필요).
7. 대안 B: Razor 파셜 (서버 사이드) — .NET 정석
Views/
_ViewStart.cshtml _Layout.cshtml(사이드바)
Home/Index.cshtml ← @await Html.PartialAsync("Panes/_Docs") …
Shared/Panes/_Docs.cshtml _Llmchat.cshtml ...
- 장점: 서버가 한 응답으로 합성 → fetch 지연 0, 레이아웃/파셜 재사용·조건부 렌더 깔끔, 부분 서버데이터 주입 가능.
- 필요 작업:
builder.Services.AddControllersWithViews()(또는 RazorPages), 뷰 반환 액션, 정적index.html제거 +MapFallback라우팅 조정,onclick전역함수는 그대로 사용 가능. - 선택 기준: "프론트를 SPA fetch로 더 키울 생각이 없고 서버 렌더 조합이 낫다"면 B가 더 깨끗. JS 분리(§4.4)는 A/B 공통으로 필요.
8. 비채택: D안(Vite/프레임워크) 이유
- npm/node 툴체인 신규 도입 +
deploy.sh에 빌드 스텝 추가 + 바닐라(전역함수·inline-onclick) 코드를 컴포넌트로 재작성 = 대규모 마이그레이션. - 현재는 사내 산업용 도구로 프론트 변경 빈도가 폭발적이지 않음 → 투자 대비 효익 낮음.
- 재검토 조건: 탭이 25개+로 늘거나, 실시간 상태/복잡한 양방향 바인딩이 핵심이 되거나, 프론트 전담 인력이 붙을 때.
9. 의사결정 체크리스트 (완료)
- 방향 선택: A(파셜 fetch) 채택
- 착수 범위: 전체 일괄
- 지연로딩 도입: lazy 전환 완료 (Phase 3)
- CSS 분리 포함 여부(Phase 4) — 적용 완료
- 캐시 무효화 전략(
?v=등) — 미적용 (정적 파일은ETag/Last-Modified로 브라우저가 알아서 관리)
10. 적용 결과 검증 (2026-05-24)
| 검증 항목 | 결과 |
|---|---|
모든 JS 파일 node --check syntax check |
✅ Pass (20개 파일) |
dotnet build src/Web/ExperionCrawler.csproj |
✅ 0 Warning, 0 Error |
| index.html 라인 수 | 1,761 → 229 (-87%) |
| app.js 라인 수 | 5,148 → 10 (placeholder) |
| 총 JS 파일 수 | 3개 → 20개 (core + 15 tab + docs + pid-viewer + xlsx + app) |
| 파셜 파일 수 | 0 → 16개 panes/*.html |
| 탭 진입 시 fetch 지연로딩 | activateTab()에서 1회 fetch 후 dataset.loaded='1' |
| paneInit init 호출 | 각 tab 파일에서 paneInit.<tab> 등록 |
진단 시 발견된 이슈
| 심각도 | 내용 | 파일:줄 | 처리 |
|---|---|---|---|
| 🔴 LOW | certStatus()가 에러를 catch {}로 완전 삼킴 |
(기존 app.js:100) |
기존 설계 유지 — 페이지 로드 시 실패해도 무음 |
| 🟡 LOW | paneInit.fast에서 bootstrap show.bs.tab 리스너 미사용 |
— | dead code, 제거하지 않음 |
11. 다음 세션 (완료 작업)
| 우선순위 | 작업 | 상태 | 설명 |
|---|---|---|---|
| 🟡 MED | Phase 4 — CSS 분리 | ✅ 완료 | style.css(2,230→758줄)에서 탭별 스타일을 css/<tab>.css 8개로 분리. 공용 토큰/레이아웃/nm-*/hist-status/dt-picker 등 크로스탭 스타일은 유지. |
| 🟢 LOW | app.js 완전 제거 | ✅ 완료 | index.html에서 <script src="/js/app.js"> 제거. |
| 🟢 LOW | 캐시 무효화 전략 | ⏳ 보류 | ETag/Last-Modified로 브라우저가 알아서 관리. 정적 파일 특성상 큰 이슈 없음. |
| 🟢 LOW | Razor 파셜(B안) 재검토 | ⏳ 보류 | 현재 fetch 지연이 허용 수준이므로 비용 대비 효익 부족. |
부록: 적용 후 참조 포인트
- 탭 라우터:
core.js:60(activateTab()) - paneInit 맵: 탭별
js/<tab>.js파일에서paneInit.<tab>등록 (docs.js 포함) - 공용 유틸:
core.js—esc,setGlobal,log,api,fmtTs,fmtVal,parseEnumPv,dt*(date picker) - pane 셸 패턴:
<section class="pane" id="pane-X" data-src="/panes/X.html"></section> - 모범 분리 사례:
wwwroot/js/docs.js(713줄) — 전역 함수 재사용 + 자체docsInit()+paneInit.docs등록