설치했지만 안 쓰는 오픈소스 44건 — 8개 에이전트 병렬 감사와 당일 배포까지
화면은 전부 켜지고 기능은 전부 동작하는데, 그 밑에는 조용히 설치만 되어 있고 실제로는 안 쓰이는 오픈소스 라이브러리들이 있었습니다. 코드를 직접 열어보기 전까지는 잘 드러나지 않는 낭비입니다.
1. 계기 — 급여 화면 하나가 시작이었다
섹션 제목: “1. 계기 — 급여 화면 하나가 시작이었다”발단은 급여 마스터 화면들이었습니다. 손으로 만든 <table>을 Tabulator(MIT 라이선스 오픈소스 그리드)로 바꿨더니, 정렬·컬럼 리사이즈·엑셀 유사 조작감이 인사(HR) 화면 22개에 걸쳐 한 번에 올라갔습니다. 새로 짠 코드는 오히려 줄었습니다. 그리드 하나를 갈아 끼웠을 뿐인데 “화면 품질”이라는 축이 통째로 움직인 셈입니다.
그 결과를 지켜보고 물었습니다. “이런 식으로 좋아질 오픈소스가 또 있을까?” 이 질문에 감으로 후보를 나열해서는 답이 안 됐습니다. 실제 코드를 읽어야 했습니다 — 어디에 무엇이 몇 번 반복돼 있는지, 이미 설치는 됐는데 실제로는 안 쓰이는 게 있는지.
2. 8개 영역에 조사관을 동시 투입하다
섹션 제목: “2. 8개 영역에 조사관을 동시 투입하다”한 사람이 전 영역을 훑는 대신, 코드베이스를 8개 영역(문서출력·엑셀, 캘린더·날짜, 시각화, 공통 UX, 기존 라이브러리 실사용률, 백엔드 인프라, 테스트·품질, Flutter 모바일)으로 나눠 조사관 8명을 동시에 투입했습니다. 각 조사관에게 준 규칙은 하나였습니다 — 추측 금지, 실제 파일 근거 필수. 발견마다 파일경로:줄번호가 붙지 않으면 인정하지 않았습니다.
이 구성 자체가 하나의 원칙이었습니다. 조사한 사람이 그 조사의 우선순위를 매기지 않는다. 44건 중 7건은 “근거가 약하다·이미 있는 것과 중복된다·1인 개발 체제에서 유지비가 이득을 넘는다”는 이유로 비평 단계에서 걸러졌습니다.
3. 가장 큰 낭비는 “새로 사자”가 아니라 “이미 산 걸 안 쓴다”였다
섹션 제목: “3. 가장 큰 낭비는 “새로 사자”가 아니라 “이미 산 걸 안 쓴다”였다”Tabulator와 정확히 같은 구조의 문제가 코드베이스 곳곳에 반복돼 있었습니다. 라이브러리는 package.json에 버젓이 설치돼 있는데, 실제 화면 코드는 그걸 쓰지 않고 매번 새로 손으로 짠 것입니다.
| 라이브러리 | 실사용률 | 비고 |
|---|---|---|
@tanstack/react-query | 12% (51 / 411개 파일) | 루트에 전역 설치·설정까지 끝나 있었다 |
react-hook-form + zod | 10% (10 / 100개 폼) | 전자결재 화면에서만 실사용, 포털 폼은 전부 수제 |
| 라이브러리 | 실사용 상태 |
|---|---|
recharts | 전사에서 2개 파일만 사용. 홈 대시보드 “월별 생산/매출” 차트는 좌표를 하드코딩한 장식용 SVG — 실제 데이터와 무관하게 항상 같은 모양이었다 |
dayjs | 설치는 됐는데 import하는 파일은 3개뿐. 반면 날짜 포맷을 손으로 다시 짠 코드가 71개 파일에 흩어져 있었다 — 쓰는 곳보다 다시 만든 곳이 20배 이상 많았다 |
@playwright/test | 배포 검증용으로 설치돼 있었지만, 설정 3벌이 전부 죽은 포트·폐기 도메인·틀린 계정을 가리키고 있어 배포 스모크는 매번 사람이 브라우저를 열어 손으로 확인하고 있었다 |
flutter_riverpod | 모바일 앱 pubspec.yaml에 선언은 돼 있는데 실제 import는 0건. 상태관리는 전 화면 setState 수작업이었다 |
4. 도입 후보 Tier 1 — 화면을 매일 보는 사람이 가장 먼저 느끼는 것
섹션 제목: “4. 도입 후보 Tier 1 — 화면을 매일 보는 사람이 가장 먼저 느끼는 것”| 순번 | 후보 (라이선스) | 공수 | 문제 |
|---|---|---|---|
| 01 | sonner · 토스트 (MIT) | M | 네이티브 alert() 257회 · 136개 파일 — 저장/실패 안내가 전부 브라우저 OS 팝업이었다. 상용 ERP 인상을 가장 깎아먹는 부분 |
| 02 | Radix UI Dialog / AlertDialog (MIT) | M | 수작업 모달 오버레이 162개 파일, 포커스트랩·스크롤잠금·포탈 렌더링이 전무. confirm() 139회, 반려 사유 입력이 window.prompt로 처리된 곳도 13곳 |
| 03 | FullCalendar · MIT 플러그인만 (MIT) | M | 업무포탈 일정 화면의 수작업 캘린더 약 420줄 — 멀티데이 막대·드래그 일정변경·길이 비례 표시가 없다(지금은 미니캘린더 클릭조차 안 된다) |
| 04 | Tabulator dataTree · 기설치 (MIT) | S | BOM 화면이 들여쓰기 평면 테이블로 그려지고 있다 — 셀→모듈→팩 계층을 접기/펼치기로 바꿀 수 있다. 추가 설치 0 |
| 05 | yet-another-react-lightbox (MIT) | S | 영수증·첨부 이미지 “확대”가 이미지 한 장 띄우기가 전부다 — 휠 줌·팬·여러 장 넘기기가 불가능했다. 영수증 검수 업무에 직결되는 항목 |
5. 도입 후보 Tier 2 — 사용자는 못 보지만 사고를 막아주는 것
섹션 제목: “5. 도입 후보 Tier 2 — 사용자는 못 보지만 사고를 막아주는 것”| 순번 | 후보 (라이선스) | 공수 | 문제 |
|---|---|---|---|
| 01 | helmet + @nestjs/throttler (MIT) | S | 보안 헤더 전무 + 로그인 브루트포스 방어 전무 — 둘 다 몇 줄 수준으로 막을 수 있다 |
| 02 | zod 환경변수 부팅 검증 · 기설치 (MIT) | M | process.env 46종이 무검증이었다. 오설정으로 인한 404, 환경변수 누락 같은 실제 배포사고 유형이 기동 시점에 차단된다 |
| 03 | jest-mock-extended (MIT) | S | Prisma 수작업 목이 1,783줄 · 302개 파일, 전부 as any. 스키마가 바뀌어도 테스트가 낡은 모형을 들고 그대로 통과한다 — “라이브에서만 발견”되는 버그의 전형적 원인 |
| 04 | react-day-picker (MIT) | M | 날짜 라이브러리가 0개 — native date input 261곳 + 136줄짜리 수작업 듀얼캘린더 범위선택기가 별도로 있었다 |
| 05 | @headlessui/react Combobox + es-hangul (MIT) | M | 엔터티 검색 모달이 5벌 이상 각자 구현돼 있고, 초성검색은 없다. 후보 목록과 사번을 항상 같이 보여주면 “이름이 비슷한 다른 사람 선택” 사고 구조 자체를 UI 차원에서 막는다 |
6. 도입 후보 Tier 3 — 이미 폰에 깔려 있다는 전제 위에서
섹션 제목: “6. 도입 후보 Tier 3 — 이미 폰에 깔려 있다는 전제 위에서”Flutter 모바일 앱은 이미 직원 폰에 깔려 있으니, 점진 적용이 원칙이었습니다.
| 순번 | 후보 (라이선스) | 공수 | 문제 |
|---|---|---|---|
| 01 | firebase_crashlytics (BSD-3) | S~M | 라이브 3개 앱 전부 크래시 수집이 0건 — 직원 폰에서 앱이 죽어도 아무도 모른다. 출퇴근 앱은 Firebase 기반이 이미 있어 사실상 한 줄 추가로 끝난다 |
| 02 | json_serializable (BSD-3) | M | 타입 모델 없이 Map 원시 파싱이 267곳. “null 값 하나가 목록 전체를 크래시시킨” 실사고 이후 만들어진 방어 헬퍼가 179곳에서 호출되고 있는 게 그 증거다 |
| 03 | skeletonizer (MIT) | S | 스켈레톤 로딩이 0곳, 로딩 스피너만 69개 — 표방하는 디자인 언어와 실제 화면이 어긋나 있었다 |
| 04 | dio_cache_interceptor (MIT) | S | 영수증 이미지가 화면을 다시 열 때마다 매번 재다운로드된다 — 인터셉터 한 곳만 추가하면 끝나는 문제였다 |
7. 8명이 놓친 것을, 9번째가 찾았다
섹션 제목: “7. 8명이 놓친 것을, 9번째가 찾았다”조사관 8명이 각자 영역을 훑고 44건을 냈지만, 그중 누구도 “이 프로젝트가 배터리 제조 ERP라는 사실”에서 출발한 질문을 던지지 않았습니다. 그 역할은 별도의 비평 에이전트가 맡았습니다 — 44건을 검증하는 동시에, 빠진 영역이 있는지 직접 코드를 다시 열어 확인했습니다.
8. 44건 중 3묶음을 그날 배포까지 끝냈다
섹션 제목: “8. 44건 중 3묶음을 그날 배포까지 끝냈다”조사와 우선순위까지는 “계획”입니다. “최대 병렬로 3묶음 먼저”를 지시한 순간부터는 구현이었습니다. 28개 에이전트를 동시에 띄워 파일 단위로 작업을 쪼갰고, 중간에 하네스 결함으로 두 차례 재기동이 있었지만 진행분 유실 없이 이어 붙였습니다.
묶음 01 — 보안: xlsx 교체 + 테스트 PII 정리
xlsx(SheetJS) 0.18.5→exceljs전면 교체. 갱신이 끊긴 이 버전에는 알려진 취약점 2건(프로토타입 오염, ReDoS)이 있었고, 하필 그 경로가 고객사가 보낸 견적요청 엑셀을 서버가 직접 파싱하는 지점이었다. 한쪽 서비스는package.json에 선언조차 안 된 채 우연히 동작하던 “팬텀 의존성”이었던 것도 함께 정리했다.- 테스트 코드에 박혀 있던 실제 직원 실명 7명, 급여·주민번호 앞자리를 포함한 스펙 1건을 합성값으로 일관 치환. 금액·날짜 같은 실측 대조 데이터는 그대로 유지했다.
묶음 02 — 이미 산 도구 활용
- 홈 대시보드의 하드코딩 그래프를 recharts 실데이터로 교체(생산 집계 엔드포인트 신설, 매출은 기존 API 재사용).
- 급여 마스터 한 화면을 react-query로 전환 — 서버 에러가 “조용히 빈 목록”으로 보이던 침묵 실패를 로딩/에러/재시도 상태로 바꿨다. 이후 신규 화면은 이 패턴이 의무가 된다.
묶음 03 — sonner + Radix: 네이티브 다이얼로그 박멸
toast/confirmDialog()/promptDialog()공용 인프라를 먼저 깔고, 공용 모달 2개의 내부를 Radix Dialog로 교체(포커스트랩·스크롤잠금 자동 획득, 소비 화면 36개는 코드 수정 없이 개선).- 190개 파일에서 실사용을 확인해 전량 치환 — 잔존 0.
- ESLint에
no-alert·no-restricted-globals(confirm, prompt)를 error로 추가해, 이제는 옛날 방식으로 되돌아가는 커밋 자체가 막힌다.
이번 세션에서 변경된 규모는 265개 파일 · +3,189줄 · −1,037줄 · 5커밋입니다.
검증은 말이 아니라 브라우저로 했습니다.
tsc/eslint— 양쪽 앱 에러 0vitest/jest— 신규 회귀 0 (기존 실패 6건은 변경 전 상태에서 동일 재현시켜 무관함을 실증)- recharts — 실배포 화면에서 실데이터 2개 시리즈 렌더 확인
- sonner 토스트 — 저장 성공 시 실제 알림 표시 확인
- Radix 확인창 — 삭제 시 다이얼로그 + 배경 스크롤 잠금 동작 확인
- 콘솔 에러 — 0건
9. 그래서, 다음 프로젝트라면 — 처음부터 넣었어야 할 다섯 조항
섹션 제목: “9. 그래서, 다음 프로젝트라면 — 처음부터 넣었어야 할 다섯 조항”오늘 고친 것들의 상당수는 “나중에 발견해서 고친 것”이 아니라 “처음부터 규정이었으면 생기지 않았을 것”입니다. 되짚어 보면 다섯 가지로 줄어듭니다.
- UI 패턴마다 “표준 라이브러리”를 설계 문서에 못박는다. 그리드는 Tabulator, 확인창은 공용 훅, 알림은 토스트, 날짜는 지정된 컴포넌트 — 신규 화면을 만들 때 “이 패턴엔 이걸 쓴다”가 설계 문서 한 줄로 정해져 있으면, 나중에 100개 넘는 화면을 갈아엎을 일이 없다. (근거 — Tabulator 전환 22화면 / 모달 오버레이 162파일)
- “설치했는데 안 쓰는 코드”를 정기적으로 잡아낸다. 설치는 됐지만 import가 거의 없는 패키지를 찾는 건 코드리뷰가 절대 못 잡는 사각지대다. 분기 1회 정도의 가벼운 실사용률 점검이면 충분하다. (근거 — react-query 12% · RHF 10% · dayjs 3파일)
- 브라우저 네이티브 대화상자는 프로젝트 시작 시점부터 금지한다.
alert/confirm/prompt를 처음부터 ESLint error로 막아두면, 나중에 190개 파일을 한꺼번에 고치는 대신 애초에 한 줄도 생기지 않는다. (근거 — alert 257회 · confirm 139회 · prompt 13곳) - 외부 파일을 파싱하는 의존성은 도입 시점 + 정기적으로 취약점을 확인한다. 고객이 보낸 파일을 서버가 직접 읽는 지점은 공격 표면이다. “설치할 때 한 번 확인하고 그 뒤로 안 본다”가 방치된 버전이 실제 공격 경로에 물려 있던 이유였다. (근거 — xlsx 0.18.5, CVE 2건, 고객 첨부 파싱 경로)
- 테스트 데이터는 처음부터 합성 픽스처로 강제한다. 실명·주민번호가 테스트 코드에 스며드는 길을 “커밋 직전에 사람이 눈으로 거르기”가 아니라 구조적으로 막아야 한다. 합성 팩토리 하나면 애초에 그 길이 없다. (근거 — 스펙 파일 7명 실명 + 급여·주민번호 1건)