본문으로 건너뛰기

resumate 08. 원본 이력서 보호와 작업 상태 복원

·12 min read·8 / 8

창을 한 번 잘못 닫으면 작업이 처음부터였다. 그걸 고치다가 사용자의 원본 이력서를 지키지 못하는 버그를 셋이나 찾았다.

창을 다시 열면 다 사라졌다

Tauri 창을 숨겼다가 다시 열거나 새로고침하면 useState 값이 전부 초기화된다. 대화와 SharedContext는 SQLite에 저장하고 있었지만, 화면의 작업 상태는 아무 데도 안 남아 있었다.

날아가는 건 Echo가 방금 생성한 면접 질문 세트, 채팅 입력창에 쓰다 만 초안, Nova 분석 결과, Paws에 입력한 공고 URL과 크롤링 미리보기다. 질문 12개를 생성하는 데 30초쯤 걸리는데, 창 한 번 잘못 닫으면 처음부터다.

이런 상태를 SQLite까지 보낼 필요는 없다고 봤다. 훅 하나로 처리했다.

const [questions, setQuestions] =
  usePersistentState("echo:questions", () => createFallbackQuestionSet());

useState와 시그니처가 같고 값이 바뀔 때마다 localStorage에 쓴다. 각 화면의 상태 선언을 이걸로 바꿨다.

대화와 SharedContext에도 안전망을 하나 더 깔았다. 원래 500ms 디바운스로 SQLite에 저장하는데, 그 사이에 창이 닫히면 마지막 변경이 유실된다. localStorage에는 디바운스 없이 즉시 쓰고, 복원할 때 그쪽을 먼저 본다.

영원히 진행 중인 알림

상태를 저장했더니 새 문제가 생겼다. "AI가 질문을 생성하고 있어요" 같은 진행 중 알림까지 저장된 것이다.

새로고침으로 작업이 끊기면 알림은 영원히 진행 중으로 남는다. 그 상태에서는 다시 생성 버튼도 안 눌린다.

최초 마운트에서 working 상태를 오류로 정리하게 했다.

"새로고침으로 질문 생성이 중단됐습니다. 연결 자료는 유지되어 다시 생성할 수 있어요."

실패를 숨기지 않기 원칙이다. 끊긴 걸 끊겼다고 말해야 사용자가 다시 시도한다.

파일 트리가 276px를 항상 차지하고 있었다

파일 트리가 276px를 항상 차지해서 에이전트 화면이 좁았다. 접을 수 있게 했다.

접힌 상태에서도 20px 핸들을 남겨뒀다. 완전히 사라지면 다시 펼 방법을 찾아야 하는데, 그건 사용자가 할 일이 아니다. 데스크톱 관례대로 Cmd/Ctrl+B도 붙였다.

FileTree의 최상위 요소를 div에서 aside로 바꿨다. 스크린리더에 탐색 영역이라고 알려주는 게 맞다.

원본 이력서를 못 지킬 뻔했다

오늘 고친 것 중 제일 중요한 버그다.

Nyx가 이력서를 분석하기 전에 원본 파일을 보관함에 복사한다. 추출한 텍스트만 남기면 나중에 원본 PDF를 다시 못 주기 때문이다. 그 코드가 이렇게 생겼었다.

archiveOriginal(fileName, resumeText)
  .catch((e) => console.error("[archive] 이력서 보관 실패:", e));

await이 없다. 보관이 실패해도 분석은 그대로 진행된다. 콘솔에 에러 한 줄 찍히고 끝이다.

사용자 입장에서는 이력서를 올렸고 분석 결과도 나왔다. 그런데 원본은 어디에도 없다. 나중에 그때 그 PDF를 다시 달라고 하면 못 준다. 그리고 실패한 사실 자체를 모른다.

순서를 바꿨다. 원본 바이트를 보관함에 복사하는 걸 await으로 기다리고, 실패하면 에러를 던져 분석을 중단한다. 성공했을 때만 AI 분석을 시작한다. 원본을 못 지키면 애초에 시작하지 않는다.

첨삭본이 기본 이력서를 덮어쓰고 있었다

같은 성격의 문제가 하나 더 있었다. 첨삭 결과를 저장하는 버튼이 "마스터 버전 저장"이었는데, 실제로 이걸 누르면 이렇게 동작했다.

setResumeText(revised.markdown);      // 기본 이력서 본문 교체
setResumeFileName("마스터 이력서.md");  // 파일명 교체
setResumeAnalysis(null);              // 분석 결과 폐기

AI가 고쳐 쓴 내용이 사용자가 올린 이력서를 덮어썼다. 되돌릴 방법도 없다.

기본 이력서는 그대로 두고 새 첨삭본으로만 저장하게 바꿨다. 버튼 이름도 "새 첨삭본 저장"으로 바꿨다. 설계 원칙의 비파괴 편집이 이 얘기였다.

source: "none"을 없앴다

이력서 선택에 "이력서 없이 사용" 옵션이 있었고 그게 기본값이었다. 그래서 Nova나 Paws에 처음 들어가면 이력서가 연결되지 않은 상태로 시작했다.

이 상태에서는 "이력서에 반영" 버튼이 계속 비활성이다. 사용자는 이력서를 등록했는데 버튼이 안 눌리는 이유를 알 수 없다.

"none"을 아예 없앴다. 선택한 대상이 사라졌거나 아무것도 안 골랐으면 getActiveResume이 기본 이력서로 떨어진다. 구버전 DB에 남은 "none" 값도 같은 경로로 자동 복구된다.

Nova의 경험 카드가 어디에도 안 쓰이고 있었다

Nova는 프로젝트 자료를 읽고 경험 카드를 만든다. 상황, 행동, 결과, 근거, 기여도까지 구조화해서.

그런데 "이력서에 반영"을 누르면 결과가 원본 요약 수준이었다. 확인해보니 경험 카드가 컨텍스트에 안 들어가고 있었다. 애써 정리한 데이터가 화면에만 있고 AI에게는 안 넘어갔다.

verified_experience_cards로 넘기게 했다. 프로젝트명, 상황, 행동, 결과, 역량, 근거, 기여도, 확신도까지.

버튼도 실제로 누를 수 있게 고쳤다. 이력서를 따로 안 골랐으면 클릭 시점에 기본 이력서를 연결하고 projects 컨텍스트를 켠다.

회사 조사를 공고 분석에서 떼어냈다

회사 URL을 넣으면 공고를 분석할 때 같이 크롤링만 하고 있었다. 크롤링한 원문이 그대로 프롬프트에 들어가는 구조라, 결과가 회사 홍보 문구를 반복하는 수준이었다.

별도 조사 단계로 뗐다. 사이트에서 확인되는 사실만으로 비전, 인재상, 핵심 가치를 구분해서 정리하고, 지원용 이력서를 쓸 때 그 방향을 같이 넘긴다.

프롬프트에 못을 하나 박았다.

기업 인재상에 억지로 맞추거나 근거 없는 가치관을 추가하지 마라.

이게 없으면 "저는 도전정신을 중요하게 생각합니다" 같은 문장이 이력서에 생긴다. 회사 사이트에 '도전'이 적혀 있다는 이유만으로. 지원자가 실제로 그런 사람인지는 이력서 어디에도 근거가 없다.

사이트에서 비전도 인재상도 못 찾으면 실패로 처리한다. 빈 조사 결과를 성공인 척 넘기지 않는다.

음성 인식 경로가 둘이라 결과가 엇갈렸다

면접 답변 녹음에 경로가 두 개 있었다. 브라우저 SpeechRecognition API로 실시간 전사하는 것과, 녹음 파일을 사이드카의 whisper로 보내는 것.

WebView에서 SpeechRecognition이 불안정했고 두 결과가 서로 엇갈렸다. 실시간 전사가 써놓은 텍스트를 whisper 결과가 덮거나 그 반대가 됐다.

브라우저 쪽을 지우고 whisper 경로 하나로 일원화했다. 50줄이 사라졌다.

이 앱은 남의 이력서를 다룬다

오늘 고친 것 중 셋이 같은 뿌리였다. await 없는 보관 호출, 기본 이력서를 덮어쓰는 저장 버튼, 그리고 조용히 비활성으로 남는 버튼. 전부 사용자의 원본을 지키는 문제다. 취업 준비생이 몇 달 동안 고쳐 쓴 파일이고, 날아가면 대체할 게 없다. AI가 고쳐 쓴 버전이 아무리 좋아도 원본을 덮어쓸 권한은 없다.

.catch(console.error) 한 줄이 제일 무서웠다. 문법적으로는 에러를 처리하고 있다. 실제로는 실패를 삼키고 있었다. 성공하면서 틀리는 코드다.

설계 원칙을 문서로 적어둔 게 여기서 쓸모가 있었다. 비파괴 편집, 실패를 숨기지 않기. 오늘 고친 버그들은 전부 그 두 줄을 어기고 있었고, 문서가 없었으면 그냥 그런 동작으로 넘어갔을 것이다.

아직 실제 사용자 테스트를 못 했고, 스캔 이력서 OCR 경로도 실사용으로는 검증되지 않았다. 배포 패키징도 남았다.