Meetnote v0.3.0 — 직접 쓰면서 고친 것들

지난 글에서 Meetnote를 왜, 어떻게 만들었는지를 썼다. 일주일 만에 MVP를 완성했고, 꽤 만족스러웠다. 그런데 직접 쓰기 시작하니까 바로 문제가 보였다.

회의가 길어지면 중간에 쉬는 시간이 생기는데, 녹음을 끊고 새로 시작하면 회의록이 두 개로 쪼개진다. 반대로 쉬는 시간까지 녹음하면 Whisper가 무음 구간에서 환각을 일으킨다. 사이드 패널에서 회의가 10개를 넘으면 잘렸고, 요약 파서는 마크다운 헤딩 하나만 바뀌어도 깨졌다. “잘 되는데?” 싶었던 것들이 실전에서 하나둘 무너졌다.

v0.3.0은 그렇게 직접 쓰면서 발견한 문제들을 하나씩 고친 버전이다.

일시정지와 재개 — 회의는 끊기지 않는다

가장 먼저 필요했던 건 녹음 일시정지였다. 회의 중간에 5분 휴식, 잠깐 자리 비움 — 이런 상황이 매일 있다. 그런데 기존에는 선택지가 두 개뿐이었다.

  1. 녹음을 끊고 새로 시작한다 → 회의록이 분리됨
  2. 그냥 계속 녹음한다 → 무음 구간에서 환각 발생

둘 다 별로다. 일시정지가 있으면 깔끔하게 해결된다.

녹음 중 상태 — 일시정지 버튼과 경과 시간이 표시된다

구현 자체는 단순했다. RecordingSessionpaused 플래그를 추가하고, 일시정지 상태에서는 오디오 버퍼에 데이터를 쌓지 않으면 된다. 그런데 타이머에서 문제가 터졌다.

사이드 패널 헤더의 타이머는 벽시계(wall-clock) 기준이었고, 상태 바의 타이머는 경과 시간(elapsed) 기준이었다. 일시정지 후 재개하면 헤더는 점프하고 상태 바는 0으로 리셋됐다. 같은 녹음인데 두 곳에서 다른 시간을 보여주는 건 사용자를 혼란스럽게 만든다.

해결은 단일 시간 소스(single source of truth)로 통일하는 것이었다. plugin.getRecordedElapsedMs() 하나만 바라보게 만들어서, 어디서든 같은 경과 시간을 표시하게 했다.

이어 녹음 — 1 MD = 1 회의 원칙

일시정지만으로는 부족한 경우가 있었다. 오전 회의가 끝나고, 오후에 같은 주제로 이어서 회의할 때. 또는 녹음을 실수로 중지했을 때. 이런 상황에서 회의록이 쪼개지면 맥락이 끊긴다.

그래서 “이어 녹음” 기능을 만들었다. 핵심 원칙은 1 MD = 1 회의. 하나의 마크다운 문서가 하나의 논리적 회의를 대표한다.

사이드 패널 — 이어 녹음 버튼과 참석자 관리 화면

기술적으로는 이렇게 동작한다:

  1. “이어 녹음” 버튼을 누르면 새 WAV 파일이 생성되지만, meta.jsondocument_path는 원본 회의록을 가리킨다
  2. continued_from 필드로 원본 녹음과 연결된다
  3. 처리 시 _concatenate_wavs()로 WAV를 병합한 뒤, 하나의 전사 결과를 생성한다
  4. 사이드 패널에서는 _aggregate_recordings_by_document()로 같은 document_path를 가진 WAV들을 묶어서 하나의 항목으로 표시한다

여기서 가장 까다로웠던 건 삭제와 재처리의 연쇄(cascade) 처리였다. 원본 녹음을 삭제하면 이어 녹음한 WAV도 같이 삭제되어야 한다. 재처리를 요청하면 관련된 모든 WAV의 .done 마커와 처리 결과를 초기화해야 한다. _find_related_recordings() 함수가 document_path를 기준으로 관련 파일을 찾아서 일괄 처리한다.

처음에는 WAV 파일 단위로 사이드 패널에 표시했는데, 같은 회의가 두 번 나타나는 버그가 있었다. 문서 단위 집계로 바꾸면서 해결했다.

운영 버그 — 실전에서만 보이는 것들

직접 쓰면서 발견한 버그들은 테스트 환경에서는 재현하기 어려운 것들이었다.

요약 파서의 취약성

Claude가 생성하는 요약의 형식이 미묘하게 달라지는 경우가 있었다. ## 대신 ###을 쓰거나, 코드 펜스 안에 요약 키워드가 들어가거나, 요약 전에 서문을 붙이거나. 파서는 정확한 형식만 기대하고 있었기 때문에 조금만 달라져도 실패했다.

수정 후 파서는 ##/### 모두 처리하고, 코드 펜스를 건너뛰고, 서문을 무시하고, 레이블의 변형(| 구분 대체 표현)까지 수용한다. 그리고 실패 원인을 4가지로 분류해서(no-transcript, engine-missing, generation-failed, parse-failed) 각각에 맞는 안내 메시지를 마크다운에 남긴다. “요약 실패"라는 뭉뚱그린 메시지 대신 뭐가 왜 안 됐는지 알 수 있게 했다.

화자 라벨의 한국어 조사 문제

화자 등록 후 “화자1"을 실제 이름으로 치환할 때, 한국어 조사가 붙은 경우를 놓치고 있었다. “화자1이 말했다”, “화자1의 의견” 같은 패턴에서 “화자1” 부분만 바뀌고 조사는 그대로 남았다.

더 까다로운 건 “화자1"과 “화자10"의 구분이다. 단순 문자열 치환으로는 “화자10"을 처리할 때 “화자1"까지 잘못 치환된다. negative-lookahead 정규식으로 뒤에 숫자가 이어지지 않는 경우만 매칭하게 해서 해결했다.

사이드 패널의 소소한 불편들

  • 10개 제한: 완료된 회의가 10개를 넘으면 나머지가 안 보였다. 하드코딩된 제한을 제거
  • 이메일 설정 누락 알림: 발신자 이메일이 설정 안 되어 있으면 버튼만 비활성화되고 이유를 몰랐다. 배너로 안내 추가
  • 대기 중 결과 자동 갱신: pickupPendingResults() 후 패널이 갱신되지 않아서 수동 새로고침이 필요했다. 자동 갱신 처리
  • 마크다운 파일 누락 처리: 파일이 삭제/이동되었을 때 조용히 실패하던 것을 Notice로 사용자에게 알림
  • .env 인용 부호: .env 파일에서 값을 따옴표로 감싸면 따옴표까지 값으로 인식해서 SMTP 인증이 실패했다. 자동 제거 처리

하나하나는 작은 문제지만, 이런 것들이 쌓이면 “불안정한 도구"라는 인상을 준다.

Playwright E2E 테스트 — 0 수동 테스트

버그를 고치면서 점점 불안해졌다. 하나를 고치면 다른 데서 터질 것 같았다. 수동으로 매번 전체 시나리오를 확인하는 건 불가능했다. 테스트 자동화가 필요했다.

백엔드는 pytest로 비교적 쉬웠다. 74개 테스트가 API 엔드포인트, 보안, 엣지 케이스를 커버한다. 문제는 프론트엔드였다.

Meetnote의 프론트엔드는 Obsidian 플러그인이다. 브라우저 앱이 아니라 Electron 기반 데스크톱 앱 위에서 돌아간다. 일반적인 웹 E2E 테스트 프레임워크를 그대로 쓸 수 없다. 그래서 Playwright + Obsidian CDP(Chrome DevTools Protocol) 조합을 택했다.

Obsidian은 Electron 앱이므로 --remote-debugging-port 옵션으로 CDP를 열 수 있다. Playwright가 CDP를 통해 Obsidian에 연결하면, 실제 Obsidian 환경에서 플러그인을 조작하고 결과를 검증할 수 있다.

테스트 구조

plugin/tests/
├── SCENARIOS.md          # 시나리오 레지스트리 (50개, 단일 진실 공급원)
├── e2e/
│   ├── 01-side-panel.spec.ts         # S1-S2: 패널 상태
│   ├── 02-recording-flow.spec.ts     # S4: 녹음→일시정지→재개→중지
│   ├── 03-speakers.spec.ts           # S8-S9: 화자 DB
│   ├── 04-rename-processing.spec.ts  # S5-S6: 이름 변경, 진행 바
│   ├── 05-edge-cases.spec.ts         # S3, S13-S20: 오프라인, 중복 요청
│   ├── 06-participants.spec.ts       # S7, S24-S27: 참석자 관리
│   ├── 07-recording-list.spec.ts     # S28-S42: 목록, 집계, 이어 녹음
│   ├── 08-summary-rendering.spec.ts  # S32-S35: 요약 렌더링
│   └── 99-happy-path.spec.ts         # H1-H8: 실제 오디오 전체 파이프라인

SCENARIOS.md가 단일 진실 공급원(single source of truth) 역할을 한다. 50개 시나리오 각각에 ID, 설명, 매핑된 테스트 파일이 명시되어 있다. 새 기능을 추가하면 시나리오를 먼저 등록하고, 테스트를 작성하고, 구현한다.

Happy Path — 실제 오디오로 전체 검증

가장 공들인 건 99-happy-path.spec.ts다. 실제 녹음된 WAV 파일을 테스트 픽스처로 사용해서, 운영 환경과 동일한 흐름을 자동으로 검증한다.

H1. WAV + meta.json 셋업 → 처리 버튼 클릭
H2. STT + 화자 분리 완료 대기
H3. Claude CLI 요약 생성
H4. 화자 등록 → "화자N" 라벨 자동 치환 확인
H5. 수동 참석자 추가
H6. 최종 마크다운 검증 — 4개 요약 섹션, 전체 전사, 발화 통계, 화자N 라벨 0개
H7. 이메일 발송
H8. 완료된 회의 자동 열기

이 테스트 하나가 돌아가면 “오늘 배포해도 된다"는 확신이 생긴다. Whisper 모델 전사, pyannote 화자 분리, 화자 임베딩 매칭, LLM 요약, 라벨 치환, 이메일 발송까지 — 전체 파이프라인이 실제 오디오로 검증된다.

실제 E2E 테스트가 돌아가는 모습이다:

테스트 러너

run-tests.sh가 4단계 파이프라인을 순서대로 실행한다:

[1/4] Backend pytest — 74개 테스트
[2/4] TypeScript 타입 체크 + 빌드
[3/4] Playwright E2E — 42개 테스트
[4/4] Happy path — 8개 시나리오 (실제 오디오)

ML 모델 워밍업, 프로세스 격리(좀비 프로세스 방지), Obsidian CDP 자동 재시작까지 포함된 650줄짜리 스크립트다. pre-commit 훅으로 연결해서 커밋 전에 자동 실행된다.

처음에는 테스트 후 Obsidian과 백엔드 서버 프로세스가 좀비로 남는 문제가 있었다. nohup ... < /dev/null$! + disown 패턴으로 프로세스를 격리해서 해결했다. “Killed: 9” 에러 메시지도 사라졌다.

수동 테스트 0개

50개 시나리오 전부 자동화했다. 수동 테스트가 0개다. 이건 단순히 편의의 문제가 아니다. Obsidian 플러그인은 릴리스하면 되돌리기 어렵다. 사용자 환경에서 문제가 발생하면 디버깅도 힘들다. 자동화된 테스트가 배포 전 안전망이 된다.

사이드 패널 UI 개선

기능 수정과 함께 UI도 다듬었다. Lucide 아이콘 적용, 오프라인 상태 배너, 경과 시간 표시, Modal 다이얼로그(브라우저 기본 confirm() 대체) 등 10가지 이상의 소소한 개선을 넣었다.

개별적으로는 작은 변화지만, 전체적으로 “도구답다"는 느낌이 확실히 달라졌다. 특히 confirm() 대신 Obsidian Modal을 쓰니까 앱의 일부처럼 자연스러워졌다.

ADR 도입 — 결정의 기록

v0.3.0부터 Architecture Decision Record를 도입했다. 이어 녹음(ADR-003), 일시정지/재개(ADR-004) 같은 설계 결정을 문서로 남긴다.

혼자 만드는 프로젝트인데 ADR이 필요한가? 필요했다. 2주 전에 왜 이렇게 만들었는지 기억이 안 나는 순간이 실제로 왔다. “document_path 기준으로 집계하는 게 맞았나, WAV 파일 기준이었나?” 같은 질문에 ADR이 바로 답해준다.

숫자로 보는 v0.3.0

항목수치
변경 파일45개
추가+6,601줄
삭제-1,010줄
백엔드 테스트74개
E2E 테스트42개
Happy path8개
총 자동화 테스트124개
수동 테스트0개
시나리오 레지스트리50개

돌아보며

v0.1.0은 “돌아가는 것"을 만드는 단계였다. v0.3.0은 “쓸 수 있는 것"을 만드는 단계였다. 차이는 크다.

직접 쓰지 않았으면 발견하지 못했을 문제들이 대부분이었다. 타이머가 점프하는 것, 화자 라벨에 조사가 안 바뀌는 것, 회의 목록이 10개에서 잘리는 것 — 코드 리뷰만으로는 절대 잡을 수 없다. “자기가 만든 도구를 자기가 쓴다"는 원칙이 왜 중요한지 체감한 시간이었다.

Playwright로 Obsidian 플러그인 E2E 테스트를 구축한 건 이번 릴리스에서 가장 뿌듯한 부분이다. Electron 앱 위에서 돌아가는 플러그인이라 테스트 자동화가 까다로울 거라 생각했는데, CDP를 통한 연결이 예상보다 안정적이었다. 실제 오디오 파일로 전체 파이프라인을 검증하는 happy path 테스트가 돌아갈 때, “이제 자신 있게 배포할 수 있다"는 느낌이 든다.

다음은 웹 기반 대시보드와 팀 단위 화자 DB 공유 — 가 될 수도 있고, 또 직접 쓰면서 다른 게 급해질 수도 있다. 그게 이 프로젝트의 방식이다.


Meetnote — 로컬 회의록 자동화

Obsidian plugin for local meeting transcription, speaker diarization, and AI summarization

GitHub에서 보기 →