[이것도 AI로 될까? — 차 관리 편] 1부 — 사진 한 장으로 정비 기록하기

[이것도 AI로 될까? — 차 관리 편] 1부 — 사진 한 장으로 정비 기록하기
이번 편의 요청의 요지
정비명세서 사진 한 장에서 날짜·주행거리·항목·총액을 뽑아 정비 기록으로 바꿔 줘.
아이폰 단축어의 텍스트 추출·확인·저장 흐름을 보여 주는 편집본

단축어의 실제 흐름을 보여 주기 위해 주소 영역을 제거한 게시용 편집본이다.

사진 한 장이면 끝날 줄 알았다

정비소에서 나와 명세서 사진 한 장을 찍으면 기록이 남고, 나중에 교체할 때가 되면 알려 주는 도구를 만들고 싶었다. 예전 같으면 날짜와 주행거리를 메모장에 옮겨 적고, 다음 교체 시점을 다시 계산했을 것이다.

그래서 AI에게 먼저 이렇게 설명했다. 명세서를 사진으로 찍으면 알아서 기록되게 해 줘. 글자를 읽는 기능은 폰에 이미 있고, 읽은 글자에서 날짜와 숫자를 골라내는 것도 어려운 일 같지 않았다. 딸깍 한 번, 뚝딱 나올 것처럼 보였다.

결론부터 쓰면 딸깍은 없었다. 그런데 AI가 만든 코드가 문제였던 것도 아니다. 코드는 정말 빨리 나왔고, 대체로 맞았다. 시간을 잡아먹은 것은 그 코드가 내 폰과 내 명세서와 내 생활 방식에 맞는지 확인하는 일이었다.

주기를 바꿀 때마다 계산을 다시 했다

먼저 왜 이걸 하려 했는지부터.

엔진오일은 6,000km마다 간다. 에어클리너는 40,000km다. 항목마다 주기가 다르니 다음 교체 시점도 제각각이다. 나는 그걸 아이폰 메모장에 적어 두고 있었다. 정비를 받으면 날짜와 주행거리를 적고, 다음은 몇 km일지 계산해서 함께 적었다.

기아 앱도 정비 주기를 알려준다. 이 글을 쓰면서 다시 열어 보고 알았는데, 주기를 내 기준으로 바꾸는 것도 된다. 협력 정비망에서 정비를 받으면 이력이 자동으로 들어오고, 가장 최근 이력을 기준으로 다음 시점을 다시 계산해 준다. 내가 만들려던 것의 절반은 이미 있었다.

그런데 앱에 넣을 수 있는 항목이 여섯 가지다. 엔진오일과 필터, 에어클리너, 에어컨 필터, 브레이크오일, 냉각수. 내가 챙기는 것은 열네 가지다. 미션오일도 점화플러그도 브레이크 패드도 칸이 없고, LPG 차인데 LPG 연료필터가 없다.

LPG 연료필터는 사정이 한 겹 더 있다. 이력이 자동으로 들어오는 조건은 제조사 협력 정비망에서 정비를 받는 것인데, 그곳에서는 LPG 계통을 잘 다루지 않는다. 직영 사업소는 예약이 밀려 시간이 오래 걸린다. 그래서 LPG를 다루는 정비소나 택시 정비를 하는 곳을 찾아가게 된다. 앱에 칸이 없는 데다, 있었어도 자동으로 채워질 수 없는 자리에 있는 항목이다.

손으로 넣을 때 적을 수 있는 것도 교체일자와 그때의 누적거리, 둘뿐이다. 정비소도 금액도 들어가지 않는다. 그래서 메모장이 남아 있었다. 앱이 못 해서가 아니라, 앱이 다루는 여섯 가지 밖에 내 차의 나머지가 있어서였다.

문제는 주기를 바꿀 때 드러났다. 한 항목의 기준을 바꾸면 그 뒤의 계산을 전부 다시 해야 한다. 6,000km를 5,000km로 낮추면 다음 시점도, 그다음 시점도 달라진다. 항목이 열 개가 넘으니 한 번 손대면 한참 걸린다.

게다가 메모장은 내가 적은 것만 알고 있다. 정비소에서 두 가지를 한꺼번에 갈면 두 줄을 적어야 하고, 한 줄을 빠뜨리면 그 항목만 조용히 계산에서 빠진다. 빠진 것은 눈에 띄지 않는다.

기록이 없어서가 아니었다. 판정을 손으로 하고 있어서 생기는 일이었다. 계산을 넘기려면 먼저 기록이 숫자로 들어가 있어야 한다. 메모장에 적은 문장은 사람만 읽을 수 있다.

첫 판은 정말 금방 나왔다

명세서 사진 두 장을 놓고 시작했다. 터미널에서 Claude Code를 띄우고 무엇을 만들 건지 설명했다.

읽어낸 글자에서 날짜를 찾고, 주행거리를 찾고, 정비소 이름을 찾고, 금액과 항목을 뽑아내는 코드. 이건 한 시간이 안 걸렸다. 정규식 몇 개와 후보 중에서 고르는 규칙 몇 줄이다. 실물 두 장을 넣어 보니 전부 정답으로 나왔다. 날짜도, 주행거리도, 다섯 개 항목도.

서버 쪽도 금방 붙었다. 집 NAS에 n8n을 띄워 두고 쓰고 있어서, 웹훅으로 글자를 받아 방금 만든 규칙을 돌리고 결과를 돌려주는 흐름을 하나 만들었다. 데이터베이스는 그 안에서 PostgreSQL로 넣는다.

여기서 끝날 줄 알았다. 폰에서 사진만 넘겨주면 되는 일이었다.

그런데 아이폰에서 돌리자 아무것도 안 됐다.

다섯 번을 고쳤고 매번 같은 곳에서 끝났다

아이폰 단축어로 만들었다. 사진을 고르고, 글자를 읽고, 서버로 보내는 세 단계다.

첫 판은 사진 대신 파일 이름을 읽었다. 고른 동작이 이미지에서 글자를 읽는 것이 아니라 입력에서 텍스트를 꺼내는 것이었다. 이름이 비슷해서 잘못 골랐다.

동작을 바꿨다. 둘째 판은 아무 반응이 없었다.

셋째 판은 실행하자마자 목록으로 돌아갔다. 읽은 결과에서 항목 이름을 정규식으로 찾아 곧장 보내게 했는데, 하나도 못 찾으면 다음 단계로 가기 전에 끝나 버리는 구조였다. 그래서 넷째 판에서는 그 자동 매칭을 걷어내고 사람이 고르게 했다. 그래도 안 됐다.

다섯째 판에서는 사진을 글자 읽는 동작에 명시적으로 연결했다. 맥의 단축어 파일을 열어 연결이 실제로 걸린 것까지 확인했다. 그런데도 결과는 같았다.

다섯 번을 고치는 동안 매번 다른 곳을 손댔다. 매번 같은 곳에서 끝났다. 화면에는 아무 단서도 남지 않았다. 실패했다는 것만 알 수 있었다.

이때쯤 AI에게 물어봐도 소용이 없다는 것을 알았다. 내가 보고할 수 있는 것이 "안 됩니다"뿐이었기 때문이다. 무엇이 어디까지 갔다가 멈췄는지를 말해 줄 수 없으면 상대가 사람이든 아니든 답이 나오지 않는다.

범인은 코드가 아니라 기본값이었다

의심이 엉뚱한 데로 갔다. 이 명세서를 애초에 읽을 수 없는 것 아닐까. 종이는 구겨져 있고 표는 빽빽하다. 글자도 작다.

그래서 문제를 반으로 잘랐다. 단축어를 통째로 걷어내고, 맥에서 인식 엔진만 직접 불러 보기로 했다.

아이폰 단축어의 「이미지에서 텍스트 추출」과 사진 앱의 Live Text는 같은 것을 쓴다. 애플의 Vision 프레임워크다. 그리고 그건 맥에서도 부를 수 있다. Swift로 스무 줄짜리 스크립트를 하나 짰다. 사진 경로를 받아 VNRecognizeTextRequest에 한국어와 영어를 지정해 넘기고, 읽어낸 줄을 그대로 출력하는 것이 전부다.

swift vision-ocr.swift 명세서.HEIC

두 장 다 정확히 읽었다. 사진이 눕게 찍혀 있었는데도 그대로 읽었다. HEIC를 변환할 필요도 없었다.

읽을 수 있다는 것이 확인되자 남은 문제는 하나로 좁혀졌다. 사진이 그 동작까지 가지 못하고 있다.

이 스크립트는 그 뒤로도 계속 썼다. 새 정비소의 명세서를 만나면 아이폰을 꺼내 단축어를 돌려 보는 대신, 맥에서 이걸 먼저 돌려 본다. 읽히는지, 규칙이 그 글자를 소화하는지 몇 초 만에 안다.

맥에서 같은 단축어를 열어 동작마다 무엇이 들어가는지 하나씩 눌러 봤다. 글자를 읽는 동작의 입력 칸에서 멈췄다. 목록이 이렇게 있었다.

  • 클립보드
  • 현재 앱
  • 현재 날짜
  • 기기 세부사항
  • 단축어 입력

기본으로 선택된 것은 클립보드였다.

사진을 넘겨준 적이 없었던 것이다. 글자를 읽는 동작은 처음부터 정상으로 돌고 있었다. 다만 사진이 아니라 클립보드를 읽고 있었다. 칸을 「단축어 입력」으로 바꾸자 곧바로 명세서 글자가 나왔다.

다섯 번의 실패가 설정 하나로 설명됐다. 돌아보면 나는 계속 코드를 의심하고 있었다. 정규식을 고치고 흐름을 바꾸고 동작을 지웠다 다시 넣었다. 정작 봐야 했던 것은 그 동작이 무엇을 입력으로 받고 있는지였다.

같은 OCR인데 기기마다 결과가 달랐다

글자가 읽히기 시작하자 다음 문제가 나왔다.

맥에서 읽은 결과와 아이폰에서 읽은 결과가 달랐다. 같은 회사의 같은 기능이다. 그런데 줄이 나뉘는 자리가 다르고, 항목이 나오는 순서도 다르고, 맥이 통째로 놓친 줄을 아이폰은 읽었다. 오일필터 한 줄이 맥에서는 아예 없었다.

이건 꽤 뜻밖이었다. 나는 개발할 때 맥에서 시험하고 있었다. 맥 기준으로 규칙을 짜면 정작 손에 들고 쓰는 기기에서 어긋난다. 그래서 실기기에서 나온 글자를 그대로 받아 두고, 그것을 기준으로 규칙을 다시 맞췄다.

읽어낸 글자에는 또 다른 성격이 있었다. 품목 이름과 금액이 다른 줄에 떨어져 나오고, 순서도 눈에 보이는 순서가 아니었다. 어떤 항목은 자기 금액보다 뒤에 나왔다. 표를 표로 읽어 주는 것이 아니라 눈에 보이는 덩어리마다 끊어서 내보내기 때문이다.

그래서 항목별 금액은 뽑지 않기로 했다. 짝을 맞출 근거가 없는데 맞추면 틀린 금액이 조용히 들어간다. 실제로 규칙을 잘못 짰을 때 부품번호 가운데 여섯 자리를 떼어 금액으로 읽은 적이 있다. 필요한 것은 항목 목록과 총액이었고, 그 둘은 확실하게 뽑을 수 있었다. 기능을 하나 덜어낸 셈인데, 덜어내고 나니 틀릴 일도 없어졌다.

숫자를 고르는 일도 까다로웠다. 명세서 아래쪽에는 보증 안내가 붙어 있다. "주행거리 20000km 이내" 같은 문장이다. 규칙을 처음 짰을 때 그 숫자를 주행거리로 읽었다. 지금은 조건을 뜻하는 말이 같은 줄에 있으면 그 줄을 통째로 버린다.

금액도 마찬가지다. 어떤 양식은 부품값과 공임을 더한 소계를 먼저 적고 부가세를 뒤에 붙인다. 소계를 총액으로 잡으면 실제로 낸 돈보다 적게 기록된다. 그래서 총계, 합계, 소계 순으로 찾고 먼저 만나는 것을 쓴다.

읽는 것보다 무엇을 읽지 않을지 정하는 데 시간이 더 들었다.

저장이 안 돼서 설계를 바꿨다

읽기가 끝나자 화면에 요약이 떴다. 남은 것은 저장 버튼 하나였다. 여기서 또 막혔다.

처음 설계는 이랬다. 웹훅이 둘이다. 하나는 읽기용, 하나는 저장용. 읽기 쪽이 결과를 돌려주면 단축어가 그걸 필드별로 꺼내서 저장 요청의 본문을 다시 조립해 보낸다. 날짜는 날짜 칸에, 주행거리는 주행거리 칸에.

그런데 단축어에서 그 조립이 되지 않았다. 「URL 콘텐츠 가져오기」의 본문 형식을 고르는 자리에서 막혔다. JSON을 골라 필드를 하나씩 넣어야 하는데 원하는 대로 되지 않았고, 그 상태로 저장을 누르면 본문이 빈 채로 나갔다. n8n 실행 기록을 열어 보면 요청은 도착해 있는데 몸통이 비어 있다. 서버는 "주행거리나 항목 중 하나는 있어야 한다"며 거절했다. 요약은 화면에 잘 떠 있는데 저장만 안 되는 상황이었다.

한동안 그 조립을 되게 만들려고 애썼다. 그러다 방향을 바꿨다. 조립을 없애면 되는 것 아닌가.

읽어낸 결과는 이미 서버가 갖고 있다. 그러니 단축어가 그걸 다시 보낼 이유가 없다. 같은 곳으로 "저장해줘"라는 표시 하나만 더 보내면, 서버가 다시 읽고 저장까지 하면 된다.

고치고 나니 저장 동작은 읽기 동작을 복제해서 필드 하나를 더한 것이 됐다. 단축어에서 조립할 것이 없어졌다. 막혔던 설정을 넘을 필요도 없어졌다.

같은 사진을 두 번 읽으니 낭비 아니냐고 할 수 있다. 한 번 더 읽는 데 1초가 안 걸린다. 그 1초로 단축어에서 틀릴 수 있는 자리를 통째로 없앴다. 나쁘지 않은 거래였다.

화면에 아무것도 안 뜨는 날도 있었다

그다음에는 요약이 화면에 뜨지 않았다.

서버 기록을 보면 응답은 정상이었다. 날짜도 항목도 다 들어 있었다. 그런데 폰에서는 빈 화면이었다.

원인은 응답의 Content-Type이었다. 내용은 정상적인 JSON인데 그것이 JSON이라고 알려주는 헤더가 빠져 있었다. 그래서 단축어는 그걸 그냥 긴 문자열로 받았고, 「사전 값 가져오기」로 날짜나 항목을 꺼내려 하니 아무것도 안 나왔다. n8n의 응답 노드에서 헤더를 한 줄 명시하자 곧바로 값이 잡혔다.

여기서 하나 더 배웠다. 응답 본문을 만들 때 JSON.stringify()로 문자열을 만들어 내보내고 있었는데, 객체를 그대로 넘기는 편이 안전했다. 문자열로 나가면 받는 쪽이 그걸 다시 풀어야 하고, 그 과정에서 방금 같은 일이 생긴다.

비슷한 것이 또 있었다. 확인 화면의 제목 자리에 요약을 넣었는데, 화면에 summary라는 글자가 그대로 떴다. 변수 칩을 넣어야 할 자리에 변수 이름을 타이핑한 것이다. 단축어 편집기에서는 둘이 비슷해 보인다.

셋 다 코드의 문제가 아니었다. 어느 것도 오류로 잡히지 않고, 그냥 조용히 이상하게 동작했다. 화면을 직접 보지 않으면 알 수 없는 종류였다.

읽어낸 뒤에도 사람이 봐야 했다

한 줄이 통째로 사라질 수 있다는 것을 안 이상 저장까지 자동으로 이을 수 없었다.

지금은 이렇게 동작한다.

사진 → 글자 읽기 → 구조로 바꾸기 → 확인 → 저장

읽은 결과를 먼저 화면에 보여 준다.

2026-00-00 · 00,000km
예시 정비소 · 000,000원
엔진오일, 오일필터, 에어클리너
저장 전에 확인하는 값의 예시 화면. 실제 값 대신 예시값을 넣은 편집본

실제 명세서와 화면의 식별정보를 제거한 예시 화면이다. 저장 전에 날짜·거리·정비소·금액·항목을 사람이 확인한다.

화면에 무엇을 띄울지도 정해야 했다. 읽어낸 것을 다 보여 주면 사람은 읽지 않는다. 스무 줄이 뜨면 그냥 넘긴다. 그래서 저장될 값만 세 줄로 줄였다.

못 읽은 칸을 어떻게 할지가 더 어려웠다. 그럴듯한 값으로 채우면 화면은 깔끔해진다. 대신 확인이 무의미해진다. 채워진 값을 보면 사람은 맞다고 생각하고 넘긴다. 그래서 못 읽은 것은 빈 칸으로 두고, 무엇을 못 읽었는지 아래에 적기로 했다. 화면이 지저분해지는 쪽을 골랐다.

특히 날짜를 못 읽었을 때가 위험하다. 지난 명세서를 뒤늦게 넣는 경우가 있는데, 날짜를 못 읽으면 오늘 날짜로 들어간다. 그러면 과거 기록이 최신 기록 행세를 하고, 주행거리 판정이 통째로 어긋난다. 화면에서 날짜 한 줄만 봐도 그건 막을 수 있다.

저장 완료 알림만 남긴 편집본

저장 완료 알림 영역만 남긴 편집본이다. 아래 명세서와 사진 앱의 주소·식별정보는 제거했다.

틀릴 자리를 미리 잡아 두었다

읽기 규칙은 앞으로도 계속 고치게 된다. 새 정비소의 명세서는 양식이 다르고, 다르면 규칙이 어긋난다.

한 장을 고치다 다른 장을 깨뜨리는 일이 실제로 있었다. 그래서 실물 명세서에서 나온 글자를 텍스트 파일로 그대로 저장해 두고, 규칙을 고칠 때마다 전부 돌린다. 기대값은 문서를 눈으로 읽어 적었다. 맥에서 나온 글자와 아이폰에서 나온 글자를 둘 다 넣었다. 한쪽만 넣으면 다른 쪽에서 깨지는 것을 못 잡는다.

node tools/test-parse.js

시험 도구는 따로 쓰지 않았다. Node로 짠 스크립트 하나가 기대값과 결과를 맞춰 보고 틀린 줄만 빨갛게 찍는다. 항목별 금액의 합이 문서의 부품액과 맞는지까지 본다.

여기에 하나를 더 붙였다. 서버에서 도는 코드는 워크플로 안에 문자열로 들어가 있다. 그래서 파일을 고치고 워크플로에 옮겨 넣는 것을 잊으면, 시험은 원본을 돌리고 실제로는 옛 사본이 도는 상황이 된다. 초록불인데 깨져 있는 것이다. 그래서 옮겨 넣는 것도 스크립트로 만들고, 둘이 어긋나면 잡아내는 검사를 파이썬으로 하나 짰다. 지금은 그 검사기가 도커 설정과 nginx 설정, HTML까지 훑는다. 직접 데어 본 것마다 검사를 하나씩 늘렸다.

한 번은 시험을 만들어 놓고 일부러 규칙을 망가뜨려 봤다. 여섯 가지를 시도했는데 그중 하나가 통과했다. 검사하고 있다고 믿었던 부분이 실은 아무것도 보고 있지 않았다. 시험 자료로 넣은 부품번호가 우연히 열 자리여서, 규칙을 풀어도 결과가 같았던 것이다.

시험이 초록불이라는 것과 시험이 무언가를 검사하고 있다는 것은 다르다. 그것을 확인하는 방법은 일부러 깨뜨려 보는 것뿐이다.

AI가 만든 것은 코드보다 기록하는 습관이었다

이제 정비소에서 나오면서 명세서를 한 장 찍는다. 그게 전부다. 날짜와 주행거리와 항목이 숫자로 들어간다. 메모장에 옮겨 적고 다음 교체 시점을 계산하던 일은 사라졌다.

처음 생각한 것과 결과는 비슷하다. 걸린 시간이 달랐을 뿐이다.

코드는 정말 빨리 나왔다. 글자에서 날짜와 금액을 뽑아내는 부분은 한 시간이 안 걸렸고 처음부터 잘 돌았다. 그 뒤에 시간을 잡아먹은 것들을 세어 보면 이렇다. 목록에서 기본으로 선택돼 있던 항목 하나, 맥과 아이폰의 출력 차이, 조립이 막혀서 바꾼 설계, 응답에 빠져 있던 형식 표시 한 줄.

어느 것도 코드를 잘 짜서 해결되는 문제가 아니었다. 전부 화면을 열어 하나씩 눌러 보고, 실제 기기에서 돌려 보고, 되는 것과 안 되는 것을 갈라 봐야 알 수 있는 것들이었다.

문제를 반으로 자른 것이 결국 도움이 됐다. 단축어를 걷어내고 글자 인식만 따로 돌려 보자 "읽을 수 있는가"와 "사진이 거기까지 가는가"가 갈라졌다. 앞엣것은 처음부터 문제가 아니었다. 그걸 확인하기 전까지는 두 문제가 한 덩어리로 보였고, 그래서 엉뚱한 곳을 다섯 번 고쳤다.

바이브 코딩이라는 말이 만드는 오해가 있다면, 말로 설명하면 결과까지 나온다는 부분일 것이다. 실제로 나온 것은 코드까지였다. 그 코드가 내 폰에서, 내 명세서로, 내가 원하는 방식으로 도는 것은 그다음 일이었고 시간은 대부분 거기에 들었다.

그래서 이 일을 하며 가장 많이 한 것은 코드를 쓰는 일이 아니었다. 단축어 동작을 하나씩 눌러 보고, 서버의 실행 기록을 열어 요청 본문이 비었는지 확인하고, 인식 엔진만 따로 돌려 보고, 일부러 규칙을 망가뜨려 시험이 반응하는지 보는 일이었다. 만든 것들을 세어 보면 읽기 규칙 하나에 시험 스크립트 넷과 검사기 하나가 붙어 있다.

기록을 넣는 길도 사진 하나만 두지 않았다. 텔레그램 봇으로 버튼을 눌러 넣는 경로를 따로 만들어 뒀다. 단축어가 다섯 번 실패하는 동안에도 그쪽은 멀쩡히 돌고 있었다. 왜 그런 길을 두 개 두었는지, 그리고 사진을 왜 남의 서버로 보내지 않았는지는 다음 편에서 쓴다.

아직 계산을 넘긴 것도 아니다. 그건 4부에서 다룬다. 다만 계산에 넣을 재료를 손으로 옮겨 적는 일은 없어졌다.

AI가 대신한 것은 정비 판단이 아니었다. 내가 무엇을 기록할지 정하고, AI에게 첫 코드를 시키고, 실제로 틀리는 자리를 확인하면서 매번 하던 귀찮은 일을 한 단계 줄인 것에 가깝다.

다음 2부에서는 더 쉬워 보였던 방법을 왜 일부러 포기했는지 쓴다. 명세서 사진을 AI에게 보내면 더 빨리 끝날 텐데, 그 사진에는 보내고 싶지 않은 정보도 함께 찍혀 있었다.


제작 기록

이 글의 초고 작성과 자료 정리, 그리고 글에 나오는 읽기 규칙의 구현과 시험, 단축어와 서버 연결의 설계에는 Claude Code가 사용됐다. 정비 기록을 넣고 보관하는 구조도 같은 방식으로 만들었다. 시리즈의 제목·본문 윤문과 재구성, 게시용 이미지 편집과 Ghost 드래프트 반영에는 ChatGPT(Codex)가 함께했다. 공개 발행 전 전체 내용과 화면은 사람이 직접 검토하고 수정했다. 본문의 주행거리와 금액, 정비소 이름은 예시로 바꾼 값이다. 삽입 이미지는 실제 화면을 바탕으로 개인정보와 운영값을 제거한 편집·재구성본이다.