6시간 멘토링은 즐거운데!! 왜 때문에 꼴랑!?! 서류 3장은 괴로울까?

Gale · 비즈니스 / 재테크 · · 좋아요 0 · 답글 0

연재 스타트업 AI 실험실 4/8편

1. 처음 설계한 구조: “매일 쓰는 4가지 작업”을 한 화면에 모으기

J 스튜디오는 localhost:3737에서 실행되는 로컬 앱으로 만들었습니다. Dock의 앱 아이콘을 더블클릭하면 서버가 켜지고 브라우저가 열리도록 구성했습니다.

기능은 크게 4가지로 나눴습니다.

  1. STT 받아쓰기
    음성 파일 업로드 또는 라이브 받아쓰기로 텍스트를 만드는 기능

  2. 기록 정리
    STT 결과나 원문을 바탕으로 멘토링일지, 회의록, 강의 정리, 보고서 생성

  3. 사업 아이템 점검
    하나의 아이템을 여러 프레임워크로 병렬 분석

  4. 스터디 댓글
    사례글을 읽고 댓글과 인증 블록을 생성

전체 구조는 이런 흐름이었습니다.

음성 / 원문 / URL 입력
        ↓
STT 또는 LLM 처리
        ↓
화면 결과 표시
        ↓
Downloads 산출물 저장
        ↓
필요 시 옵시디언 기록 폴더에 저장

이때 중요한 원칙은 결과를 화면에만 남기지 않는 것이었습니다. 생성 결과가 사라지면 다시 만들 수 있지만, 그 과정에서 사용자는 불안해집니다. 그래서 모든 기능은 가능한 한 .txt, .md, .docx, .webm 같은 파일 결과를 남기도록 설계했습니다.

2. STT에서 배운 첫 번째 교훈: 에러 메시지는 정직해야 한다

처음에는 OpenAI 기반 STT를 붙였습니다. 그런데 화면에는 단순히 이런 메시지만 나왔습니다.

Connection error.

처음에는 네트워크 문제처럼 보였습니다. 하지만 직접 요청을 추적해보니 실제 원인은 네트워크가 아니라 OpenAI 크레딧/쿼터 소진으로 인한 429 오류였습니다.

그래서 에러 처리 로직을 바꿨습니다.

// 핵심 아이디어
// SDK가 Connection error로 감싸더라도
// status, code, constructor.name, cause.code를 함께 확인한다.

if (
  status === 429 ||
  code === 'insufficient_quota' ||
  constructorName === 'APIConnectionError' ||
  causeCode === 'ECONNRESET'
) {
  throw new Error(
    'OpenAI 요청이 연결 단계에서 끊겼습니다. 가장 흔한 원인은 크레딧/쿼터 소진입니다. Billing 확인 또는 로컬 엔진 전환을 권장합니다.'
  );
}

이 경험을 통해 “LLM 앱에서 좋은 UX는 멋진 화면보다 정직한 실패 메시지에서 시작된다”는 걸 다시 느꼈습니다.

3. 로컬 STT에서 만난 두 번째 삽질: arm64와 x86_64

OpenAI 비용 문제를 피하기 위해 로컬 STT 엔진도 붙였습니다. mlx-whisper를 사용했는데, Mac 환경에서 아키텍처 문제가 생겼습니다.

문제는 Python이 x86_64로 떠서 arm64 전용 mlx 모듈을 읽지 못하는 것이었습니다.

해결은 단순하지만 명확했습니다.

arch -arm64 python3 whisper_script.py

Node에서 Python을 실행할 때도 macOS에서는 arm64 실행을 강제했습니다.

// macOS에서는 부모 프로세스의 아키텍처와 무관하게 arm64 Python 실행
spawn('arch', ['-arm64', pythonPath, scriptPath, audioPath]);

여기서 배운 점은 “로컬 앱은 사용자의 실행 환경까지 제품의 일부”라는 점이었습니다. 코드가 맞아도 Dock 런처, Node 프로세스, Python 인터프리터의 실행 방식이 다르면 앱은 깨질 수 있었습니다.

4. 긴 음성 파일에서 멈춘 진짜 이유: stdout을 읽지 않아서 생긴 deadlock

더 흥미로운 문제는 긴 파일에서 발생했습니다.

짧은 음성 파일은 잘 전사되는데, 100분이 넘는 긴 파일은 어느 순간 멈췄습니다. 프로세스는 살아 있는데 CPU는 거의 쓰지 않고, 화면도 더 이상 진행되지 않았습니다.

원인은 mlx-whisper가 verbose=True 상태에서 전사 세그먼트를 stdout으로 계속 출력하는데, Node 쪽에서 stderr만 읽고 stdout을 읽지 않았던 것이었습니다.

즉, stdout 파이프 버퍼가 차면서 Python의 출력이 막히고, 전사도 함께 멈춘 것입니다.

수정은 이렇게 했습니다.

proc.stdout.on('data', (chunk) => {
  const text = chunk.toString();
  onLog?.(text); // 실시간 진행 로그로 화면에 표시
});

proc.stderr.on('data', (chunk) => {
  const text = chunk.toString();
  onLog?.(text);
});

단순히 로그를 보기 위한 코드처럼 보이지만, 실제로는 파이프를 계속 비워서 긴 파일 처리 중 deadlock을 막는 핵심 코드였습니다.

이때부터 STT 화면에는 “멈춘 것처럼 보이는 대기”가 아니라 실제 세그먼트 진행 로그가 표시되도록 바뀌었습니다.

5. 라이브 받아쓰기의 불안정함을 원본 녹음으로 보완하기

라이브 받아쓰기는 브라우저의 Web Speech API를 사용했습니다. 사용성은 좋지만, 네트워크 오류가 나면 해당 구간의 텍스트가 사라질 수 있다는 문제가 있었습니다.

그래서 질문을 바꿨습니다.

“받아쓰기를 더 안정적으로 만들 수 있을까?”
보다
“받아쓰기가 실패해도 원본을 잃지 않게 만들 수 있을까?”

해결책은 동시 녹음이었습니다.

Web Speech API로 텍스트를 받는 동시에, MediaRecorder로 원본 음성을 .webm 파일로 저장했습니다.

const recorder = new MediaRecorder(stream);
const chunks: BlobPart[] = [];

recorder.ondataavailable = (event) => {
  if (event.data.size > 0) chunks.push(event.data);
};

recorder.onstop = async () => {
  const audioBlob = new Blob(chunks, { type: 'audio/webm' });
  // 서버에 저장하고, 필요하면 Whisper 재전사로 넘긴다.
};

이후 사용자는 라이브 받아쓰기가 끊기더라도 녹음본을 다시 Whisper로 전사할 수 있게 되었습니다.

결과적으로 STT는 다음 구조가 되었습니다.

라이브 받아쓰기
  ├─ 실시간 텍스트 저장
  └─ 원본 음성 .webm 저장
          ↓
     필요 시 Whisper로 재전사

개인적으로 이 변경이 가장 마음에 들었습니다. AI 기능의 정확도를 높이는 것도 중요하지만, 실제 업무에서는 복구 가능한 구조가 훨씬 더 큰 안정감을 주기 때문입니다.

6. 멘토링 일지를 “기록 정리 4종 허브”로 확장하기

처음 기록 정리 기능은 멘토링 일지 하나만 만들었습니다. 그런데 실제 업무를 돌아보니 멘토링만 있는 것이 아니었습니다.

  • 회의록

  • 강의 정리

  • 보고서

  • 멘토링 일지

모두 “긴 기록을 읽고 구조화된 문서로 만드는 일”이었습니다.

그래서 기능을 4개로 따로 만들지 않고, 하나의 범용 모델로 통일했습니다.

type StudyDoc = {
  docType: 'mentoring' | 'meeting' | 'lecture' | 'report';
  subject: string;
  date: string;
  classifier: string;
  author?: string;
  meta: Array<{ key: string; value: string }>;
  sections: Array<{
    title: string;
    kind: 'prose' | 'bullets' | 'numbered' | 'titledList' | 'checklist';
    items?: string[];
    body?: string;
  }>;
};

문서 유형마다 프롬프트와 섹션 구조만 다르게 두고, 렌더링·마크다운 변환·docx 생성·옵시디언 저장 로직은 하나로 공유했습니다.

예를 들어 멘토링 일지는 이런 구조를 사용했습니다.

1. 기업 및 아이템 현황
2. 멘토링 주요 논의사항
3. 멘토 진단 및 제언
4. 향후 실행과제

회의록은 이렇게 달라집니다.

1. 회의 개요
2. 주요 논의사항
3. 결정사항
4. 액션 아이템

이 설계 덕분에 나중에 다섯 번째 문서 유형을 추가하더라도, 새 페이지를 만드는 것이 아니라 DOC_TYPES 설정과 프롬프트만 추가하면 되는 구조가 되었습니다.

7. 옵시디언 저장에서 만난 예상 밖의 문제: YAML 키 중복

기록 정리 결과는 옵시디언의 50.기록정리 폴더로 저장되도록 했습니다.

50.기록정리/
  ├─ 멘토링 일지/{의뢰처}/
  ├─ 회의록/{분류}/
  ├─ 강의 정리/{분류}/
  ├─ 보고서/{분류}/
  └─ 아이템 점검/{프레임워크}/

그런데 저장된 마크다운 파일에서 옵시디언 속성 오류가 발생했습니다.

원인은 YAML frontmatter의 키 중복이었습니다. 예를 들어 앱에서 이미 classifier를 저장했는데, LLM이 추출한 meta에도 같은 의미의 키가 다시 들어가면 YAML 속성이 깨졌습니다.

그래서 frontmatter 생성 단계에서 중복 키를 방어했습니다.

const used = new Set<string>();

function addMeta(key: string, value: string) {
  const normalizedKey = normalizeKey(key);
  if (used.has(normalizedKey)) return;
  if (isReservedKey(normalizedKey)) return;

  used.add(normalizedKey);
  lines.push(`${yamlKey(key)}: ${yamlScalar(value)}`);
}

이 문제는 작지만 중요했습니다. AI가 생성한 메타데이터를 그대로 신뢰하면, 문서 저장소 쪽에서 깨질 수 있습니다. 그래서 LLM 출력은 항상 “정규화하고, 필터링하고, 저장 가능한 형태로 바꾸는 과정”이 필요했습니다.

8. 스터디 댓글 기능: 자동화하되, 사람의 톤은 잃지 않기

스터디 댓글 기능도 함께 다듬었습니다.

초기 댓글은 친절하긴 했지만, 가끔 ~입니다, ~에요처럼 결론을 단정하는 문장이 많았습니다. 스터디 리더의 댓글은 선생님처럼 평가하는 톤이 아니라, 동료로서 보고 배우고 반응하는 톤이어야 했습니다.

그래서 댓글 생성 규칙에 이런 방향을 넣었습니다.

- 핵심 인사이트는 단정형보다 감탄·깨달음형으로 마무리한다.
- 예: “이건 단순한 기획 정리가 아니군요!!”
- 스터디장은 선생님이 아니라 피어러닝 리더다.

기술적으로는 댓글 생성과 인증 블록 생성, 키워드·작성자 추출을 병렬로 실행했습니다.

const [commentRes, certRes, keywordRes] = await Promise.all([
  generateComment(),
  generateCertBlock(),
  extractKeywordAndAuthor(),
]);

또한 생성된 댓글 파일에는 YAML frontmatter를 붙여 최근 댓글 히스토리에서 다시 불러올 수 있게 했습니다.

---
author: 이민서
keyword: 레코드설계
tone: 기본
tools: Cursor, n8n
sourceUrl: https://example.com
createdAt: 2026-05-28T00:53:23.079Z
---

자동화의 목적은 사람의 감각을 없애는 것이 아니라, 반복 작업을 줄이고 더 좋은 피드백에 집중할 수 있게 만드는 것이라는 점을 다시 확인했습니다.

결과 1. 흩어진 작업이 하나의 로컬 작업실로 모였다

이전에는 받아쓰기, 문서 정리, 사업 아이템 점검, 댓글 작성이 각각 다른 흐름이었습니다. 이제는 J 스튜디오 안에서 입력부터 저장까지 이어집니다.

STT → 기록 정리 → 파일 저장 → 옵시디언 보관
사업 아이템 입력 → 5개 프레임워크 분석 → 옵시디언 보관
사례글 URL 입력 → 댓글/인증 생성 → 최근 댓글 히스토리 저장

작업이 줄었다기보다, 작업 사이의 끊김이 줄었다는 느낌이 더 컸습니다.

결과 2. “AI가 실패해도 원본은 남는다”는 안정감이 생겼다

라이브 받아쓰기에 동시 녹음을 붙인 뒤 가장 큰 변화는 심리적 안정감이었습니다.

받아쓰기가 완벽하지 않아도 괜찮습니다. 네트워크가 끊겨도 괜찮습니다. 원본 음성이 남아 있으니 다시 처리할 수 있습니다.

이번 작업에서 가장 중요한 설계 원칙은 이것이었습니다.

AI 결과보다 원본 데이터가 더 중요하다.
원본을 잃지 않는 구조가 먼저다.

결과 3. 문서 유형을 늘릴 수 있는 구조가 되었다

처음에는 멘토링 일지 하나를 잘 만드는 것이 목표였습니다. 하지만 범용 StudyDoc 모델로 바꾸면서 기록 정리 기능은 4종 허브가 되었습니다.

이제 새로운 문서 유형을 추가할 때도 전체 구조를 다시 만들 필요가 없습니다.

새 문서 유형 추가 = DOC_TYPES 설정 + 프롬프트 추가

기능을 많이 만드는 것보다, 확장 가능한 중심 모델을 먼저 잡는 것이 유지보수에 훨씬 유리했습니다.

결과 4. 로컬 도구일수록 UX가 더 중요했다

이 앱은 다수 사용자를 위한 SaaS가 아니라, 한 사람이 매일 쓰기 위한 로컬 도구입니다. 그래서 인증, 멀티테넌트, 권한 같은 기능은 과감히 제외했습니다.

대신 이런 부분에 더 신경 썼습니다.

  • Dock에서 바로 실행되기

  • 결과가 자동으로 파일로 남기

  • 최근 결과가 복원되기

  • 오류 메시지가 원인을 숨기지 않기

  • 옵시디언 저장 위치가 실제 업무 폴더와 맞기

1인용 도구의 완성도는 거창한 기능보다 “매일 다시 쓰고 싶은가?”에서 결정된다는 걸 느꼈습니다.

다음 계획

앞으로는 J 스튜디오를 “기록을 만드는 도구”에서 조금 더 나아가, “기록을 다시 활용하는 도구”로 확장해보고 싶습니다.

예를 들면 이런 방향입니다.

  • 옵시디언에 저장된 기록을 다시 검색해서 다음 멘토링에 활용하기

  • 멘토링 일지와 회의록의 액션 아이템을 모아 추적하기

  • 사업 아이템 점검 결과를 시간순으로 비교하기

  • 이번 작업을 하면서 가장 크게 느낀 건, AI 도구를 잘 쓰는 방법은 단순히 프롬프트를 잘 쓰는 것이 아니라는 점이었습니다.

프롬프트, 저장 구조, 오류 복구, 파일 흐름, 사용자의 실제 습관이 함께 맞물릴 때 비로소

“내가 매일 쓰는 도구”가 되는 것 같습니다.

태그: #스타트업실험실