Skip to main content
← 블로그

비어있는 description, AI 가 대신 써줍니다

VauDium ·

Task 의 title, 의도, 기대 결과를 바탕으로 AI 가 description 본문을 만들어주는 기능. 가벼운 제목엔 짧게, 진지한 작업엔 구체적으로 — 결과가 한 줄씩 나타나는 경험까지.

비어있는 description, AI 가 대신 써줍니다

Task 를 만들 때 가장 자주 비워두는 칸이 description 입니다. 제목만 적어두고 본문은 미루는 경우가 대부분이에요. 막상 채우려면 무엇부터 써야 할지 막막하고요.

그래서 description 칸이 비어 있을 때, AI 가 대신 채워주는 버튼을 넣었습니다. 다나 한 개로요.

어떻게 만드는가

입력은 task 가 이미 갖고 있는 정보입니다. title, 그리고 — 작성하셨다면 — 현재 상황(target), 기대 결과(expectation), 난관(obstacle). 이 네 가지를 종합해 AI 가 “어떻게 진행할지” 본문을 만듭니다.

처음엔 단순했습니다. “구체적·실행 가능한 plan 을 3~5 단원, 접근·작업영역·위험·체크포인트 구성으로.” 결과는 그럴듯해 보였습니다.

그러다 “멋진 아침이에요!” 라는 제목으로 돌려봤습니다. 본문은 이렇게 나왔습니다.

Approach — Begin with a gentle sequence that shifts attention from sleep to action…

Immediate actions to start the day — Hydrate and open a window or bring in natural light…

Sustaining momentum and focus — Work in a single prioritized direction…

Risks, obstacles, and checkpoints — Grogginess: Rely on movement and hydration…

감탄사 하나에 자기계발서 한 챕터가 돌아온 셈입니다. 입력에 없는 “물 마시기”, “창문 열기” 같은 행동들이 채워졌고, “Risks, obstacles, checkpoints” 라는 섹션 제목은 프롬프트가 강제한 구조 그대로였습니다.

프롬프트를 다시

문제는 두 가지였습니다.

첫째, 구조 강제. “approach/risks/checkpoints” 라는 4단을 모든 task 에 박아놓으니, 짧은 감정 표현에도 큰 그릇이 씌워졌습니다.

둘째, 빈 입력에 대한 채움. 입력이 부족하면 AI 는 일반론으로 그릇을 채우려 합니다. “Hydrate”, “open a window” 같은 generic self-help 가 들어오는 거죠. Task 와 직접 관련 없는 행동인데도요.

프롬프트를 title 의 성격별로 분기하도록 다시 썼습니다.

  • 실행 가능한 작업 (“보고서 작성”) → 구체적 계획, 3~5 단원
  • 학습·조사 (“Rust 공부”) → 무엇을 알아볼지, 2~4 단원
  • 감정·상태 (“멋진 아침이에요!”) → 짧은 메모·관찰·격려, 1~2 단원

그리고 명시적으로 적었습니다. “입력에 없는 행동 항목을 채워 넣지 마세요 — ‘물 마시기’, ‘창문 열기’ 같이 task 와 무관한 generic self-help 금지.”

같은 “멋진 아침이에요!” 로 다시 돌렸습니다.

Morning mood — Savor the gentle light and the calm of this hour…

한 단원, 짧은 한 단락. 톤도 가벼움. 입력에 비해 과하지 않은 분량.

결과가 한 줄씩 나타나기

본문 한 편이 한꺼번에 뿅 나타나는 건 어색합니다. 3~5 초 동안 사용자는 빈 화면만 보고요. 그래서 OpenAI 스트리밍 응답을 SSE 로 클라이언트까지 흘렸습니다.

서버는 토큰이 들어올 때마다 누적된 HTML 을 그대로 발행합니다. 클라이언트는 chunk 를 받을 때마다 editor 의 내용을 그 시점의 누적 값으로 갈아끼웁니다. 사용자에겐 본문이 한 줄씩 늘어나는 것처럼 보입니다.

서브태스크 스트리밍 글 에서 JSON 을 brace depth 로 쪼개 하나씩 뽑던 것과 비교하면, description 은 HTML 이라 훨씬 쉽습니다. 그냥 누적된 텍스트를 통째로 보내면 됩니다. tag 가 닫히지 않은 중간 상태여도 editor 가 받아내고요.

보이지 않게 만든 race 하나

여기서 끝나는 줄 알았는데, mobile 에서 한 가지 이상한 증상이 있었습니다. 이미 내용이 있는 상태에서 AI 버튼을 누르면, 화면이 갱신되지 않는다. chunk 는 잘 들어오고 React state 도 업데이트되는데 native editor 만 갱신이 안 됩니다. 페이지를 나갔다 들어오면 그제야 적용돼 있고요.

원인은 의외로 iOS UIKit 의 동작이었습니다. RN Modal (다나 확인 창) 이 닫힐 때, iOS 가 친절하게도 이전 first responder 를 description editor 로 복원시켜 줍니다. 우리 코드는 onConfirm 시점에 즉시 blur() + editable=false 를 시도하지만, 이게 모달 닫힘 애니메이션과 동시에 일어나서 first responder 복원에 덮입니다. Editor 는 focus 가 살아있는 상태로 streaming 을 받고, native 쪽 “편집 중엔 prop 갱신 무시” 가드에 막힙니다.

다행히 우리 BottomSheetModal 은 이미 onClosed 콜백을 갖고 있었습니다. 코멘트엔 “iOS first responder 가 깨끗이 풀린 시점” 이라고 적혀 있었어요. 모달이 완전히 unmount 된 다음 generation 을 시작하도록 옮기니, race 가 사라졌습니다.

Desktop 도 같은 경험으로

Mobile 만 진행 표시가 되고 desktop 은 await 한 번에 끝까지 기다리는 건 어색합니다. Desktop 에도 동일한 SSE handler 를 붙였고, AI 버튼을 editor 의 toolbar 우측 끝으로 옮겨 — bold/italic 옆에서 — spinner 가 같은 자리에서 도는 모양으로 다듬었습니다. Generation 중에는 toolbar 의 다른 포맷 버튼들도 회색이 됩니다. Editor 자체가 비활성 상태이니 클릭이 작동하지 않는데, 시각적으로도 비활성으로 보이는 게 일관됩니다.

회고

기능 자체보다 흥미로웠던 건 프롬프트였습니다. “더 좋은 결과를 위한 더 강한 지시” 가 통하지 않을 때가 있더군요. “3~5 단원, X·Y·Z 구성으로” 라는 지시는 보고서 작성엔 도움이 되지만, “멋진 아침이에요!” 엔 폭력적이었습니다. 분류를 두고 톤을 맞추라고 풀어주니, AI 가 알아서 적당히 줄였습니다.

Generic 하게 채우려는 충동도 명시적으로 막아야 했습니다. 모델은 빈 자리를 견디지 못합니다. “입력에 없는 건 만들지 마세요” 를 두 번 세 번 적어두니 그제야 짧게 끝냅니다.

처음엔 description 빈 칸을 AI 가 채워주는 단순한 기능으로 시작했지만, 결과적으로 “AI 가 무엇을 채우지 말아야 하는가” 를 다듬는 작업이 더 컸습니다. 비울 줄 아는 게 더 어려운 모양입니다.