[이것도 AI로 될까? — 차 관리 편] 3부 — 사진이 실패해도 기록은 남겨야 했다
이번 편의 요청의 요지
사진을 못 읽는 날에도 버튼 몇 번으로 정비 기록을 끝까지 남길 수 있게 해 줘.
사진이 안 되는 날에도 기록은 남아야 했다
앞선 편에서 명세서를 사진으로 찍어 기록하는 이야기를 썼다. 잘 돌 때는 좋다. 문제는 안 돌 때다.
글자를 못 읽는 경우가 있다. 명세서가 구겨졌거나, 손글씨로 적어 준 경우거나, 애초에 종이를 안 받은 경우다. 오일만 갈고 영수증도 없이 나오는 날이 있다. 그럴 때 기록을 못 하면 그 항목은 다음 계산에서 통째로 빠진다.
그래서 사진과 무관한 입력 경로를 하나 더 뒀다. 텔레그램 봇이다. 폰에서 메시지 앱을 열고 버튼 몇 번 누르면 끝나는 것.
만들면서 알게 된 것은, 이 "버튼 몇 번"이 사진 경로보다 손이 많이 갔다는 점이다.

사용자가 제공한 실사용 캡처. 봇이 먼저 기록 종류를 묻고, 사용자는 버튼으로 다음 단계에 들어간다.

주행거리를 숫자로 받은 뒤 다음 질문으로 넘어가는 흐름이다.

정비 항목은 자유롭게 입력하게 두지 않고 버튼 목록으로 보여 준다. 마지막에는 선택 완료와 직접 입력 경로도 둔다.
AI에게 자유롭게 말하면 알아듣는 봇을 시키는 대신, 이번에는 무엇을 물어볼지와 다음 화면을 미리 정해 달라고 했다. 사용자가 아무 말이나 잘해야 하는 도구보다, 사용자가 틀릴 자리를 줄이는 도구가 더 필요했기 때문이다.
봇을 새로 파야 했던 이유
텔레그램 봇은 이미 하나 쓰고 있었다. 다른 용도로 만들어 둔 것이다. 처음에는 거기에 기능을 얹을 생각이었다.
워크플로를 만들어 올리자 텔레그램 노드 열 개에 기존 자격증명이 저절로 연결돼 있었다. 같은 종류의 자격증명이 하나뿐이면 알아서 골라 주는 편의 기능이다. 편리한데, 여기서는 위험했다.

텔레그램 업데이트를 입력받아 상태를 읽고, 다음 질문과 버튼을 보내고, 확인된 값을 저장하는 흐름을 n8n 워크플로로 나눠 둔 모습이다.
텔레그램은 봇 하나에 웹훅 하나다. 새 워크플로를 그대로 켜면 그 봇의 웹훅 주소가 이쪽으로 바뀐다. 그러면 기존에 쓰던 것은 메시지를 못 받는다. 오류가 나는 것도 아니고 조용히 멈춘다.
켜기 전에 알아차린 것이 다행이었다. 봇을 새로 파고 자격증명을 따로 만들어 붙였다. 그리고 워크플로에 커다랗게 메모를 하나 붙여 뒀다. 이 자격증명을 바꾸지 말 것.
자동으로 채워 주는 값은 대개 편하다. 다만 그게 무엇을 밀어내고 들어온 값인지는 봐야 한다.
무엇을 물어볼지부터 정했다
대화형은 화면이 없다. 순서가 곧 화면이다. 그래서 무엇을 어떤 순서로 물을지부터 정했다.
주행거리 → 항목 → 비용 → 정비소 → 날짜 → 확인
주행거리를 맨 앞에 둔 것은 정비소에서 나오는 순서가 그렇기 때문이다. 계기판은 차에 타면 바로 보인다. 영수증은 주머니에서 꺼내야 한다.
여기에 지름길을 하나 뒀다. 주행거리만 넣고 싶은 날이 있다. 정비를 받은 게 아니라 그냥 지금 몇 km인지 기록해 두고 싶을 때다. 그때는 명령 하나로 주행거리를 묻고 바로 확인 화면으로 건너뛴다. 나중에 이 지름길이 꽤 중요해졌다. 거리 판정이 이 숫자에 매달려 있어서다.
각 단계에는 건너뛰기를 붙였다. 정비소 이름을 모를 수도 있고 금액을 나중에 넣고 싶을 수도 있다. 다만 주행거리의 건너뛰기는 맨 뒤에 뒀다. 없으면 판정에 못 쓰는 값이라 누르기 어렵게 만들었다.
버튼이 조용히 잘렸다
항목을 고르는 화면이 가장 까다로웠다. 정비 항목이 열넷이고, 여기에 페이지 넘김과 완료와 취소가 붙는다.
텔레그램의 인라인 키보드는 워크플로에서 버튼 개수를 표현식으로 바꿀 수 없다. 화면을 만들 때 몇 개짜리인지 고정해 두어야 한다. 그런데 대화 로직은 상황에 따라 다른 개수를 내놓는다. 항목이 열넷이면 한 화면에 다 못 넣으니 페이지를 나눠야 하고, 마지막 페이지는 앞 페이지보다 버튼이 적다.
로직이 내놓은 개수와 화면의 슬롯 수가 어긋나면 넘치는 것은 그냥 사라진다. 오류가 나지 않는다. 사용자에게는 "왜 에어클리너가 목록에 없지" 하는 상황으로 보인다.
그래서 슬롯 수를 고정하고, 로직이 항상 그 개수를 채우게 했다. 빈 자리는 눌러도 아무 일 없는 가운뎃점으로 메운다. 텔레그램이 빈 문자열 버튼을 거부하기 때문에 무언가는 넣어야 했다.
버튼에는 눌렀을 때 서버로 갈 값을 함께 담는데, 여기에도 제한이 있다. 64바이트를 넘으면 텔레그램이 거부한다. 항목 코드를 그대로 쓰면 한글이 섞였을 때 금방 넘긴다. 그래서 짧은 코드를 따로 두고 그걸 실어 보낸다.
대화 중간은 어디에 두나
버튼을 누르면 서버가 깨어나서 다음 화면을 만든다. 그런데 그 서버는 직전에 무슨 대화를 했는지 기억하지 않는다. 요청 하나가 끝나면 잊는다.
그래서 대화 중간 상태를 데이터베이스에 뒀다. 작성 중인 기록을 임시 상태로 저장해 두고, 버튼이 눌릴 때마다 그걸 읽어서 다음 단계를 정하고 다시 저장한다.
여기에 규칙을 두 개 걸었다. 하나는 대화방마다 작성 중인 것은 하나만 있을 수 있다는 것. 여러 개가 동시에 열려 있으면 어느 것에 값을 넣는지 알 수 없다.
다른 하나는 오래된 것은 버린다는 것이다. 대화를 하다 말고 몇 시간 뒤에 돌아와 버튼을 누르면 어떻게 되나. 그때는 이어서 하는 것보다 처음부터 하는 편이 낫다. 자기가 어디까지 입력했는지 이미 잊었을 테니까.
저장 직전에 값이 사라졌다
여기서 가장 오래 붙잡은 문제가 나왔다.
확인 화면에서 저장을 눌렀는데 이런 오류가 났다.
column "undefined" does not exist
저장 쿼리가 값을 못 받고 있었다. 그런데 그 앞 화면까지는 값이 멀쩡히 있었다. 확인 화면에 날짜도 항목도 다 떠 있었으니까.
원인은 노드 사이를 흐르는 데이터에 있었다. 저장 노드는 상태를 저장하는 노드와 화면을 나누는 분기를 거쳐서 온다. 그런데 중간의 데이터베이스 노드가 자기 조회 결과로 데이터를 갈아치운다. 확정 저장 시점에는 그 조회가 아무것도 못 찾아서 0행이었고, 그래서 흐르던 값이 통째로 사라졌다. 빈 값으로 SQL이 조립되니 컬럼 이름 자리에 undefined가 들어갔다.
고치는 방법은 간단했다. 바로 앞 노드에서 값을 받지 말고 이름을 지정해 앞쪽 노드에서 직접 가져오는 것. 다만 이걸 알아내는 데 시간이 걸렸다. 화면에는 값이 보이는데 저장만 안 되니 엉뚱한 곳을 뒤졌다.
그래서 정적 검사에 규칙을 하나 넣었다. 분기 뒤에 있는 노드가 바로 앞 데이터를 쓰고 있고, 그 앞이 값을 갈아치우는 종류라면 경고한다. 처음에는 분기 뒤의 모든 참조를 잡게 만들었는데 멀쩡한 것까지 걸려서 조건을 좁혔다.
검사를 만들고는 일부러 그 형태로 되돌려 봤다. 안 잡히면 검사가 아니라 장식이다.
밑줄 하나로 봇이 멈췄다
메시지를 굵게 쓰려고 HTML 서식을 켜 뒀다. 항목 이름과 정비소 이름을 그 안에 넣어 보냈다.
어느 날 봇이 저장 직전에 아무 반응이 없었다. 서버 기록을 보니 텔레그램이 메시지를 거부하고 있었다.
can't parse entities
정비소 이름에 밑줄이 하나 있었다. HTML 서식을 켜면 그런 문자가 태그의 일부로 해석된다. 짝이 안 맞으니 텔레그램은 메시지 전송 자체를 거부한다. 사용자에게는 저장 직전에 봇이 먹통이 된 것처럼 보인다.
사람이 쓴 문자열은 전부 한 번 걸러서 넣게 고쳤다. 다만 버튼에 들어가는 글자는 거르면 안 된다. 인라인 키보드는 HTML을 해석하지 않아서, 걸러 놓으면 이스케이프한 흔적이 그대로 보인다.
같은 문자열인데 어디에 들어가느냐에 따라 처리가 달라야 했다. 이런 것은 규칙을 안다고 피해지지 않는다. 한 번 겪고 나서야 코드에 자리를 만들어 준다.
버튼 개수를 시험이 세게 했다
지금까지 나온 것들에는 공통점이 있다. 올린 뒤에야 드러난다.
버튼이 잘리는 것, 데이터가 64바이트를 넘는 것, 서식이 깨져 전송이 거부되는 것. 전부 로컬에서 로직만 돌려서는 안 보인다. 그래서 대화 로직을 워크플로 밖으로 빼서 파일 하나에 담고, 그 파일에 시험을 붙였다.
시험이 보는 것은 대화 전이만이 아니다.
- 화면마다 로직이 내놓는 버튼 개수가 슬롯 수와 같은지
- 버튼에 실리는 값이 64바이트를 넘지 않는지
- 빈 버튼이 섞이지 않는지
- 사진 읽기가 실패했을 때 메뉴로 되돌아가는지
마지막 것은 특히 중요했다. 사진 경로가 실패해도 버튼 경로는 살아 있어야 한다는 것이 이 봇의 존재 이유이기 때문이다. 그래서 읽기가 빈 결과를 낼 때와 아예 죽을 때를 둘 다 시험에 넣었다.
워크플로 안에서 로직만 실행해 보는 기능도 있어서 그것도 썼다. 자격증명이 붙은 노드는 가짜 데이터로 대체되고 코드와 분기만 실제로 돈다. 메시지가 나가지도, 데이터가 바뀌지도 않는다. 로컬 시험이 못 잡는 것 — 시간대 계산이나 노드 이름 참조 같은 것 — 이 여기서 드러났다.
묻는 쪽이 정해져 있으면 헤매지 않는다
지금은 이렇게 쓴다. 정비소에서 나와 차에 타면 계기판이 보인다. 메시지 앱을 열고 봇에게 한 마디 보내면 무엇을 기록할지 묻는다. 주행거리를 치고, 항목을 버튼으로 고르고, 금액을 넣고, 확인을 누른다.
사진 경로보다 손이 몇 번 더 간다. 그런데 틀릴 일이 없다. 항목은 목록에서 고르니 표기가 어긋나지 않고, 숫자는 내가 직접 넣으니 잘못 읽힐 일도 없다.
만들기 전에는 대화형이 자유로운 방식이라고 생각했다. 아무 말이나 하면 알아들어 주는 것. 실제로 만들어 보니 정반대였다. 묻는 쪽이 무엇을 물을지 미리 정해 두어야 쓰는 쪽이 헤매지 않는다. 자유롭게 만들수록 사용자는 "뭐라고 써야 하지"에서 멈춘다.
버튼은 그 제약을 눈에 보이게 만든 것뿐이다. 고를 수 있는 것만 보여 주니 고민할 것이 없다. 사진이 잘 읽히는 날에는 빠른 길을 쓰고, 사진이 실패한 날에는 버튼으로 돌아오면 된다.
AI가 만들어 준 것은 대화 상대가 아니라 기록이 끊기지 않는 우회로였다. 지금은 정비소에서 영수증을 못 받았거나 사진을 읽지 못해도, 차에 타기 전에 기록을 끝낼 수 있다. 덕분에 다음 계산에서 조용히 빠지는 항목이 줄었다.
다음 4부에서는 기록을 남기는 것만으로는 부족했던 이유를 다룬다. 기록이 쌓여도 내가 열어 보지 않으면 아무 변화가 없었다. 그래서 이번에는 차가 먼저 알려 주게 만들었다.
실제 사용 화면
사진이 실패해도 기록이 끊기지 않게 만든 흐름은 실제로 이렇게 보인다. 텔레그램이 질문을 던지고, 사용자가 숫자를 입력하거나 목록에서 고르면, n8n이 다음 화면을 이어 붙인다.
제작 기록
이 글의 초고 작성과 자료 정리, 그리고 글에 나오는 대화 로직과 시험의 구현에는 Claude Code가 사용됐다. 시리즈의 제목·본문 윤문과 재구성, 게시용 이미지 편집과 Ghost 드래프트 반영에는 ChatGPT(Codex)가 함께했다. 공개 발행 전 전체 내용과 화면은 사람이 직접 검토하고 수정했다.