resumate 15. 이력서 저장소 통합과 읽기 경로 회귀
같은 이력서가 화면마다 다르게 보였다. 이력서 하나를 표현하는 저장소가 셋으로 갈려 있었기 때문이다.
이력서가 세 벌로 흩어져 있었다
resumeText(Nyx 소유 원본), 캐릭터별 attachments, resumeVersions(첨삭본·지원본 저장소) — 저장소가 세 개였고, 그 위에 캐릭터별 선택 넷과 화면에 드러나지 않는 폴백(명시적으로 고른 게 없을 때 대신 쓰이는 이력서)이 얹혀 있었다. 보관함에서 파일을 연결하면 헤더에는 새 파일명이 참고자료 칩으로 붙는데, 이력서 필드와 분석 패널은 여전히 예전 파일을 말했다. 한 화면에 파일명 두 개가 뜨고 둘 다 맞는 말이었다.
8월 28일 첫 작업은 설계를 문서로 적는 것이었다. resumes 목록 한 벌과 currentResumeId 하나로 합치고, 분석을 이력서에 귀속시켜 "분석을 언제 비워야 하는가"라는 무효화 문제 자체를 없앤다는 방향이었다. 이 시점엔 아직 구현하지 않았다고 문서에 명시해뒀다.
flowchart LR
subgraph before["통합 전"]
A["resumeText — Nyx 원본"]
B["attachments — 캐릭터별 첨부"]
C["resumeVersions — 첨삭본·지원본"]
S["캐릭터별 선택 4개 + 숨은 폴백"]
A --- S
B --- S
C --- S
end
subgraph after["통합 후"]
R["resumes 목록"]
I["currentResumeId"]
AN["analysisByResume"]
R --- I
R --- AN
end
before --> after
순수 함수로 먼저 만들고 나중에 연결했다
마이그레이션 로직을 타입과 순수 함수로만 준비했다. 아직 어디에도 연결하지 않아 앱 동작은 그대로였다. 한 번뿐인 변환이라 틀리면 사용자 자료가 사라지는 종류의 코드였기 때문에, 연결 전에 26개 테스트로 옛 저장본의 다양한 모양을 먼저 고정했다.
지켜야 했던 불변식들:
- 이미 옮긴 컨텍스트는 다시 옮기지 않는다. 옛 필드를 동결(읽지도 쓰지도 않고)해서 남기는 방식이라, 재실행 가드가 없으면 실행마다 그 동결된 값에서 목록을 다시 만든다. 그러면 마이그레이션 이후에 추가한 이력서가 조용히 사라진다.
- 같은 파일이 두 경로(업로드=master, 첨부)로 들어오면 합친다. 안 그러면 새 목록 첫 화면에서 같은 이름이 두 줄로 뜬다.
purpose가 없는 옛 첨부는 이력서로 본다. 참고자료로 잘못 분류하면 본문이attached_files로 새어 나간다.- 첨삭본이 들고 있던 분석 스냅샷이 그 이력서의 분석이 된다.
실제로 연결했다
그다음 마이그레이션을 실제 배선에 연결했다. 분석을 이력서에 붙이는 analysisByResume 구조 덕에 setResumeAnalysis(null) 호출부 아홉 곳이 통째로 사라졌다. 비울 일이 없으니 빠뜨릴 곳도 없어졌다.
옛 "이력서 없이 사용"(source: "none") 상태도 존중해야 했다. 아무도 고르지 않았고 master도 없으면 currentResumeId는 null로 남아야 한다. 목록 첫 항목을 자동으로 집으면 사용자가 일부러 빼두었던 이력서가 프롬프트로 도로 들어간다.
이때 사라진 폴백만 검증하던 테스트 둘(getActiveResume.test.ts, agentStore.test.ts)을 지웠다. 대체 불변식은 resumeLibrary.test.ts가 대신 지킨다.
쓰기만 옮기고 읽기를 놓쳐 회귀가 났다
v0.2.9로 릴리스한 직후, 분석해도 결과가 안 뜨는 회귀가 나갔다. 쓰는 곳은 새 목록으로 옮겼는데 읽는 곳 여러 군데가 여전히 옛 필드(sharedContext.resumeAnalysis)를 보고 있었다. 옛 필드가 타입에서 삭제된 게 아니라 그냥 안 쓰이게 됐을 뿐이라, 타입 체크가 이 실수를 잡아주지 않았다. 필드가 존재하니 컴파일은 통과하고, 화면은 조용히 빈다.
찾아 고친 곳:
- NyxView의 분석 패널 전체가
sharedContext.resumeAnalysis를 읽고 있었다 →getCurrentAnalysis로 바꿨다. 버전 스냅샷을 되살리던 effect는 아예 지웠다. 분석이 이력서에 딸려 있으니 고르는 순간 알아서 따라온다. - HomeView의
hasResume이 늘 false였다. 다음 단계 안내가 항상 "이력서부터"로만 나왔다. - ResumeBuilder의 "이력서 불러오기" 목록이 비어 있었다.
resetCharacter가 동결된 필드를 지우고 있어서 Nyx 초기화가 실질적으로 아무 일도 하지 않았다 →currentResumeId와 그 이력서의 진단만 비우도록(목록은 남기도록) 고쳤다.- 보관함 카운터와 "이 이력서로 쓰기" 문구도 같은 병이었다.
이 회귀는 이후 CLAUDE.md에 규칙으로 남았다. 동결된 필드(resumeText·resumeFileName·resumeAnalysis·resumeSelections·resumeVersions)를 화면에서 읽지 말 것.
이력서를 바꾸면 예전 결과가 남는다
이력서 자체는 목록 하나로 정리됐지만, 그 이력서로 만든 결과물은 별개 문제였다. 이력서를 바꿔도 Paws의 맞춤 이력서 초안과 적합도가 그대로 남아, 다른 문서 기준의 88%가 지금 이력서의 점수인 것처럼 보이는 상황이 실제로 있었다. 초안을 비우는 코드가 nyx 캐릭터 하나에만 있었던 것이 직접 원인이었다.
이걸 구조적으로 고쳤다. 이력서를 바꾸는 입구가 선택기·보관함·Nyx의 닫기 버튼 세 곳이라 각자 묻게 하면 어긋난다. requestResumeSwitch가 한 곳에서 받아 pendingResumeSwitch에 담고, 앱 루트의 다이얼로그 하나가 처리한다.
무엇을 지우고 무엇을 남기는지가 이 기능의 핵심 계약이다.
| 지운다 | 남긴다 |
|---|---|
미저장 초안(revisedResume 셋)·자기소개서 | 이력서 목록 — 저장된 지원본·첨삭본 |
| 공고의 적합도·요건별 근거 | 면접 답변 — 사용자가 직접 쓴 글이다 |
| 공고 자체(회사·직무·요건) — 이력서와 무관하다 |
낡을 것이 없으면 묻지 않는다. 이력서를 견주어 보려고 오가는 것은 자연스러운 사용인데, 매번 확인창이 뜨면 그 길이 막힌다. "보관하고 바꾸기"를 고르면 지우기 전에 PDF로 먼저 내보내고, 하나라도 실패하면 바꾸지 않는다. 잃는 쪽의 손실이 더 크기 때문이다.
flowchart TD
A["이력서 교체 요청"] --> B{"낡을 파생 결과가 있나"}
B -->|없음| C["묻지 않고 바로 교체"]
B -->|있음| D["교체 다이얼로그"]
D -->|정리하고 바꾸기| E["파생 결과 정리 후 교체"]
D -->|보관하고 바꾸기| F["PDF 내보내기"]
F -->|전부 성공| E
F -->|하나라도 실패| G["교체하지 않음"]
같은 이력서가 세 줄로 쌓였다
정리가 다 끝난 줄 알았는데, 실사용 중 보관함을 다 비웠는데도 선택기에는 resume.pdf가 세 줄 남아 있는 게 발견됐다. 원인이 두 가지 겹쳐 있었다.
addResume에 본문 합치기 로직이 없었다. 마이그레이션 함수(migrateResumeLibrary)에만 있어서, 올리는 경로마다(Nyx 업로드·선택기 첨부·파일트리 드롭·보관함 지정) 새 id로 항목이 쌓였다. 두 경로가 서로 다른 목록을 만들면 안 된다는 원칙 아래, 합치는 규칙을 마이그레이션과 똑같이 맞췄다.- 보관함에서 파일을 삭제해도 이력서 목록이 그 사실을 몰랐다. 파일 실체는 사이드카(프런트와 별도로 도는 로컬 프로세스로, 파일·PDF 작업을 맡는다)가 들고 있고 목록은 프런트가 들고 있어서 서로 통신하지 않았다. 그 파일을 가리키던 항목을 삭제와 함께 지우도록 고쳤다. 안 그러면 원본이 없는 이력서를 계속 고를 수 있다. 생성물(첨삭본·지원본)은 보관함 파일이 아니므로 이 규칙과 무관하다.
이 김에 선택기 문구 "네 담당이"도 "네 에이전트가"로 다듬었다.
"기본 이력서"라는 말이 세 가지 뜻이었다
네 에이전트가 폴백으로 쓰는 이력서(resumeText)를 한동안 "기본 이력서"라고 불렀는데, 실제로는 이 말이 최소 세 곳에서 서로 다른 뜻으로 쓰이고 있었다. 이 이력서 자체, 저장된 버전의 basedOn(옛 값이 문자 그대로 "기본"이었다), 그리고 아무것도 고르지 않은 상태({ source: "master" }).
선택기 메뉴에서 "기본 이력서 없음" 항목 바로 아래에 "기본"이라는 이름의 다른 항목이 붙어 나와, 같은 것을 가리키는 말처럼 보였다. Nyx의 버튼이 "내 이력서로 확정"인데 그게 만드는 결과물의 이름은 "기본 이력서"인 것도 서로 어긋났다.
이걸 정리했다:
- 다른 화면에서는
${characterName("nyx")} 이력서, Nyx 화면 안에서는 그냥 "이력서"로 적는다. 자기 것을 자기 이름으로 부를 필요는 없다. 캐릭터 이름은 사용자가 바꿀 수 있으므로 하드코딩하지 않고characterName("nyx")를 쓴다. - 선택기 첫 줄은
getActiveResume에 직접 물어 실제로 쓰이게 될 이력서 이름을 그대로 적는다. 폴백 여부를 따로 판정하던 것을 없앴다. 판정을 두 벌로 두면 필드와 체크가 어긋난다. 실제로 어긋나 있었다. - 버전 라벨의 접두어를 뗐다. 저장 이름이 이미 "첨삭본· 2026. 8. 28."인데 접두어를 또 붙이면 "첨삭본· 첨삭본· 2026. 8. 28."이 됐다. 대신
basedOn을 보조 줄로 쓰되, 뜻 없는 값(옛 레코드의 "기본", 회사명 없이 저장된 "채용공고")은 걸러낸다.
같은 이유로 캐릭터 초기화도 다시 짚었다. Nyx를 초기화해도 Paws의 지원본·적합도와 Echo의 질문은 남는다. 사라지는 건 그것들이 딛고 있던 근거뿐이다. 그런데 확인 창이 "근거를 잃습니다"까지만 적으면 사용자는 그걸 삭제로 읽었다. 문구를 "지워지지 않지만 근거를 잃습니다 — 다시 만들어야 정확해집니다"로 바꿨다. 경고 문구와 실제 동작이 어긋나 보인다고 코드를 삭제 쪽으로 바꾸지 않도록, 주석에 "사용자가 명시적으로 만들어 저장한 문서라 지우지 않는다"는 이유를 남겼다.
눌러도 프롬프트가 안 바뀌는 토글이 있었다
이날 자료 연결 배관 전체를 정리했다. 이전에는 화면(ContextBar의 relevantSources)과 프롬프트를 만드는 곳(buildContext)이 각자 규칙을 갖고 있어서 서로를 보지 않았다. 그 결과:
- 눌러도 프롬프트가 안 바뀌는 토글이 셋 있었다. Nyx의 채용공고, Paws의 프로젝트, Echo의 면접 기록.
- 버튼이 없어서 영원히 실리지 않는 자료가 둘 있었다. Paws·Echo의 이력서 분석이다.
ContextSource의"resume"항목 자체가relevantSources어디에도 정의돼 있지 않았다.
AGENT_SOURCES라는 표 하나가 buildContext(프롬프트 생성)와 currentBasis·nextBoost(산출물에 붙는 근거·권유 두 줄)를 동시에 정의하게 바꿨다. contextSources.test.ts가 자료 종류와 프롬프트 키의 짝을 소스 레벨에서 고정한다.
유저가 켜고 끄는 방식도 이때 버렸다. 예전 contextSelections 토글은 initialContextSelections가 전부 빈 배열이라, 앱의 핵심인 자료 공유가 기본값으로 꺼져 있었다. 프로젝트와 공고를 다 채워둔 유저에게도 아무것도 실리지 않았고, 화면의 절반은 켤 수 없는 가짜 스위치였다. 지금은 자료가 있으면 무조건 싣는다. 대가는 알고 골랐다. 요청당 토큰이 늘고 "이번엔 이 공고만 빼고" 같은 선택은 못 한다. 되살릴 거면 토글이 아니라 이력서 선택기처럼 고르는 메뉴로 만들어야 한다는 원칙을 남겼다.
이 밖에 함께 정리된 것:
- 회사 조사는 공고에 딸려 실린다. 홀로 실리면 무엇에 대한 조사인지 알 수 없다. 예전에는 조건 없이 실려서 한 번 조사하면 이후 모든 Paws 대화에 계속 붙었다.
- 없는 자료는 헤더가 아니라 산출물 옆에서 권한다. 헤더에 상태를 나열하던 것(
describeContext)은 그 자리에서 유저가 아무것도 결정할 수 없는 숫자였다. - 면접 기록은 최신 5개만 싣는다. 전부 실으면 회차가 쌓일수록 프롬프트가 무한정 길어진다.
이력서가 바뀌는 모든 경로에서 resumeAnalysis를 비워야 한다는 규칙도 이때 다시 적었다. 분석이 Nyx 패널만 보는 값이던 시절에는 남아도 그 화면만 틀렸는데, 이제 Paws·Echo 프롬프트에도 실리는 값이라 사라진 문서의 점수·약점이 두 에이전트를 오염시킬 수 있다. 놓치기 쉬운 두 곳으로 removeResumeVersionFromContext와 detachFile을 짚었다. 선택이 조용히 master로 되돌아가는 경로라 이력서가 바뀌는 줄 모르고 지나치기 쉽다.
문서화 자체도 한 번 더 정리했다. 헤더 표시줄은 만들었다가 걷어냈는데 CLAUDE.md는 여전히 그 기능을 설명하고 있었고, 실제로 다른 세션이 그 낡은 문장을 읽고 없는 기능을 기록에 적은 일이 있었다. 문서가 코드보다 먼저 낡으면 그 대가는 다음 사람이 치른다. resumeAnalysis를 비우는 호출부 목록도 문서에서 지웠다. 세어서 적으면 그 숫자 자체가 또 낡기 때문에, setResumeAnalysis(null)로 찾으라는 방법만 남기고 놓치기 쉬운 두 곳만 이름으로 적었다.
이력서 문장이 계속 관형절로 눌렸다
이력서 서술 뼈대(RESUME_STORY_SHAPE)가 들어왔다. 세 이력서 프롬프트(Nyx 첨삭·Nova 초안/반영본·Paws 지원본)가 전부 구조·정직성·분량만 제약하고 문장이 어떻게 읽혀야 하는지는 비워뒀는데, 그 빈자리는 학습 데이터의 이력서 기본값으로 채워졌다. 소제목 아래 세 줄이 모두 "~하던 것을 ~했습니다" 한 틀로 나왔다. 문제 인식이 없는 게 아니라 전부 관형절(명사를 꾸미는 절, "~하던 / ~한" 꼴)로 눌려 한 번도 독립 문장으로 서지 못했다.
뼈대는 상황·제약 / 판단·조치 / 결과 세 층이고, 층마다 문장을 따로 세운다.
- 상황·제약을 관형절로 누르지 말고 제 문장으로 세울 것.
- 판단 근거를 종속절에 묻지 말 것 — "~해야 했기 때문에 ~했습니다"로 접으면 이력서에서 가장 값어치 있는 대목이 그냥 지나간다.
- 같은 조치를 배율만 바꿔 여러 줄로 다시 말하지 말 것.
- 결과가 자료에 없으면 비워둘 것 — 숫자를 지어내거나 "대폭 개선"으로 채우지 말 것.
8월 28일 저녁에는 다른 작업 세션이 만들어 두고 종료된 규칙 추가가 남아 있어 이걸 이어받았다. 실제 산출물에서 발견된 구체적 실패를 규칙으로 못 박았다.
- 조치를 되풀이한 문장을 결과라고 쓰지 말 것. "승인 절차를 한 곳으로 모았습니다" 뒤에 "여러 곳에서 승인하지 않게 됐습니다"를 붙이면 조치의 정의를 다시 말한 것에 그친다.
- 문제를 둘 꺼냈으면 조치와 결과도 둘 — 한쪽만 답하면 문단이 중간에 멎는다.
- 끝을 수동태로 맺지 말 것 — 마지막 문장이 가장 오래 남는 자리다.
- 층을 나누는 것과 문장을 짧게 끊는 것은 다르다. 연결어미로 이어도 좋고, 금지하는 것은 상황을 꾸밈말로 누르는 것뿐이다.
이 변경에는 솔직한 단서를 붙여뒀다. 실제 출력은 아직 검증되지 않았고, 테스트는 규칙이 프롬프트에 실리는지만 본다. Nyx 분석이나 Nova 반영을 실제로 돌려보고 문장이 오히려 나빠졌으면 이 변경만 되돌리면 된다는 메모를 남겼다.
뼈대를 넣으면서 지켜야 했던 건 서식 규약(RESUME_MARKDOWN_FORMAT, 제목 단계)과 이 뼈대가 다른 계약이라는 점이었다. 서식 규약은 렌더러와 추출기가 읽는 형식이고, 뼈대는 문장이 읽히는 방식이다. 합쳐두면 첨삭처럼 뼈대를 절반만 걸어야 하는 곳(항목 재배치 금지)에서 형식 규약까지 함께 흔들린다. 그래서 첨삭에는 축약본(RESUME_STORY_SHAPE_REVISION)을, 나머지 전문 작성 프롬프트에는 전체 규칙을 건다.
Nova 반영본에는 별개 문제가 있었다. handleRevise가 캐릭터를 구분하지 않고 REVISION_PROMPT 하나만 모두에게 보냈는데, 그 프롬프트는 "섹션 순서를 바꾸지 말 것", "없는 섹션을 만들지 말 것"이 규칙이라 정리한 프로젝트를 반영할 권한 자체가 없었다. 반영본이라는 이름만 붙어 있고 실제로는 재배치 금지 첨삭본이었다. Nova 전용 프롬프트 표로 갈라 항목 추가·보강·재배치를 허용하고, 화면의 분석 결과(우선순위·어필 포인트)를 프롬프트에 실제로 싣게 했다. Nova의 재배치 기준은 Paws(공고 요건 순)와 달리 어필 강도 순이다. 공고 없이도 성립하는 정렬이라 두 문서가 같은 물건이 되지 않는다.
이 배경을 CLAUDE.md에 문서로 남겼다. 라벨을 캐릭터별로 나눴으면 프롬프트도 나눠야 한다는 것, novaResult가 buildContext를 타지 않아 호출부에서 직접 인라인해야 하는 이유까지 적었다.
화면을 옮겼다 돌아오면 지나간 알림이 되살아났다
원인은 React <Activity>의 동작 방식에 있었다. React 19.2에 들어온 컴포넌트로, 자식을 숨긴 채 상태를 유지하고 다시 보일 때 effect를 재실행한다. App은 화면을 전환할 때 컴포넌트를 언마운트하지 않고 <Activity>로 숨긴다. 숨기면 effect가 정리되어 타이머가 취소되지만 컴포넌트의 상태(토스트 문구)는 그대로 남는다. 그 화면으로 돌아오면 effect가 다시 실행되어 5초 타이머가 처음부터 다시 시작한다. 그래서 몇 분 전에 이미 사라졌어야 할 토스트가 화면을 다시 열 때마다 되살아났다.
고친 방향은 상태와 타이머를 컴포넌트 생명주기에서 완전히 떼어내는 것이었다.
- 토스트 상태·타이머를
toastStore가 앱 전체에 하나로 쥔다. 타이머를 모듈 레벨에 두면 컴포넌트 수명과 끊겨서, 화면을 숨기든 옮기든 실제로 흐른 시간만큼만 살아 있다가 사라진다. - Toast는
App.tsx가 한 번만 그린다. 화면마다 각자 그리면 같은 사건이 두 번 뜬다. useToast는 toast 값 자체를 반환하지 않는다. 반환하면 그 값을 받은 화면이 또 자기 컴포넌트를 그리게 되어 결국 같은 문제로 돌아간다.- MaterialsView가 아직 들고 있던 자체 4초 타이머도 공용 5초로 통일했다.
이 규칙은 CLAUDE.md에도 남았다. 알림 세 곳(ExportControls·PawsView·ResumeBuilderView)이 각자 다른 위치·색·타이머로 토스트를 만들어 쓰던 이력이 있었으니, 재발을 막기 위해 지연은 5초 하나로 고정하고 그 이유를 명시했다.
권유는 결과 아래에 있으면 못 보고 지나쳤다
산출물에는 두 가지 정보가 붙는다. 근거(이 결과가 무엇으로 만들어졌는가)와 권유(무엇을 더하면 좋아지는가)다. 전까지는 둘 다 결과 아래에 있었는데, Paws의 적합도 분석이나 Nova의 정리 결과처럼 화면이 긴 곳에서는 끝까지 스크롤해야 권유가 보였다. 안 보이는 권유는 아무 일도 하지 않는 것과 같다.
근거는 "이 결과가 무엇으로 나왔나"라 읽은 뒤에 보는 게 맞지만, 권유는 "다음에 무엇을 더할까"라 읽기 전에 보여야 실제로 쓸모가 있다. 그래서 ContextFooter 하나였던 컴포넌트를 ContextBoost(위)와 ContextBasis(아래)로 갈랐다. 파일 이름도 AgentContextNotes로 옮겼다. 절반은 결과 위에 그리므로 "Footer"라는 이름이 더 이상 맞지 않았다.
더할 자료가 없으면 권유 줄은 통째로 사라진다. 앞서 describeContext와 함께 걷어낸 헤더 표시줄처럼 상주하는 요소로 만들지 않는다는 원칙을 여기서도 지켰다.
영어 라벨이 대시보드처럼 읽혔다
사용자가 직접 지적한 문제였다. 작은 대문자 영어 눈썹 라벨(제목 위에 얹는 작은 분류 라벨, QUESTION BANK, MOCK INTERVIEW 같은 것)에 넓은 자간을 준 조합이 "AI가 만든 대시보드"처럼 읽힌다고 했다. 게다가 그 라벨들은 대부분 정보가 없었다. QUESTION BANK 바로 아래에는 이미 "연습할 질문을 선택하세요"라는 한국어 제목이 있었고, MOCK INTERVIEW는 정작 참고자료를 올리는 영역에 붙어 있어 내용과 맞지도 않았다.
- 아래에 한국어 제목이 이미 같은 말을 하고 있으면 눈썹 라벨을 삭제한다(Question bank, Echo feedback).
- 남길 것은 한국어로 바꾼다. Resume Upload → "이력서 파일", Explorer → "탐색기"(툴팁·
aria-label포함), Mock Interview → "참고 자료". uppercase와tracking-widest(0.1em)를 화면 전체에서 걷어낸다.uppercase는 한글에 아무 효과가 없어 오해만 부르고, 넓은 자간은 한글에서 특히 어색하다.
57줄에 걸친 변경이었고, 릴리스 노트에는 "화면 곳곳의 영어 라벨을 한국어로 바꾸고 글자 간격을 다듬었습니다"로 나갔다.
Nova의 분석이 이력서를 8,000자에서 잘랐다
배관 정리로 새로 만든 buildContext 경로를 Nova의 분석 함수(NovaView.runAnalysis)가 타지 않고 있었다. 이 함수는 스트리밍 표시와 중단 제어를 직접 쥐고 있어서 streamChat을 곧바로 호출하는데, 그러다 보니 이력서·공고·회사 조사를 프롬프트에 직접 인라인해서 박고 있었다. 그 인라인 코드가 이력서를 8,000자에서 잘랐는데 buildContext의 기준은 20,000자다. 같은 자료가 두 가지 다른 규칙으로 갈려 있었다.
useChat으로 완전히 옮기지는 않았다. 스트리밍 제어 방식이 다르기 때문이다. 대신 컨텍스트 생성 자체만 buildContext와 같은 곳에서 만들어 넘기도록 고치고, 인라인으로 박던 자료는 걷어내고 지시문(공고에 유리한 각도로 분석할 것)만 남겼다. 이런 종류의 어긋남은 다시 인라인으로 되돌려도 아무 테스트도 깨지지 않고 조용히 갈리기 때문에, 소스 코드 자체를 검사하는 테스트(contextSources.test.ts)를 추가로 붙였다.
그 밖의 작은 정리
REVISE_PROMPT의 paws 칸 주석이 "paws가 여기 없다"고 잘못 말하고 있었다. 실제로는Record<ResumeAuthor, string>타입이 세 키를 모두 요구해서 표에는 존재하지만 도달하지 않는 자리였다. 여기에 지원본 프롬프트를 넣으면 이력서를 만드는 곳이 둘이 되므로 넣지 말라는 이유를 남겼다.- 미리보기 보기 버튼을 "이력서 보기" 하나로 합치면서, 무엇으로 만든 문서인지 말하는 자리가 미리보기 창 부제 하나만 남았다. 그런데 Nyx만 그 값이 문자 그대로 "기본"이라 아무 정보도 주지 않았다. Nova는 "프로젝트 12건", Paws는 회사·직무를 적는 것과 대비됐다.
- "누를 수 있는 것은 이력서 선택과 실행 버튼뿐"이라던 주석이 파괴적 동작(초기화 버튼)이 뒤늦게 추가되면서 사실과 어긋났다. 성격이 다른 버튼이 한 줄에 섞이는 것을 막으려고 쓴 문장이었으니, 틀린 채로 두면 규칙이 아니라 장식이 된다는 이유로 고쳤다.
- 헤더 폭이 좁을 때 왼쪽 클러스터가 잘리는 문제(캐릭터 이름을 최대 길이로 늘렸을 때)는 손대지 못했다.
CLAUDE.md에 남은 구멍으로 기록만 해뒀다.
8월 29일에는 릴리스(v0.2.12)만 있었다. 8월 28일 저녁의 정리 작업들을 묶어 내보낸 버전이다. 이 기간 동안 릴리스는 v0.2.6부터 v0.2.12까지 총 7번 나갔다. 이력서 통합처럼 여러 날에 걸친 큰 작업을 조금씩 릴리스로 쪼개 내보낸 결과다.
구조를 합치는 일의 대부분은 읽는 곳을 찾는 작업이었다
이 기간에 고친 것의 절반은 쓰기는 옮겼는데 읽기를 놓친 종류의 회귀였다. 셋에서 하나로 합치는 리팩터는 "쓰는 곳을 옮기면 끝"이라는 착각을 부르기 쉽다. 실제로는 읽는 곳을 전부 찾아 옮기는 게 더 큰 작업이었고, 그걸 다 옮겼는지 확인할 방법이 grep과 눈으로 훑는 것뿐이었다.
마이그레이션 함수 자체를 26개 테스트로 미리 촘촘히 굳혀 놓은 게 그나마 안전판 역할을 했다. 변환 로직은 한 번도 안 깨졌고, 깨진 건 항상 그 결과를 소비하는 화면 쪽이었다.
코드에서 갈라진 이름이 화면에서는 한 단어로 뭉쳤다
"기본 이력서"라는 말 하나가 세 가지 뜻으로 쓰이며 화면 곳곳에서 서로 다른 값을 가리켰다. 코드에서는 각각 다른 변수(resumeText, basedOn, source: "master")였지만 사용자 눈에 닿는 문구는 전부 "기본"이라는 한 단어로 뭉뚱그려져 있었다. 변수 이름이 갈라져 있어도 화면 문구가 하나로 뭉치면 결국 사용자에게는 같은 혼란으로 돌아온다.