지난 개발 일지 #3에서 대표 점수를 갈아엎고 피드·차트·차트 주석 같은 성장 기능을 만들었다. 이번 회차는 그 뒤 몇 주 동안 실제로 쓰면서 걸린 것들을 고친 기록이다. 모바일에서 프로필 사진이 찌그러지는 자잘한 버그부터, 차트를 10년으로 늘리려다 만난 세 겹의 원인, 지난 회차에 만든 드로잉 도구를 다시 갈아엎은 이야기, 그리고 “AI가 정말 더 잘하나”라는 불편한 질문까지. 화려한 새 기능보다는, 이미 있는 것들을 정직하게 만드는 작업이 많았다. 그중에서도 가장 오래 붙잡고 있었던 건 점수다 — 새 점수로 바꾸고 나서 리더보드가 보기 좋아졌는데, 그게 오히려 마음에 걸렸다.
멀쩡히 빌드되는데 죽어 있던 ‘알림’ 탭
시작은 내가 직접 밟은 버그였다. 모바일 하단 탭의 “알림”을 누르면 404, 그러니까 “없는 페이지” 오류가 떴다. 알림 전용 페이지 파일을 만든 적이 없으니 당연한데, 이상한 건 빌드(배포 전 검사)가 아무 불평 없이 통과해 왔다는 점이다.
이유가 재미있다. LDBD에는 /@핸들 형태의 프로필 주소를 처리하는 동적 라우트가 있는데, 전용 페이지가 따로 없는 한 단어짜리 주소는 전부 이 라우트가 받아 간다. /notifications도 “notifications라는 사용자의 프로필”로 해석하고, 그런 사용자는 없으니 404를 돌려준 것이다. 빌드 입장에서는 모든 주소가 어딘가로 연결되긴 하니 문제가 없다. 살아 있는 것처럼 보이는데 죽어 있는 링크다.
고치는 것 자체는 전용 페이지를 만들면 끝이다. 중요한 건 재발 방지였다. 같은 실수를 또 하면 또 빌드가 통과해 버릴 테니, 빌드 단계에 메뉴·탭의 모든 링크가 전용 페이지로 연결되는지 검사하는 가드를 넣었다. 이제 이 부류의 죽은 링크는 배포 전에 빌드가 실패하면서 잡힌다.
고치고 나서 보니 애초에 하단 탭의 알림 자체가 애매했다. 화면 상단 헤더에 이미 종 모양 알림 버튼이 있어서 기능이 중복이었던 것이다. 그래서 하단 탭의 알림은 탐색으로 바꾸고(탐색이 오히려 햄버거 메뉴에 묻혀 있었다), 알림 전체 페이지는 종 버튼을 눌렀을 때 뜨는 창에 “모두 보기” 링크로 연결했다.
차트가 2년까지만 보이던 진짜 이유는 세 겹이었다
자산 페이지의 캔들 차트(하루의 시가·고가·저가·종가를 막대 하나로 그리는 차트)는 과거로 스크롤해도 2년 전까지만 보였다. 처음엔 “설정 어딘가에 2년이라고 적혀 있겠지, 그 숫자만 늘리면 되겠지”라고 생각하고 Claude에게 더 길게 하면 단점이 뭔지 물었다. 조사해 보니 “2년”은 한 군데 설정이 아니라 서로 다른 원인 세 개가 겹친 결과였다.
첫째, 캔들에 필요한 고가·저가 데이터를 과거로 소급해 채우는 백필(backfill) 작업이 2년치만 돼 있었다. 그 이전 구간은 캔들 대신 종가를 이은 선으로 대체되고 있었다. 둘째, 더 깊은 병목이 따로 있었다. 데이터베이스 앞단의 API에 한 번의 요청당 최대 1,000행까지만 돌려주는 기본 제한이 있어서, 차트가 5년치를 요청해도 최신 1,000행(거래일로 약 4년)에서 잘려 나갔다. 셋째, 정작 종가 자체는 2016년부터 약 10년치가 이미 데이터베이스에 들어 있었다. 재료는 있는데 조리 과정 두 군데에서 각각 잘리고 있었던 셈이다.
해법도 원인마다 따로였다. 1,000행 제한은 전역 설정을 올리면 다른 API 전부에 영향을 준다. 그래서 전역 설정은 그대로 두고, 차트 API 안에서만 데이터를 여러 번에 나눠 받아 이어 붙이는 방식으로 우회했다. 백필은 조심할 게 하나 있었다. LDBD의 점수는 종가(정확히는 배당·분할까지 반영해 보정한 종가)로 계산되기 때문에, 과거 가격 데이터를 잘못 건드리면 이미 확정된 점수의 근거를 훼손할 수 있다. 다행히 이번에 필요한 건 이미 있는 행에 고가·저가만 채워 넣는 경로라 점수의 입력은 건드리지 않는다는 걸 확인하고 실행했다. 당시 대상 603개 자산 중 601개 성공(2개는 데이터 출처인 야후 파이낸스에서 못 찾는 종목), 약 147만 행이 채워졌다.
결과적으로 주식·ETF는 2016년부터, 크립토는 확보 가능한 전체 구간의 캔들이 나온다. 기본 보기는 그대로 6개월이고, 길게 보고 싶으면 줌으로 당기면 된다. 증상은 “차트가 2년만 보인다”라는 한 줄이었지만, 원인은 세 겹이었다. 증상을 곧장 고치려 들기 전에 어디서 잘리는지부터 확인한 게 이번엔 통했다.
자잘하지만 아픈 화면 손질들
같은 기간에 작은 것들도 여럿 고쳤다. 자산 페이지에서 진행 중인 예측들의 근거는 원래 한 줄로 죽 나열됐는데, 넓은 화면에서는 상승 근거와 하락 근거를 좌우 두 열로 나눠 한눈에 비교되게 바꿨다(모바일에서는 자동으로 세로로 쌓인다). 근거가 적힌 카드를 위로 올려서 “양쪽 논리 비교”라는 느낌이 살아나게 했다. 서버 쪽은 하나도 안 건드린 순수 화면 작업이다.
모바일에서 프로필 사진이 타원으로 찌그러져 보이는 버그도 잡았다. 나는 “옆에 버튼이 너무 많아서 자리가 부족한 것 아니냐”고 짐작했는데, 진단해 보니 정확히 그거였다. 한 줄에 놓인 버튼들이 좁은 화면에서도 공간을 전혀 양보하지 않도록 만들어져 있어서, 그 줄에서 유일하게 줄어들 수 있는 요소였던 프로필 사진이 가로로 눌렸고, 눌린 영역에 맞춰 이미지를 잘라내는 처리가 타원을 만들어냈다. 이미지 문제처럼 보이는 “잘림”의 범인은 대개 이미지가 아니라 배치 규칙이라는 걸 배웠다. 사진에는 “줄어들지 마라”, 버튼 줄에는 “좁으면 줄바꿈해라”를 지정하는 것으로 끝났다.
탐색에서 자산 페이지로 들어온 사람이 바로 예측으로 넘어갈 버튼이 없다는 것도 그제야 보였다. 팔로우와 공유 버튼은 있는데 정작 이 서비스의 본업인 “예측하기”가 없었던 것이다. 자산 페이지 상단에 예측하기 버튼을 넣고, 누르면 예측 페이지에 해당 자산이 미리 선택된 채 열리게 했다. 첫 버전은 옆의 팔로우·공유 버튼보다 눈에 띄게 커서 오히려 어색했고, 한 라운드 다듬어서 크기는 통일하되 색으로만 구분되게 정리했다 — 예측하기만 초록색, 나머지는 흰색 계열, 관심 자산으로 등록된 상태는 금색 별.
지지선과 저항선은 사실 같은 선이었다
이번 회차에서 가장 재미있었던 작업은 개발 일지 #3에서 만든 차트 드로잉 도구를 다시 갈아엎은 일이다. 당시엔 지지선·저항선·추세선·박스를 각각 별도 도구로 만들었다. 쓰다 보니 의문이 들었다. 이렇게 종류별로 나누는 게 차트 도구의 표준일까?
TradingView를 비롯한 주요 차트 플랫폼들을 조사해 보니 답이 명확했다. 확인해 본 곳 어디에도 “지지선 도구”나 “저항선 도구”는 없다. 전부 수평선, 추세선, 박스, 피보나치 같은 기하학적 도구만 있고, 그 선이 지지선이냐 저항선이냐는 현재 가격이 선의 위에 있느냐 아래에 있느냐로 해석한다. 차트를 아는 사람들에겐 “저항선이 뚫리면 지지선이 된다”는 격언이 있을 정도로 역할 반전이 기본 패턴인데, 그리는 순간에 라벨을 못 박아 버리는 우리 방식이 오히려 비표준이었던 것이다.
코드를 열어 보니 더 민망했다. 우리의 지지선과 저항선은 완전히 같은 수평선이었고, 색(초록과 빨강)과 라벨 문자열만 달랐다. 같은 것을 두 번 만들어 둔 셈이다.
여기에 AI 관점의 논거가 하나 더 있었다. LDBD에는 차트에 선을 긋고 그걸 예측 근거로 남기는 봇이 있는데, AI에게 그림을 그리게 할 때는 도구 종류가 적고 규칙이 단순할수록 실수가 줄어든다. 적은 수의 기하 도구에 “역할” 속성을 붙이는 구조가 AI가 안정적으로 생성하기에도 좋은 형태였고, 마침 그게 산업 표준과 같은 결론이었다.
그래서 지지선과 저항선을 별도 타입으로 두지 않고 “수평선 + 역할”로 통합했다. 역할은 지지·저항·목표가·손절 같은 목록에서 고르고, 색은 역할에서 자동으로 정해진다. 차트 주석이 유연한 JSON 형식으로 저장돼 있던 덕에(#3에서 이미지 대신 데이터로 저장하기로 했던 결정) 기존 데이터는 손대지 않고 호환됐고, 화면의 버튼은 사용자에게 친숙한 지지선·저항선 그대로 두되 내부적으로만 역할 기반으로 바꿨다. 내친김에 가장 큰 공백이던 피보나치 되돌림(고점과 저점 두 지점을 찍으면 38.2%·50%·61.8% 같은 되돌림 수준을 자동으로 그려 주는, 조정이 어디까지 올지 가늠하는 고전 도구)과 차트 위에 짧은 메모를 남기는 텍스트 도구도 추가했다.
안전장치도 같이 정리했다. 웹 화면에서 그리든, 봇이 API로 넣든, 외부 AI가 연결 통로(MCP)로 넣든 같은 검증기를 통과해야 한다 — 역할과 색은 허용 목록에 있는 값만, 라벨은 길이 제한, 주석 개수도 상한이 있다. 텍스트는 웹페이지에 HTML로 삽입되는 게 아니라 캔버스에 그림으로 그려지기 때문에, 적어도 입력한 글자가 코드로 실행되는 류의 공격은 통하지 않는다. 다만 텍스트 입력이 아직 브라우저 기본 팝업이라 투박한 건 사실이고, 이건 다듬을 거리로 남겨뒀다.
“AI가 정말 더 잘하나?” — 점수를 한 번 더 의심했다
이번 회차에서 가장 깊게 들어간 건 기능이 아니라 토론이었다. #3에서 대표 점수를 연율화 수익률로 바꾸고 나서 리더보드를 보는데, AI 봇들이 상위권으로 올라오고 “무조건 상승”에 거는 단순 규칙 봇들이 내려가 있었다. 만든 사람 입장에선 보기 좋은 그림이다. 그런데 마음에 걸렸다. AI가 꼭 더 잘한다고 보기는 어려운데, 새 지표가 장기 우상향 자산에 계속 거는 쪽에 불리하게 설계된 건 아닐까?
Claude의 첫 대답은 “옛 점수는 쌓이는 합계라 예측 횟수가 많은 봇이 유리했고, 새 점수는 그 왜곡을 걷어낸 것”이었다. 그럴듯했지만 사실이 아니었다. 옛 대표 점수도 평균이라 횟수와는 무관했던 것이다. 지적하니 Claude가 데이터를 다시 뒤졌고, 진짜 원인은 건당 점수를 매기는 공식의 차이로 확인됐다. 옛 공식은 수익률의 크기를 그대로 쓰지 않고 작은 등락일수록 상대적으로 후하게 쳐 주는 구조여서, “자주 조금씩 맞히는” 상승 고정 봇들이 실제보다 부풀려져 있었다. 새 공식에서는 그 보너스가 사라지고, 상승 고정 봇의 점수는 그 자산의 실제 장기 수익률 수준으로 수렴한다. 실제로 나스닥 ETF에 무조건 상승으로 거는 봇의 새 점수는 연 +19% — QQQ의 실제 장기 연평균 수익률과 얼추 맞는 수준이다. 새 점수가 이 봇들을 억울하게 깎은 게 아니라, 옛 점수가 과대평가하고 있었던 것이다. 점수를 의심하는 일을 AI에게 시켰지만, 그 설명까지 그대로 믿을 수는 없었던 셈이다.
여기서 끝났으면 “새 점수가 옳았다”로 마무리됐을 텐데, 데이터를 뒤지는 과정에서 진짜 문제가 따로 드러났다. 랭킹이 신뢰도를 무시하고 있었다. 아래 표의 평활 보정은 표본이 적을수록 점수를 0 쪽으로 끌어당기는 장치인데, 6월 20일 무렵의 수치로 보면 이렇다.
| 봇 | 확정 예측 수 | 기존 평활 보정 점수 | 강화된 평활 보정 점수 | 95% 신뢰구간 |
|---|---|---|---|---|
| Claude 데일리 봇 | 37건 | +99% | +41% | −61% ~ +367% (미검증) |
| 초기 테스트 AI 봇 | 176건 | +36% | +25% | −37% ~ +116% (미검증) |
| QQQ 상승 고정 봇 | 7,451건 | +19% | +19% | +14% ~ +25% (검증) |
| VOO 상승 고정 봇 | 7,359건 | +14% | +14% | +10% ~ +19% (검증) |
신뢰구간은 “진짜 실력이 있을 법한 범위”다. 1위 AI 봇의 +99%는 겨우 37건에서 나온 숫자라 신뢰구간이 −61%에서 +367%까지 벌어진다 — 운인지 실력인지 아직 아무것도 말할 수 없다는 뜻이다. 그 아래 상승 고정 봇의 +19%는 7,451건이 쌓여 좁은 범위로 증명돼 있다. 요컨대 증명 안 된 운이 증명된 실력 위에 앉아 있었다.
고치는 방법으로는 그 평활 보정을 다섯 배 세게 걸었다. 건당 점수 공식은 그대로 두고 보정의 강도만 올린 것이다. 점수가 저장된 값이 아니라 읽는 순간 계산되는 구조라, 데이터를 다시 채점할 필요 없이 상수 하나를 바꾸는 것으로 끝났다. 표본 7,000건짜리 봇들의 점수는 거의 변하지 않고, 37건짜리 +99%는 +41%로 내려온다. 그래도 1위를 지키면 그건 인정하면 된다. 더 근본적인 방법 — 신뢰구간의 하한으로 줄을 세우는 것 — 은 후보로 적어 뒀다.
하루짜리 예측을 연 단위 수익률로 환산하면 한 건 한 건의 출렁임이 극단적으로 커진다는 게 이 문제의 뿌리다. 짧은 연승이 10년의 기록 위에 앉지 못하게 하려면 보정을 세게 하거나 신뢰도를 랭킹 자체에 넣어야 한다. 그리고 원래 질문 — AI가 정말 더 잘하나 — 의 답은, 출렁이는 장에서 양방향을 맞힐 수 있다면 방향이 고정된 봇을 이기는 게 당연하지만, 그걸 통계적으로 말할 만큼의 표본은 아직 안 쌓였다는 것이다. 리더보드가 그 판정을 성급하게 내려 주는 것처럼 보이지 않게 만드는 것까지가 이번 작업이었다.
이번 회차의 교훈
증상 하나 뒤에 원인이 세 겹일 수 있다.“차트가 2년만 보인다”는 백필 범위, 응답 행 수 제한, 그리고 “사실 종가는 10년치가 이미 있었다”는 세 사실이 겹친 결과였다. 증상 한 줄을 곧장 고치기 전에, 어디서 잘리는지부터 따라가 봐야 한다.
분류를 쪼개는 게 직관적으로 보여도, 종종 통합이 정답이다. 지지선과 저항선을 별도 도구로 만든 건 기하학적으로 같은 것을 두 번 만든 일이었다. 업계 표준도, AI가 다루기 좋은 구조도 같은 결론을 가리켰다.
빌드를 통과했다고 살아 있는 게 아니다. 죽은 알림 탭은 배포 검사를 멀쩡히 통과했고, 내가 우연히 직접 눌러 보지 않았다면 훨씬 오래 죽어 있었을 것이다. 사람이 매번 눌러 보는 데는 한계가 있으니, 이런 부류는 검사를 빌드에 심는 게 답이었다.
그리고 점수를 바꿨으면, 바꾼 점수를 다시 의심하는 것까지가 한 세트다. “AI가 올라왔네”에서 멈췄다면 표본 37건짜리 행운이 7,451건짜리 기록 위에 앉아 있는 걸 계속 놔뒀을 것이다. 이번에도 결론은 지난 회차와 같은 자리로 돌아온다. 무엇을 재는지, 그리고 그 숫자를 얼마나 믿을 수 있는지부터 바로잡아야 그 위의 모든 것이 의미를 가진다.