Files
ExperionCrawler/웹UI-개선플랜-byOPUS.md
windpacer eb9ce9a501 docs: 웹 UI 구조 개선 플랜 추가 (HTML/JS 모놀리식 분리)
index.html(1.7K줄)·app.js(5.1K줄) 모놀리식 분리 방안 — HTML 파셜(fetch),
Razor 파셜, Vite+바닐라 ESM, 프레임워크 비교 및 점진 이관 단계.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-24 06:32:24 +09:00

12 KiB

웹 UI 구조 개선 플랜 — HTML/JS 모놀리식 분리 (by Opus)

작성: 2026-05-24 · 상태: 제안 (구현 보류) · 대상: src/Web/wwwroot/


0. TL;DR

  • 진짜 문제는 "모놀리식 2개": index.html 1,761줄 + app.js 5,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 <section class="pane"> 15개가 한 파일에 인라인
wwwroot/js/app.js 5,148 전 탭 로직 한 파일
wwwroot/js/docs.js 571 이미 분리된 모범 사례 (문서 탐색기)
wwwroot/css/style.css 2,230 전 탭 스타일 한 파일 (docs.css만 분리됨)

빌드 스텝 없음 — 순수 정적 SPA. <script src> 직접 로드 + 라이브러리 로컬 번들(wwwroot/lib/). 서빙은 ASP.NET:

Program.cs:
  app.UseDefaultFiles();        // index.html
  app.UseStaticFiles();         // wwwroot/
  app.MapFallbackToFile("index.html");

현재 pane 인벤토리 (index.html 내 라인 시작 위치)

data-tab id 시작줄 data-tab id 시작줄
cert pane-cert 109 fast pane-fast 1033
conn pane-conn 156 pid pane-pid 1083
crawl pane-crawl 213 evt pane-evt 1201
db pane-db 301 llmchat pane-llmchat 1285
nm-dash pane-nm-dash 357 kbadmin pane-kbadmin 1384
pb pane-pb 436 write pane-write 1548
hist pane-hist 753 docs pane-docs 1634
opcsvr pane-opcsvr 907
t2s pane-t2s 938

2. 문제 정의

  1. 편집 마찰 — 한 탭만 손봐도 1,700줄짜리 파일을 열어야 함. 충돌/오편집 위험.
  2. 인지 부하 — 탭 경계가 파일 경계와 불일치. 어디가 어느 기능인지 스크롤로 탐색.
  3. 초기 파싱 낭비 — 안 쓰는 14개 탭의 DOM까지 매번 파싱.
  4. 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.jsesc/kbToken을 재사용하는 방식과 동일. 로드 순서: core.js 먼저, 그다음 탭 JS들.

4.5 CSS 분리 (선택, 우선순위 낮음)

style.css(2,230줄)는 공용 토큰/레이아웃만 남기고 탭별 스타일은 css/<tab>.css로(이미 docs.css 분리됨). HTML/JS 분리 완료 후 진행 권장.


5. 점진 이관 단계 (빅뱅 금지)

Phase 내용 리스크 롤백
0. 로더 추가 core.jsactivateTab/파셜 로더 추가. data-src 있는 pane만 fetch, 나머지 인라인 pane과 공존 없음 로더 제거
1. 안전 분리 (eager) pane 하나씩 panes/*.html로 들어내되, 시작 시 전체 파셜을 한 번에 eager-주입 → 동작 100% 동일, 파일만 분리 매우 낮음 파셜 내용을 index.html에 되붙임
2. JS 분리 app.js의 탭 함수군을 js/<tab>.js로 이동(4.4 표), 공용은 core.js로. index.html<script> 추가 낮음(전역 공유) 함수 되돌림
3. 지연로딩 전환 eager-주입 → 탭 진입 시 fetch로 전환. init 타이밍 점검 필수(§6) eager로 복귀
4. CSS 분리 탭별 css 추출 낮음

권장 착수 순서: docs(이미 깔끔) → write/cert/conn(작은 탭으로 패턴 검증) → llmchat/kbadmin(큰 탭).


6. 리스크 & 주의사항

  1. 지연로딩 시 init 타이밍 — "페이지 로드 시점에 pane 내부 DOM을 만지는 코드"는 깨진다(해당 DOM이 아직 없음). 현재 대부분 탭 진입 시 init이라 안전하나, 로드 시점 getElementById('pane 내부') 호출이 있으면 주입 이후로 이동 필요. → Phase 1은 eager-주입으로 시작해 이 리스크를 0으로.
  2. <script> 미실행 — innerHTML 주입된 <script>는 안 돈다. pane 파일엔 마크업만, 로직은 js/<tag>.js에.
  3. inline onclick 의존 — 현 코드 다수가 onclick="foo()". foo가 전역이면 동작하므로 유지 가능(단 전역 오염은 그대로 — 큰 리팩터는 별도 과제).
  4. fetch 실패 처리 — 파셜 404/네트워크 오류 시 빈 화면 방지용 에러 표시.
  5. 캐시 — 파셜은 정적이라 브라우저 캐시 대상. 배포 후 Ctrl+F5 또는 쿼리 버전(?v=) 고려.
  6. ASP.NET fallbackMapFallbackToFile("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) vs B(Razor) — JS 분리(§4.4)는 공통
  • 착수 범위: 전체 일괄 vs 큰 탭(llmchat/kbadmin)부터 vs 작은 탭부터
  • 지연로딩 도입 여부(A안): eager 유지 vs lazy 전환
  • CSS 분리 포함 여부(Phase 4)
  • 캐시 무효화 전략(?v= 등)

부록: 적용 시 참조 포인트

  • 탭 라우터 현 위치: app.js:5 (document.querySelectorAll('.nav-item')…)
  • 진입 훅 현 위치: app.js 탭 전환부 if (tab === 'opcsvr') srvLoad();
  • 모범 분리 사례: wwwroot/js/docs.js(571줄) — 전역 esc/kbToken 재사용, 자체 docsInit() 진입
  • pane 셸 패턴: <section class="pane" id="pane-X" data-src="/panes/X.html"></section>