index.html(1.7K줄)·app.js(5.1K줄) 모놀리식 분리 방안 — HTML 파셜(fetch), Razor 파셜, Vite+바닐라 ESM, 프레임워크 비교 및 점진 이관 단계. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
12 KiB
웹 UI 구조 개선 플랜 — HTML/JS 모놀리식 분리 (by Opus)
작성: 2026-05-24 · 상태: 제안 (구현 보류) · 대상:
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 | <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,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/파셜 로더 추가. 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. 리스크 & 주의사항
- 지연로딩 시 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) 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>