오태훈 · Frontend Engineer · oth5447@naver.com · https://www.th-oh.site
Frontend Engineer · Platform & Build Architecture
오태훈. 온프레미스 클라우드 관리 플랫폼(B2B 어드민)에서 여러 팀의 프론트엔드 앱을 하나의 제품으로 묶는 플랫폼 계층을 3년간 담당했습니다. 도구의 오탐과 제 자신의 진단 오류까지 실측으로 검증하고 정정하는 것을 습관으로 삼습니다.
앱 4 · 공통 패키지 3 · 원격 저장소 6을 단일 이미지로 통합하는 환경에서 쓰는 것들.
CI가 원리적으로 못 잡는 조용한 실패 — 이슈 등록부터 릴리스 반영까지 하루.
eager: true shared 사용 시 프로덕션 빌드 산출물이 부팅 시점에 크래시.
빌드는 EXIT 0·경고 0으로 정상 종료 — 배포 후 런타임에만 드러난다.// 플러그인 후처리: Vite preload 헬퍼를 지우려는 정규식이 모듈 경로를 보지 않고
// `{ _ as X }` 형태의 import를 전부 매칭한 뒤, X의 "함수 호출"이 없으면 문장째 삭제한다.
// 네임스페이스 import는 값으로만 참조되므로 여기 걸려 지워졌다.
if (!hasCall(local, chunk)) dropImportStatement() // 판정: 호출되는가
// 수정(v1.20.3): 판정 기준을 "참조되는가"로 교체 — 기존 동작 보존, 결함만 제거
if (!new RegExp(`\\b${local}\\b`).test(chunk)) dropImportStatement()
"Vite를 쓰려면 전체를 마이그레이션해야 한다"는 팀 내 전제를 POC 하나로 반박했다.
@module-federation/vite로 Vite(Host) ↔ Webpack(Remote)
런타임 통신이 성립함을 POC로 먼저 입증하고 Host 측 Vite MF 도입을 주도했다.
React Query와 Vue Query가 단일 Query Client를 공유하도록 통합해
두 프레임워크가 같은 API 캐시를 참조한다.
이후 타 팀 소유 Remote의 빌드 설정을 webpack 내장 ModuleFederationPlugin에서
@module-federation/enhanced(MF 2.0)로 전환하고 런타임을
0.21.6 → 2.8.1로 올렸으며, remote 로딩을 remoteEntry.js 직접 참조에서
mf-manifest.json 기반으로 이관했다 — manifest의 publicPath가 루트로 내려오는 환경은
런타임 플러그인으로 방어하고 remoteEntry 경로는 폴백으로 남겼다.마이크로 프론트엔드는 배포 빈도가 높은 조직에서 값을 낸다. 저빈도 온프레미스 모델에서는 아니었다.
사람이 매번 확인하던 일을 파이프라인과 스크립트로 옮겼다.
dev tip이 깨지면 전 팀 배포가 막혔다 — 통합 빌드 전면 차단이
하루 2회. 타 팀 브랜치 상태를 사람이 확인하고 배포를 조율하고 있었다.postinstall이 매니페스트만 복사하는 Docker base 스테이지에서 자기 스크립트를 못 찾고 죽는다git replace --graft로 복구 — 원격 저장소 5개 재동기화 비용 없이 코드 소유권 추적성 회복잡을 빠르게 만드는 것과 파이프라인을 빠르게 만드는 것은 다른 문제였다.
| 병목 | 원인 (실측) | 조치 | 효과 |
|---|---|---|---|
| 정적분석 스캔 | 제외 설정이 분석 대상에서만 빼고 순회 자체는 막지 않음 — pnpm 심볼릭 링크가 실파일 7.5만 개를 33만 경로로 부풀린다 | 스캔 루트를 실제 대상만 열거 (순회 → 약 2.8만 경로) | 350 → 80초 |
| 캐시 아카이빙 | 4개 잡이 동일한 2.5GB 캐시를 각각 push | 쓰기를 임계 경로 밖 잡으로 한정 | 잡당 −35~39초 |
| 테스트 환경 | 전역 jsdom 설정이 파일 단위 환경 선언과 충돌 — DOM 불필요한 167개 파일이 비용 지불 | 기본값 반전 + 누락 독블록 6개 보강 (전수 실행으로 확인) | 91 → 59초 |
| 러너 자원 | 호스트 14코어 중 4개만 VM에 할당 | VM 8코어로 확장 (공용 머신이라 메모리는 불변) | vitest 124 → 58초 |
| 스케줄링 | 슬롯 부족 76초 큐잉(queued_duration으로 확인) + 의존 없는 잡의 스테이지 직렬화 82초 | concurrent 확장 + needs: [] | 사장 82 → 3초 |
web-worker가 workerData 존재만으로 자신을 워커로 오판해 로컬은 통과하고 CI만 깨졌다. 원인을 규명한 뒤 격리를 되찾기 위해 forks로 복귀 — 이 파이프라인에서 "로컬 통과·CI 실패"가 나온 두 번째 경로다「잠재결함 928건」이라는 정적분석 리포트에서 먼저 규명한 것은 도구의 측정 범위 결함이었다.
index.ts) 233개 전면 정리 요구에 대해, tsconfig paths를 해석하는 정적 분석 스크립트로 barrel 경유 import 735건을 계량화하고 636건(387파일)을 자동 치환하는 코드모드를 작성해 A/B 빌드를 3회씩 반복 측정storybook build가 "번들이 만들어지는가"만 보고 스토리 런타임 실패를 통과시키던 구멍을 막았다.
storybook build는
번들 생성만 확인해 스토리 런타임 실패를 통과시켰다. 조사 중 공통 컴포넌트 패키지 테스트
23개가 CI에서 한 번도 돌지 않고 있었다는 사실도 발견했다 —
전 잡이 host만 --filter하고 있었다.composeStories)로 전 스토리를 jsdom에 실제 마운트한다 —
권장 경로 addon-vitest는 CI에 브라우저 인프라를 요구해 배제했다.
비주얼 회귀는 솔루션 4종 × 라이트/다크 8테마에서
컴포넌트 17개 대표 샷 136장으로 시작해 현재
264장까지 늘리고, 리눅스 컨테이너 baseline으로 고정했다.vite preview가 ::1에만 붙어 CI 컨테이너에서 타임아웃)도 사전에 검출했다.
baseline 갱신 절차를 문서로 고정해 "스킨을 의도적으로 바꾸면 잡이 상시 빨간불"이 되는
방치 경로를 차단했다.@storybook/addon-vitest는 browser mode와 Playwright를 요구한다 — 게이트 1개를 위해 브라우저 런타임을 CI에 들이는 비용은 이 규모에서 과하다고 판단해 대체 경로를 택했다?inline을 쓸 수 없다토큰이 주입돼도 antd 기본색이 이기는 스킨 계약 구멍도 별건으로 특정했다.
이미 발행한 문서를 정정하고 주입 축을 컴파일 타임으로 재설계. 남길 만한 것은 결과가 아니라 검증 과정이다.
nodeTransform으로 빌드 타임 주입. 템플릿 AST에서 업무 의미를
추출하는 9단계 폴백 + 위치 기반 스냅샷 병합으로 문구 수정이 테스트를
깨뜨리지 않게. 해결 안 되는 구간은 "해결했다" 대신 미보증 등급으로 정직하게 표기..map 삭제 후에도 sourceMappingURL 주석이 남아 소스맵 존재 힌트를 노출하던 것은 hidden으로 전환) — 수정 전 소스로 되돌려 신규 테스트가 전부 실패함을 확인하는 역방향 검증을 습관화dropConsole이 console.error·warn까지 제거한다는 것을 확인해, 제거 대상을 선별하는 플러그인을 직접 작성. 앞서 이중으로 무력화돼 있던 프로덕션 console 제거 정책을 새 번들러에서도 유지했다