본문으로 건너뛰기

resumate 04. PDF 파싱, 모델 배정, 그리고 Resumate로 개명

·8 min read·4 / 8

이력서 PDF에서 텍스트를 뽑는 엔드포인트를 만들고, 캐릭터마다 다른 Claude 모델을 배정했다. 앱이 뜰 때마다 되기도 하고 안 되기도 하던 사이드카 준비 감지를 폴링으로 바꿨고, 드래그앤드랍을 붙이다 WKWebView 함정에 두 번 막혔다. 그리고 프로젝트 이름을 Resumate로 바꿨다.

이력서를 못 읽으면 Nyx가 할 일이 없다

먼저 손댄 건 PDF 파싱이다. 이력서를 읽지 못하면 이력서 분석가인 Nyx가 아무것도 못 한다.

PDF에서 텍스트를 뽑는 건 생각보다 지저분하다. PDF는 텍스트 파일이 아니라 그리기 명령의 나열이라서, 글자가 어디에 어떤 순서로 찍히는지가 내용과 별개다. 일단은 BT/ET 마커(텍스트 블록의 시작과 끝) 사이를 긁어내는 방식으로 /parse-pdf를 만들었다.

완벽하진 않다. 표가 많거나 이미지로 스캔된 이력서는 그대로 실패한다. 그건 나중 문제로 미뤘다.

캐릭터마다 다른 모델을 쓴다

네 캐릭터가 같은 모델을 쓸 이유가 없다는 걸 깨달았다.

캐릭터모델
novasonnet
nyxsonnet
pawsopus
echosonnet

Paws만 opus로 올렸다. 채용공고를 읽고 이력서를 재구성하는 게 가장 판단이 많이 들어가는 작업이라서다. 나머지는 sonnet으로 충분했다. CHARACTER_MODELS 맵 하나로 관리하게 만들어서, 나중에 모델을 바꿔도 한 군데만 고치면 된다.

같이 고친 것이 하나 있다. claude CLI 프로세스를 죽일 때 SIGTERM만 보내고 끝내면 가끔 안 죽는다. 5초 뒤 SIGKILL을 보내는 타임아웃을 붙였다.

sidecar-ready 이벤트를 놓치고 있었다

이건 좀 오래 잡았다.

Rust 쪽에서 사이드카 바이너리를 띄우고 포트가 열리면 sidecar-ready 이벤트를 프론트엔드로 보낸다. 프론트엔드는 그 이벤트를 기다린다. 구조만 보면 멀쩡하다. 문제는 순서였다.

사이드카가 빨리 뜨면 프론트엔드가 리스너를 걸기 전에 이벤트가 지나가 버린다. 그럼 waitReady()는 아무것도 못 받고 15초 뒤 타임아웃으로 실패한다. 앱이 뜰 때마다 되기도 하고 안 되기도 했던 이유가 이거였다.

이벤트를 버퍼링하는 대신 방향을 바꿨다. 물어보는 쪽으로. get_sidecar_info Tauri 커맨드를 추가하고, React가 300ms 간격으로 폴링해서 값이 오면 준비 완료로 처리한다.

이벤트는 놓칠 수 있는 신호고 폴링은 언제 물어봐도 답이 있는 상태 조회다. 타이밍에 의존하는 코드를 상태 조회로 바꾸면 race condition 자체가 사라진다. 우아함을 좀 포기하고 확실함을 골랐다.

WKWebView에서 드래그앤드랍이 두 번 막혔다

파일 트리에서 에이전트 화면으로 파일을 끌어다 놓는 기능을 붙이다가 두 번 막혔다.

첫 번째는 dataTransfer.getData()가 drop 시점에 빈 문자열을 반환하는 것이었다. Tauri의 WKWebView에서 발생하는 문제라 표준 방식으로는 경로를 넘길 수가 없었다. 그래서 드래그 중인 항목을 Zustand 스토어에 넣어두고 drop 핸들러가 스토어에서 읽게 했다. 드래그앤드랍 API를 신호로만 쓰고 데이터는 옆길로 넘겼다.

두 번째는 드롭존 하이라이트가 미친 듯이 깜빡이는 것이었다. dragleaverelatedTarget이 null로 오는 경우가 있어서, 자식 요소 위를 지나갈 때마다 영역을 벗어났다고 판단한 게 원인이었다. 카운터로 해결했다. dragenter에서 depth를 1 올리고 dragleave에서 1 내린 뒤, depth > 0일 때만 하이라이트를 켠다. 자식으로 들어가면 enter와 leave가 짝을 이루니 depth가 0으로 안 떨어진다. 지금도 이 방식을 쓰고 있다.

아이콘이 생기니까 진짜 앱 같아 보인다

이름을 바꿨다. 리브랜딩이라고 하기엔 거창하고, 그냥 프로젝트 이름과 번들 ID, 문서를 전부 Resumate로 통일했다. macOS/iOS/Android 아이콘 세트도 만들어 넣었다. Dock에 뜨는 아이콘이 기본 Tauri 로고일 때와는 기분이 다르다.

ARCHITECTURE.md와 CLAUDE.md도 이때 처음 썼다. 코드가 1000줄을 넘어가면서 이 프로젝트가 어떻게 생겼는지를 어딘가 적어두지 않으면 나도 헷갈리기 시작했다.

재현이 안 되는 버그가 제일 어렵다

오늘 고친 버그 세 개가 전부 타이밍 문제이거나 브라우저 API가 기대대로 동작하지 않는 경우였다. 로직 자체가 틀린 버그보다 이런 게 훨씬 찾기 어렵다. 재현이 안 되니까.

race condition을 폴링으로 바꾼 건 코드가 예뻐지는 선택은 아니었다. 그래도 앱이 뜰 때마다 되기도 하고 안 되기도 하는 것보다는 낫다.

파일 트리에는 검색과 미리보기도 같이 붙였다. 남은 건 채용공고 URL 스크래핑, 캐릭터별 시스템 프롬프트의 취업 특화, 스캔된 PDF를 읽기 위한 OCR, 그리고 결과물 내보내기다.