
본문 검색 대상에 PDF를 추가하려다 Opus와 충돌한 이야기
요약
VBA 기반 파일 관리 툴에 PDF 본문 검색 기능을 추가하는 과정에서 Claude Opus와 협업하며 겪은 시행착오를 다룹니다. 기술적 정답과 실제 운용 방식 사이의 간극을 극복하고, OCR을 통해 PDF 자체에 문자층을 심는 최적의 해결책을 찾아가는 과정을 기록했습니다.
핵심 포인트
- 기술적 정답과 실제 운용의 정답은 다를 수 있음
- PDF 검색을 위해 PyMuPDF와 Tesseract를 활용한 OCR 구현
- 외부 인덱스 방식 대신 PDF 내부에 문자층을 심는 방식 채택
- AI와의 협업 시 명확한 스타일과 운용 원칙 전달의 중요성
저는 사무원입니다. 프로그래머가 아닙니다.
지난번 기사에서, 10년 이상 키워온 Excel VBA 제작 파일 관리 툴인 「파일 목록」에 본문 검색 기능을 추가하여, ①파일명 → ②본문 → ③AI가 읽고 답하기, 라는 3단계 구조로 만든 이야기를 썼습니다.
그 마지막에 다음으로는 「AI 요약 설명」을 하겠다고 적었습니다. 그런데 그전에 먼저 해결해야 할 숙제가 있었습니다. 본문 검색의 PDF 대응입니다. 텍스트 계열 파일과 새로운 형식의 Office(.docx / .xlsx / .pptx)의 ZIP 전개 읽기까지는 대응이 완료된 상태였습니다. 남아있던 것이 PDF였습니다.
이번에는 이것을 Claude Code(Opus)와 함께 해결하려다가, 도중에 크게 부딪힌 이야기입니다. 기술적인 이야기이기도 하지만, 어느 쪽인가 하면 「AI와의 분업에서 무엇이 어긋나는가」에 대한 기록입니다.
- 본문 검색 대상에 PDF를 추가했습니다. 문자를 가지고 있지 않은 스캔본이나 AI 생성 PDF가 있으므로 OCR이 필요합니다.
- AI(Opus)의 제1안은 「모든 PDF의 본문을 외부 인덱스 파일 1개로 합치는」 방식이었습니다. 기술적으로는 타당하며, 실제로 작동했습니다.
- 하지만 「외부에 대장을 만들어 관리하는」 형태는 저의 운용 방식과 정면으로 배치되기에, 전부 철거했습니다. 정답은 25년 전부터 있었습니다. DocuWorks와 마찬가지로, 문자는 파일 스스로가 가지게 한다. OCR 문자를 PDF 자체에 불가시(invisible) 상태로 심습니다. 검색하는 순간에는 문자층(text layer)을 추출할 뿐입니다. 실측 결과 3개 파일에 0.26초. 외부에는 아무것도 남지 않습니다.
- 교훈: 기술적 정답과 운용의 정답은 별개이다. 운용의 정답은 코드 어디에도 적혀 있지 않으므로, 스타일을 말로 전달할 수밖에 없다.
본문 검색 메커니즘은 단순합니다. 파일을 열어 문자열을 찾는 것뿐입니다. 텍스트 파일은 그대로 읽을 수 있습니다. Office 신규 형식은 실체가 ZIP이므로, Windows 표준인 tar.exe로 본문 XML만 전개하여 읽을 수 있습니다.
PDF는 그렇게 되지 않습니다.
- 내용은 압축된 바이너리이며, 텍스트로 읽어도 문자가 되지 않습니다.
- 문자를 가지고 있는 PDF라도, 외부에서 보이는 것은 폰트의 글리프(glyph) 번호입니다.
- 스캔본이나 AI 생성 PDF는 애초에 문자를 가지고 있지 않습니다 (단순한 이미지).
즉 「문자를 추출하는 공정」과 「이미지에서 문자를 일으키는 공정(OCR)」이 필요합니다. 이 부분은 VBA만으로는 한계가 있어 Python을 사용했습니다. PyMuPDF 내장 Tesseract를 사용하면, Ghostscript나 별도로 설치하는 Tesseract도 필요 없이 OCR까지 한 번에 해결됩니다.
OCR의 정밀도는 정답을 알 수 있는 일본어 문서 8페이지로 실측하여 결정했습니다.
| 엔진 | 재현율 | 속도 |
|---|---|---|
| Tesseract dpi200 | 94.3% | 1.10초/페이지 |
| ... |
dpi300보다 200이 더 빠르고 정밀도도 높다는 결과는 직접 측정해 보지 않으면 알 수 없었습니다.
자, 추출한 문자를 어디에 둘 것인가. 여기서 AI가 만든 것이 「인덱스 방식」이었습니다.
모든 PDF의 본문을 탭 구분자 파일 1개로 합친다. 행의 형식은 「PDF 경로, 페이지, 본문」. 차분 업데이트(incremental update)를 위해 파일 내용에서 지문(크기 + 시작과 끝의 해시)을 추출하여 대장을 겸한다. 검색하는 순간에는 Python을 호출하지 않고 이 파일 1개만 읽기 때문에 빠르다——.
기술적으로는 타당합니다. 실제로 작동했습니다. 16개의 PDF로부터 인덱스가 만들어졌고, 검색도 정확했습니다.
도중에 오류도 발견했습니다. AI가 HTML로부터 만들게 한 리포트 PDF는 문자가 하나씩 좌표 지정되어 배치되어 있기 때문에, 텍스트를 추출하면 문자 하나마다 줄바꿈이 들어가 인덱스가 문자 1개당 1행이 되어 있었습니다. 12페이지에 625행, 1행 평균 5자. 이 상태로는 「北秋田(기타아키타)」로 검색해도 행 안에 「北(기타)」밖에 없으므로 영원히 검색되지 않습니다. 페이지 단위로 본문을 다시 연결하여 해결했습니다 (625행이 19행으로, 1행 평균 5자가 137자가 되었습니다). 덧붙여 OCR이 「令和8年(레이와 8년)」을 「令和⑧年」으로 읽는 원문자(circled number) 오독도 기계 변환으로 되돌리도록 했습니다 (1회 인덱싱에서 617개 발견되었습니다).
수정하여 검색도 잘 되게 되었고, 자 이제 다음으로——라고 하려는 순간, 근본부터 뒤집혔습니다.
Claude Opus가 인덱스 파일에 대해 설명한 순간, 저는 따져 물었습니다.
외부에 대장을 만들어 관리하는 방식은 저의 스타일과 정면으로 배치되었기 때문입니다.
- 파일을 이동하면 대장의 경로가 틀어진다.
- 대장은 눈에 보이지 않는 곳에 있으며, 내용도 쉽게 읽을 수 없다.
- 누가 관리할 것인가. AI만이 볼 수 있는 파일이 늘어만 간다.
아무리 기술적으로 올바르더라도, 관리할 수 없는 것은 사용할 수 없다. 이 거절은 단순한 감각론이 아니라, 저에게는 비교 대상이 있었습니다.
예전에 DocuWorks를 깊이 있게 사용한 적이 있습니다. Fuji Xerox(현 Fuji Film Business Innovation)가 1996년에 출시한 '전자 종이' 소프트웨어입니다. 사실 '파일 목록'의 파일 관리 방식도 DocuWorks Desk를 상당히 참고하고 있습니다.
DocuWorks는 4반세기 전부터 스캔한 이미지 문서에 OCR(광학 문자 인식) 문자를 삽입해 왔습니다. 문자가 문서 자체 안에 들어가는 것입니다. 그래서 파일을 이동하거나 이름을 변경해도 검색은 그대로 작동합니다. 외부에 별도의 대장(Ledger)을 두지 않습니다.
즉, 답은 25년 전 현장에 있었습니다. 문자는 파일 스스로가 갖게 한다.
여기서부터는 모델을 Claude Fable로 전환하여 다시 만들었습니다. 방침 전환 후의 구성은 다음과 같습니다.
① 이미지 PDF에는 문자를 삽입한다
목록에서 B열의 셀을 선택하고 실행하면, 해당 셀부터 아래에 있는 PDF들에 대해 OCR로 읽은 문자를 원래 페이지와 동일한 좌표에 보이지 않게 겹쳐 넣습니다. 외관은 변하지 않으면서 Ctrl+F만 작동하게 됩니다. 이른바 '검색 가능한 PDF(Searchable PDF)'입니다.
실측 결과는 다음과 같았습니다.
- 파일 3개, 42페이지: 18.3초
- 용량: 13.4MB → 13.6MB (거의 불변)
- 업데이트 일시: 원래 값을 복원 (이 부분을 놓치면 목록의 날짜순 정렬이 하루 만에 망가집니다)
원본을 수정하는 처리이므로, 실행 전에 건수를 출력하여 확인 과정을 거칩니다.
② 본문 검색은 검색하는 순간에 문자층을 추출한다
본문 검색 대상 확장자에 .pdf를 추가했습니다. 검색 시작 시, 대상 PDF만을 Python 호출 1회로 묶어서 문자층을 추출합니다 (Python을 하나씩 실행하면 기동 비용 때문에 느려지기 때문입니다). 실측 결과 3개 파일에 0.26초였습니다. 추출한 결과는 읽은 즉시 삭제하여 외부에 아무것도 남기지 않습니다.
결과 목록의 발췌 열에는 '3쪽 ...'과 같이 페이지 번호가 붙으므로, 어느 PDF의 몇 페이지에서 해당 내용이 나왔는지까지 알 수 있습니다.
③ 도구의 보관 장소는 '눈에 보이는 하나의 폴더'
Python 스크립트와 OCR 언어 데이터는 데스크톱의 '도구' 폴더에 모두 모았습니다. 위치 정의는 북(Book)의 설정 시트에 셀 하나로 관리합니다. 이사를 할 때는 '폴더를 옮긴 후 셀을 수정한다'는 두 단계만 거치면 됩니다.
코드에 고정 경로(Hard-coded path)를 쓰지 않습니다. 처음에는 스크립트 위치를 코드에 직접 적어 두었는데, 이 또한 "이사를 할 수 없지 않느냐"라는 지적을 받고 수정했습니다.
이번 사례를 한마디로 요약하면 다음과 같습니다.
AI는 기술적인 정답을 내놓는다. 하지만 운용의 정답은 사용하는 사람의 머릿속에만 있다.
색인 방식은 기술적으로 나쁘지 않은 설계입니다. 검색 엔진의 세계에서는 상식적인 구조이기도 합니다. 하지만 '파일을 손으로 움직이고 눈으로 보며 관리하는' 저의 운용 방식에는 맞지 않습니다. 이 판단 기준은 코드 어디에도 적혀 있지 않으므로 AI는 볼 수 없습니다.
따라서 충돌이 발생했을 때는 저의 스타일을 언어화하여 전달할 수밖에 없습니다. 이번에 쏟아낸 말들은 다음과 같은 느낌입니다.
- 대장을 만들지 마라. 데이터는 파일 스스로가 갖게 하라
- 연속 처리는 선택한 셀부터 아래로, 빈칸이 나올 때까지
- 도구는 눈에 보이는 폴더 하나에, 필요한 것을 전부 그곳에
- 성공은 무언(無言). 끝나면 상태 표시줄(Status bar)도 지워라
한 번 언어화한 스타일은 다음 개발부터 처음부터 적용됩니다. 트러블이 생겨 허비한 시간은 스타일을 사양서(Specification)로 바꾸는 시간이었다고 생각하면 나쁘지 않은 투자였습니다.
이번에 도중에 모델을 Opus에서 Fable로 전환했는데, 성격 차이에 놀랐습니다. 제가 좋아하는 유튜버가 Opus를 '괴팍하다'고 표현했는데, 솔직히 동감합니다. 요청하지 않은 것을 선의라며 덧붙입니다. 대장을 멋대로 설계하고, 보관 장소를 멋대로 결정합니다. 이번 트러블의 근원도 결국 그곳에 있었습니다. Fable로 전환한 이후에는 말한 대로 빠르게 구성하고, 실측으로 확인하며, 묵묵히 끝냅니다. 매끄러웠습니다.
신기해서 Fable 본인에게 물어보니 이런 답변이 돌아왔습니다. 모델의 성격은 기반 학습 이후의 '마무리 공정'에서 거의 결정된다고 합니다. 대량의 답변 사례를 인간이 평가하여 '이런 방식의 답변이 좋다'고 다듬는 공정이 있는데, 거기서 무엇을 좋게 볼 것인지에 대한 조절(Sajungam)이 모델마다 다릅니다. 신중함을 중시하면 주의 사항이 많은 성격이 되고, 간결함을 중시하면 묵묵히 손을 움직이는 성격이 됩니다. 같은 회사의 형제라도 키우는 방침이 다르면 다른 사람이 됩니다. 게다가 만드는 쪽에서도 성격을 완전히 설계할 수는 없어서, 완성되고 나면 예상치 못한 버릇이 붙어 있다고 합니다. 인간의 육아와 비슷해서 완벽하게 재현할 수는 없다는 것입니다.
공정성을 위해 덧붙이자면, Fable이 매끄럽게 작동했던 것은 Opus와 충돌하는 과정에서 저의 방식(流儀)이 언어화된 이후였다는 사정도 있습니다. 그럼에도 실무적인 결론은 변하지 않습니다. 모델의 성격 차이는 방식의 언어화를 통해 흡수한다. 한 번 언어로 만들어 메모로 남겨둔 방식은, 다음에 어떤 모델이 자리를 맡더라도 처음부터 효과를 발휘합니다.
DocuWorks는 주류의 자리를 PDF에 내주었지만, 제품은 지금도 현역이며 설계 사상은 옳았습니다. 그 사상을 승리한 쪽의 형식인 PDF 위에서 자신의 도구로 재현했다——이번에 한 일은 결국 그것뿐입니다.
도구는 사라져도 올바른 설계는 낡지 않습니다. AI와 협업하며 가장 효과를 보는 사람은, 어쩌면 그런 "현장이 예전부터 알고 있었던 답"을 언어화하여 전달할 수 있는 사람일지도 모릅니다.
Opus에게 악의는 없었습니다. 그것은 그것대로 기술적인 정답을 내놓는 성격으로 길러졌을 뿐입니다.
너는, 너다.
다음 회차에는 예고했던 "AI 요약 설명" 이야기로 돌아갈 예정입니다. 참고로 "파일 목록"은 지난번에 쓴 대로 아직 공개하지 않았습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Qiita AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기