콘텐츠로 이동

설치했지만 안 쓰는 오픈소스 44건 — 8개 에이전트 병렬 감사와 당일 배포까지

화면은 전부 켜지고 기능은 전부 동작하는데, 그 밑에는 조용히 설치만 되어 있고 실제로는 안 쓰이는 오픈소스 라이브러리들이 있었습니다. 코드를 직접 열어보기 전까지는 잘 드러나지 않는 낭비입니다.

1. 계기 — 급여 화면 하나가 시작이었다

섹션 제목: “1. 계기 — 급여 화면 하나가 시작이었다”

발단은 급여 마스터 화면들이었습니다. 손으로 만든 <table>을 Tabulator(MIT 라이선스 오픈소스 그리드)로 바꿨더니, 정렬·컬럼 리사이즈·엑셀 유사 조작감이 인사(HR) 화면 22개에 걸쳐 한 번에 올라갔습니다. 새로 짠 코드는 오히려 줄었습니다. 그리드 하나를 갈아 끼웠을 뿐인데 “화면 품질”이라는 축이 통째로 움직인 셈입니다.

그 결과를 지켜보고 물었습니다. “이런 식으로 좋아질 오픈소스가 또 있을까?” 이 질문에 감으로 후보를 나열해서는 답이 안 됐습니다. 실제 코드를 읽어야 했습니다 — 어디에 무엇이 몇 번 반복돼 있는지, 이미 설치는 됐는데 실제로는 안 쓰이는 게 있는지.

2. 8개 영역에 조사관을 동시 투입하다

섹션 제목: “2. 8개 영역에 조사관을 동시 투입하다”

한 사람이 전 영역을 훑는 대신, 코드베이스를 8개 영역(문서출력·엑셀, 캘린더·날짜, 시각화, 공통 UX, 기존 라이브러리 실사용률, 백엔드 인프라, 테스트·품질, Flutter 모바일)으로 나눠 조사관 8명을 동시에 투입했습니다. 각 조사관에게 준 규칙은 하나였습니다 — 추측 금지, 실제 파일 근거 필수. 발견마다 파일경로:줄번호가 붙지 않으면 인정하지 않았습니다.

오픈소스 도입 감사 — 조사 → 비평 → 우선순위 조사관 8명 영역별 병렬 조사 파일:줄 근거 필수 44건 발견 독립 비평 에이전트 중복·과잉엔지니어링 검증 누락 영역 재확인 7개 최우선순위
조사관 8명이 영역별로 병렬 조사해 44건을 냈고, 별도의 비평 에이전트가 근거를 재검증해 7건으로 좁혔다. 하네스 운용 자체는 역할별로 분리된 에이전트 팀 원칙을 그대로 따른다.

이 구성 자체가 하나의 원칙이었습니다. 조사한 사람이 그 조사의 우선순위를 매기지 않는다. 44건 중 7건은 “근거가 약하다·이미 있는 것과 중복된다·1인 개발 체제에서 유지비가 이득을 넘는다”는 이유로 비평 단계에서 걸러졌습니다.

3. 가장 큰 낭비는 “새로 사자”가 아니라 “이미 산 걸 안 쓴다”였다

섹션 제목: “3. 가장 큰 낭비는 “새로 사자”가 아니라 “이미 산 걸 안 쓴다”였다”

Tabulator와 정확히 같은 구조의 문제가 코드베이스 곳곳에 반복돼 있었습니다. 라이브러리는 package.json에 버젓이 설치돼 있는데, 실제 화면 코드는 그걸 쓰지 않고 매번 새로 손으로 짠 것입니다.

라이브러리실사용률비고
@tanstack/react-query12% (51 / 411개 파일)루트에 전역 설치·설정까지 끝나 있었다
react-hook-form + zod10% (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 — 화면을 매일 보는 사람이 가장 먼저 느끼는 것”
순번후보 (라이선스)공수문제
01sonner · 토스트 (MIT)M네이티브 alert() 257회 · 136개 파일 — 저장/실패 안내가 전부 브라우저 OS 팝업이었다. 상용 ERP 인상을 가장 깎아먹는 부분
02Radix UI Dialog / AlertDialog (MIT)M수작업 모달 오버레이 162개 파일, 포커스트랩·스크롤잠금·포탈 렌더링이 전무. confirm() 139회, 반려 사유 입력이 window.prompt로 처리된 곳도 13곳
03FullCalendar · MIT 플러그인만 (MIT)M업무포탈 일정 화면의 수작업 캘린더 약 420줄 — 멀티데이 막대·드래그 일정변경·길이 비례 표시가 없다(지금은 미니캘린더 클릭조차 안 된다)
04Tabulator dataTree · 기설치 (MIT)SBOM 화면이 들여쓰기 평면 테이블로 그려지고 있다 — 셀→모듈→팩 계층을 접기/펼치기로 바꿀 수 있다. 추가 설치 0
05yet-another-react-lightbox (MIT)S영수증·첨부 이미지 “확대”가 이미지 한 장 띄우기가 전부다 — 휠 줌·팬·여러 장 넘기기가 불가능했다. 영수증 검수 업무에 직결되는 항목

5. 도입 후보 Tier 2 — 사용자는 못 보지만 사고를 막아주는 것

섹션 제목: “5. 도입 후보 Tier 2 — 사용자는 못 보지만 사고를 막아주는 것”
순번후보 (라이선스)공수문제
01helmet + @nestjs/throttler (MIT)S보안 헤더 전무 + 로그인 브루트포스 방어 전무 — 둘 다 몇 줄 수준으로 막을 수 있다
02zod 환경변수 부팅 검증 · 기설치 (MIT)Mprocess.env 46종이 무검증이었다. 오설정으로 인한 404, 환경변수 누락 같은 실제 배포사고 유형이 기동 시점에 차단된다
03jest-mock-extended (MIT)SPrisma 수작업 목이 1,783줄 · 302개 파일, 전부 as any. 스키마가 바뀌어도 테스트가 낡은 모형을 들고 그대로 통과한다 — “라이브에서만 발견”되는 버그의 전형적 원인
04react-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 모바일 앱은 이미 직원 폰에 깔려 있으니, 점진 적용이 원칙이었습니다.

순번후보 (라이선스)공수문제
01firebase_crashlytics (BSD-3)S~M라이브 3개 앱 전부 크래시 수집이 0건 — 직원 폰에서 앱이 죽어도 아무도 모른다. 출퇴근 앱은 Firebase 기반이 이미 있어 사실상 한 줄 추가로 끝난다
02json_serializable (BSD-3)M타입 모델 없이 Map 원시 파싱이 267곳. “null 값 하나가 목록 전체를 크래시시킨” 실사고 이후 만들어진 방어 헬퍼가 179곳에서 호출되고 있는 게 그 증거다
03skeletonizer (MIT)S스켈레톤 로딩이 0곳, 로딩 스피너만 69개 — 표방하는 디자인 언어와 실제 화면이 어긋나 있었다
04dio_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.5exceljs 전면 교체. 갱신이 끊긴 이 버전에는 알려진 취약점 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 — 양쪽 앱 에러 0
  • vitest / jest — 신규 회귀 0 (기존 실패 6건은 변경 전 상태에서 동일 재현시켜 무관함을 실증)
  • recharts — 실배포 화면에서 실데이터 2개 시리즈 렌더 확인
  • sonner 토스트 — 저장 성공 시 실제 알림 표시 확인
  • Radix 확인창 — 삭제 시 다이얼로그 + 배경 스크롤 잠금 동작 확인
  • 콘솔 에러 — 0건

9. 그래서, 다음 프로젝트라면 — 처음부터 넣었어야 할 다섯 조항

섹션 제목: “9. 그래서, 다음 프로젝트라면 — 처음부터 넣었어야 할 다섯 조항”

오늘 고친 것들의 상당수는 “나중에 발견해서 고친 것”이 아니라 “처음부터 규정이었으면 생기지 않았을 것”입니다. 되짚어 보면 다섯 가지로 줄어듭니다.

  1. UI 패턴마다 “표준 라이브러리”를 설계 문서에 못박는다. 그리드는 Tabulator, 확인창은 공용 훅, 알림은 토스트, 날짜는 지정된 컴포넌트 — 신규 화면을 만들 때 “이 패턴엔 이걸 쓴다”가 설계 문서 한 줄로 정해져 있으면, 나중에 100개 넘는 화면을 갈아엎을 일이 없다. (근거 — Tabulator 전환 22화면 / 모달 오버레이 162파일)
  2. “설치했는데 안 쓰는 코드”를 정기적으로 잡아낸다. 설치는 됐지만 import가 거의 없는 패키지를 찾는 건 코드리뷰가 절대 못 잡는 사각지대다. 분기 1회 정도의 가벼운 실사용률 점검이면 충분하다. (근거 — react-query 12% · RHF 10% · dayjs 3파일)
  3. 브라우저 네이티브 대화상자는 프로젝트 시작 시점부터 금지한다. alert/confirm/prompt를 처음부터 ESLint error로 막아두면, 나중에 190개 파일을 한꺼번에 고치는 대신 애초에 한 줄도 생기지 않는다. (근거 — alert 257회 · confirm 139회 · prompt 13곳)
  4. 외부 파일을 파싱하는 의존성은 도입 시점 + 정기적으로 취약점을 확인한다. 고객이 보낸 파일을 서버가 직접 읽는 지점은 공격 표면이다. “설치할 때 한 번 확인하고 그 뒤로 안 본다”가 방치된 버전이 실제 공격 경로에 물려 있던 이유였다. (근거 — xlsx 0.18.5, CVE 2건, 고객 첨부 파싱 경로)
  5. 테스트 데이터는 처음부터 합성 픽스처로 강제한다. 실명·주민번호가 테스트 코드에 스며드는 길을 “커밋 직전에 사람이 눈으로 거르기”가 아니라 구조적으로 막아야 한다. 합성 팩토리 하나면 애초에 그 길이 없다. (근거 — 스펙 파일 7명 실명 + 급여·주민번호 1건)