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

- 작성자: [Gale](https://community.clauders.ai/member/imported-member-qrbGdBTz59)
- 공간: [비즈니스 / 재테크](https://community.clauders.ai/business)
- 게시: 2026-05-31
- 연재: [스타트업 AI 실험실](https://community.clauders.ai/series/startup-agent-experiments) 2/3편
- 태그: #스타트업실험실
- 주소: https://community.clauders.ai/business/post/6%EC%8B%9C%EA%B0%84-%EB%A9%98%ED%86%A0%EB%A7%81%EC%9D%80-%EC%A6%90%EA%B1%B0%EC%9A%B4%EB%8D%B0-%EC%99%9C-%EB%95%8C%EB%AC%B8%EC%97%90-%EA%BC%B4%EB%9E%91-%EC%84%9C%EB%A5%98-3%EC%9E%A5%EC%9D%80-%EA%B4%B4%EB%A1%9C%EC%9A%B8%EA%B9%8C-ab383c3f-2b64-8b68-83c8-fa301b14055d

---

### 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 도구를 잘 쓰는 방법은 단순히 프롬프트를 잘 쓰는 것이 아니라는 점이었습니다.

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

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