음성봇을 스텁부터 만든 이유 — 테스트로 설계를 먼저 굳히다

시리즈 · 음성봇 직접 만들기 — 실시간 음성봇의 뼈대를 바닥부터 만든 8편 기록입니다. 1. 끼어들기(barge-in) — 재생 중인 TTS를 실시간으로 끊는 법 2. "말이 끝났다"는 판단 — VAD 엔드포인터 3. 엔진을 코드 수정 없이 바꾸는 인터페이스 설계 4. 로컬 LLM 대화 두뇌와 도구 호출 루프 5. 매 턴 지연시간을 재는 이유 6. 브라우저 마이크에서 서버까지 — 오디오 프로토콜 7. 로컬 Whisper STT 연결하기 8. 스텁부터 만든 이유 — 테스트 우선 설계 (완결) — 지금 보는 글 돌아보며 시작하는 마지막 편 이 시리즈를 시작할 때부터 정해 둔 규칙이 하나 있었습니다. 진짜 음성 인식 모델과 진짜 합성 모델이 준비되기 전까지는, 그 둘을 흉내만 내는 가짜 엔진으로 전체 구조를 먼저 돌려본다는 규칙입니다. 여덟 편을 거치며 끼어들기 처리, 상태 흐름, 프로토콜, 엔진 인터페이스를 하나씩 다뤘는데, 그 모든 것이 사실 이 가짜 엔진 위에서 검증됐습니다. 지금 코드를 다시 열어 보면 그때는 별생각 없이 세운 규칙이 결과적으로 시리즈 전체의 진행 속도를 결정했다는 게 눈에 들어옵니다. 이번 편에서는 왜 굳이 가짜부터 만들었는지, 그리고 그 선택이 시리즈 전체에 어떤 영향을 남겼는지 정리하면서 이 시리즈를 마무리하려 합니다. 실제 모델을 기다리지 않기로 한 이유 음성봇의 핵심 로직은 사실 음성 인식이나 음성 합성 자체가 아닙니다. 사용자가 말을 시작했는지 끝냈는지 판단하는 상태머신, 합성 중에 끼어들기가 들어왔을 때 스트림을 멈추는 동시성 처리, 이벤트가 순서대로 오가는 흐름 제어 같은 것들이 훨씬 까다롭습니다. 반면 실제 인식·합성 모델은 무겁고 설치가 까다롭고 실행 속도도 느립니다. 이 두 가지를 한꺼번에 붙잡고 있으면 어려운 동시성 버그를 실제 모델의 로딩 시간과 뒤섞어 디버깅하게 됩니다. 모델이 느려서 반응이 늦은 건지, 상태머신이 잘못 짜여서 늦은 건지 구분이 안 되는 상황을 미...

Claude Code로 만족도 설문 만들기 — 교육자료만 주고 30분 만에 맞춤 설문


교육 끝나기 30분 전, 설문이 필요해졌습니다

AI 코딩 교육을 하루 진행했습니다. 세 개 세션으로 짜인 과정이었습니다. 왜 지금 AI로 코딩하는가, Claude Code를 어떻게 다루는가, 마지막은 직접 에이전트를 하나 만들어 보는 핸즈온이었습니다.

교육이 거의 끝나갈 무렵 한 가지가 빠졌다는 걸 알았습니다. 만족도 설문입니다. 다음 교육을 더 낫게 만들려면 참가자들의 솔직한 반응이 필요했고, 무엇보다 그 자리에서 받아야 했습니다. 집에 가면 응답률이 반의반으로 떨어집니다.

남은 시간은 30분. 구글폼을 열고 문항을 하나하나 짜기엔 빠듯했습니다. 그래서 다른 선택을 했습니다. 그 자리에서, 참가자들이 보는 앞에서 설문을 직접 만들기로 했습니다. 미리 준비한 게 아니라 즉석이었습니다.

Claude Code로 즉석에서 만든 만족도 설문 폼
Claude Code로 즉석에서 만든 만족도 설문 폼

왜 구글폼이 아니라 직접 만들었나

구글폼은 빠릅니다. 하지만 이날은 직접 만드는 쪽을 택했습니다. 이유는 세 가지였습니다.

  • 데모 그 자체가 교육이었습니다. "AI로 코딩하면 이만큼 빠르다"를 한 시간 설명하는 것보다, 눈앞에서 도구 하나가 30분 만에 튀어나오는 걸 보여주는 게 훨씬 강합니다. 설문 제작 과정이 곧 마지막 실습이 됐습니다.
  • 데이터가 내 손에 남습니다. 폼과 응답, 집계 방식까지 전부 직접 통제할 수 있습니다.
  • 교육에 딱 맞는 문항이 필요했습니다. 범용 만족도 설문이 아니라, 그날 다룬 세션과 실습을 정확히 겨냥한 문항이 필요했습니다.

마지막 이유가 핵심이었습니다. 그리고 이게 이 글에서 가장 하고 싶은 이야기입니다.

내가 한 건 두 가지뿐이었습니다

라이브로 설문을 만들면서 실제로 한 일은 딱 두 가지입니다.

  1. 교육자료와 커리큘럼을 통째로 Claude Code에 줬습니다. 슬라이드, 세 세션의 구성, 핸즈온에서 무엇을 만들었는지가 담긴 자료입니다.
  2. "이 교육 만족도 설문 만들어줘"라고 했습니다. 그게 끝입니다.

문항을 불러주지 않았습니다. "1번은 전반 만족도, 2번은 NPS, 3번은…" 같은 지시를 한 줄도 하지 않았습니다. 교육 맥락을 던지고 목적만 말했습니다.

30분 뒤 나온 맞춤 설문

결과물을 보고 놀란 건 저였습니다. Claude Code가 만든 설문에는 그날 교육의 맥락이 그대로 박혀 있었습니다.

세션 구성을 정확히 반영한 세션별 평가가 들어가 있었습니다.

Session 1 · Why AI Coding (왜 지금 바이브 코딩인가)
Session 2 · Claude Code Guide (사고방식·명령어·워크플로우)
Session 3 · Hands-on (에이전트 직접 빌드 실습)

핸즈온에서 실제로 쓴 도구 이름까지 집어낸 문항도 있었습니다.

환경 설치·설정(Notion·Discord 등)은 매끄러웠나요?
실습(YouTube→Notion 에이전트)을 끝까지 따라가셨나요?

이건 범용 템플릿에서는 절대 나오지 않습니다. "환경 설치가 매끄러웠나"를 묻는 일반 설문은 많지만, 그날 참가자들이 손으로 만진 바로 그 도구를 짚어 묻는 문항은 교육 맥락을 이해해야만 나옵니다. 제가 시킨 게 아니라, 자료를 읽고 알아서 구성한 것입니다.

그 외에도 갖출 건 다 갖췄습니다.

  • 전반 만족도(5점)와 동료 추천 의향(NPS, 0~10)
  • 난이도·분량·이론과 실습의 비중
  • 실습 완성도와 "이제 스스로 비슷한 걸 만들 수 있겠다는 자신감" 측정
  • 업무 적용 의향과 다음에 배우고 싶은 주제
  • 자유 의견 3종

구글폼으로 이만큼을 짜려면 문항 설계에만 30분이 더 걸립니다. 저는 그 시간을 "자료 주고 한 마디 하는 것"으로 압축했습니다.

교육 세션과 실습 도구를 그대로 반영한 문항
교육 세션과 실습 도구를 그대로 반영한 문항

응답은 어떻게 받았나 — Google Apps Script

폼만 있으면 반쪽입니다. 응답을 모아야 합니다. 외부 서버 없이 구글 계정만으로 끝내고 싶었고, Google Apps Script 웹앱이 답이었습니다.

스프레드시트에 Apps Script를 붙이고 웹앱으로 배포하면, 그 URL로 POST를 받아 시트에 한 줄씩 쌓을 수 있습니다. 수신 쪽 코드는 이 정도입니다.


const SHEET_NAME = 'responses';

function doPost(e) {
  const lock = LockService.getScriptLock();   // 동시 제출이 몰려도 한 번에 한 행씩
  lock.waitLock(30000);
  try {
    const data = JSON.parse(e.postData.contents);
    const ss = SpreadsheetApp.getActiveSpreadsheet();
    let sheet = ss.getSheetByName(SHEET_NAME) || ss.insertSheet(SHEET_NAME);

    data._receivedAt = new Date();

    // 헤더(첫 행)를 응답 키에 맞춰 자동 생성·확장
    const lastCol = sheet.getLastColumn();
    let header = lastCol > 0 ? sheet.getRange(1, 1, 1, lastCol).getValues()[0].filter(String) : [];
    let changed = false;
    Object.keys(data).forEach(k => { if (header.indexOf(k) === -1) { header.push(k); changed = true; } });
    if (header.length && (changed || sheet.getLastRow() === 0)) {
      sheet.getRange(1, 1, 1, header.length).setValues([header]);
    }

    sheet.appendRow(header.map(k => (k in data ? data[k] : '')));
    return ContentService.createTextOutput(JSON.stringify({ ok: true }))
      .setMimeType(ContentService.MimeType.JSON);
  } catch (err) {
    return ContentService.createTextOutput(JSON.stringify({ ok: false, error: String(err) }))
      .setMimeType(ContentService.MimeType.JSON);
  } finally {
    lock.releaseLock();
  }
}

여러 명이 동시에 제출해도 안전하도록 LockService로 한 번에 한 행씩 기록하고, 응답에 새 항목이 생기면 헤더를 자동으로 맞추도록 했습니다.

Apps Script 편집기에 붙여넣은 응답 수집 코드(doPost)
Apps Script 편집기에 붙여넣은 응답 수집 코드(doPost)

폼에서 보내는 쪽은 평범한 fetch입니다. 단, 한 군데 함정이 있습니다.


fetch(ENDPOINT, {
  method: "POST",
  headers: { "Content-Type": "text/plain;charset=utf-8" }, // CORS 회피 포인트
  body: JSON.stringify(data)
});

자주 막히는 곳 — Apps Script CORS 에러

Apps Script 웹앱은 처음 붙일 때 두세 군데에서 꼭 막힙니다. 라이브에서도 잠깐 멈췄던 지점들입니다.

1. application/json으로 POST하면 막힙니다

브라우저에서 Content-Type: application/json으로 POST하면 CORS preflight(OPTIONS) 요청에 걸려 차단됩니다. Apps Script는 이 preflight를 제대로 처리하지 못합니다. 해결책은 Content-Type을 text/plain으로 보내는 것입니다. 그러면 preflight가 없는 "단순 요청"으로 분류돼 그냥 통과합니다. 받는 쪽에서는 어차피 JSON.parse로 풀기 때문에 문제없습니다.

2. 배포 권한을 "모든 사용자"로 두지 않으면 401

웹앱 배포 시 액세스 권한을 "나만"으로 두면 외부 폼에서 호출할 때 막힙니다. "액세스 권한이 있는 사용자: 모든 사용자"로 배포해야 합니다.

3. 코드를 고쳤는데 반영이 안 됩니다

Apps Script는 배포를 "버전"으로 관리합니다. 코드를 수정한 뒤 새 버전으로 다시 배포하지 않으면 예전 코드가 그대로 돕니다. "배포 관리 → 편집 → 새 버전"을 잊지 마세요.

전송 실패에 대비해 폴백도 하나 넣었습니다. 엔드포인트가 비어 있거나 전송이 실패하면 mailto로 응답을 메일 본문에 채워 보내도록 했습니다. 네트워크가 불안한 교육장에서는 이런 안전장치가 한 번씩 사람을 살립니다.

결과를 리포트로

설문이 돌기 시작하자 응답이 시트에 쌓였습니다. 그다음은 집계입니다. 시트에 모인 응답을 다시 정리해서, 무엇이 좋았고 무엇이 아쉬웠는지, NPS는 몇 점인지, 자신감 점수 분포는 어떤지를 한 장짜리 리포트로 묶었습니다.

집계에서 실제로 본 항목은 이런 것들입니다.

  • 전반 만족도 평균과 NPS(추천 9~10 비율에서 0~6 비율을 뺀 값)
  • 세션별 유익도 — 어느 세션이 가장 반응이 좋았는지
  • 실습 완성률 — "정상 동작까지 확인" 비율
  • 자신감 점수 — 교육의 진짜 목표는 "혼자서도 만들 수 있다"는 감각을 남기는 것이라 이 문항을 가장 중요하게 봤습니다
  • 자유 의견에서 반복되는 키워드
응답 집계 리포트 요약(수치는 예시로 일반화)
응답 집계 리포트 요약(수치는 예시로 일반화)

설문을 만들고, 응답을 받고, 리포트로 정리하는 이 전체가 길게 잡아도 반나절 안에 끝났습니다. 그중 폼을 만드는 데 든 시간은 30분이었습니다.

무엇을 배웠나

이날 가장 크게 남은 건 기술이 아니었습니다.

"잘 시키는 것"보다 "맥락을 충분히 주는 것"이 먼저입니다. 저는 문항을 정교하게 지시하지 않았습니다. 대신 교육의 전체 맥락을 통째로 줬습니다. 그러자 에이전트가 그 맥락 안에서 알아서 적절한 문항을 구성했습니다. 프롬프트를 깎는 데 시간을 쓰는 것보다, 충분한 재료를 주는 게 더 빠르고 더 정확할 때가 많습니다.

그리고 하나 더 있습니다. 도구는 필요한 순간에 그 자리에서 만들면 됩니다. 설문이 필요하면 설문을, 집계가 필요하면 집계 도구를 30분 안에 만들 수 있다면, "이런 도구가 없어서 못 한다"는 말은 점점 핑계가 됩니다. 그날 참가자들에게 보여주고 싶었던 것도 결국 이것이었습니다.

오늘 만든 설문 폼은 다음 교육에서도 그대로 씁니다. 30분 투자로 계속 쓰는 자산이 하나 생긴 셈입니다.


자주 묻는 질문

Q. 구글폼이 더 편하지 않나요?

A. 문항이 표준적이라면 구글폼이 맞습니다. 다만 디자인을 바꾸거나, 응답 데이터를 직접 가공하거나, 그날 맥락에 딱 맞는 문항이 필요할 때는 직접 만드는 편이 더 빠르고 자유롭습니다.

Q. 코딩을 모르는데 가능한가요?

A. 이 작업에서 제가 직접 작성한 코드는 거의 없습니다. 교육자료를 주고 요청했을 뿐입니다. Apps Script 연동 같은 부분만 안내를 따라가면 됩니다.

Q. 응답이 안 들어와요.

A. 십중팔구 CORS 또는 배포 권한 문제입니다. 위의 "자주 막히는 곳" 세 가지를 먼저 확인해 보세요.


이 글의 모든 내용은 운영자가 직접 진행한 교육과 직접 만든 도구를 기반으로 합니다. 소속·참가자 식별 정보 및 실제 응답 데이터는 포함하지 않으며, 수치와 화면은 예시로 일반화했습니다. (2026년 6월 기준)

댓글

이 블로그의 인기 게시물

디스코드 구글캘린더 연동 봇 만들기: 카톡 캡처 OCR 자동 등록 시스템 구축

클로드 코드 비용 줄이는 법 — ollama 로컬 LLM 연동 (M3 맥북)

유튜브 노션 자동 저장 봇 만들기 — Claude CLI MCP + Discord 233일 운영기