hian-chart 전체 검토

여섯 축을 동시에 훑고, 겹치는 발견은 합치고, 무거운 것은 코드로 다시 확인했다.

이 보고서의 항목은 전부 처리됐다 — 2026-09-16

고침 P0 6/6 · P1 17/17 · P2 6/9. 되돌림 1(저장소 쓰기 디바운스). 철회 3 — 아래 「검토가 틀렸던 것」. 게이트: 단위 2,284 통과 · e2e 205 통과 · 빌드 통과.

2026-09-16master @ 91bd1casrc 182파일 · 서버 4,888줄 · 백로그 5,300줄

요약지금 이 시스템은 튼튼한데, 조용한 실패가 여섯 군데 있다

치명적인 것은 전부 화면이 멀쩡해 보이는 종류다. 값이 틀리게 보이거나, 알림이 안 오거나, 없는 일이 기록되는데 어디에도 흔적이 없다. 반대로 구조는 예상보다 훨씬 좋다 — 닫힘 판정·그림 검증·저장 원자성처럼 한 번 잘 잡아 둔 자리는 그 뒤 모든 기능이 그 위에 안전하게 쌓였다.

이 검토를 읽는 법. 아래 P0 여섯은 “오늘 고칠 것”이다 — 전부 사용자가 눈으로 알아챌 수 없는 종류라 미루면 계속 조용히 손해가 쌓인다. P1은 이번 주, P2는 여유 있을 때. 각 항목의 직접확인 표시는 내가 코드로 다시 짚어 사실을 확정한 것이다.

P0오늘 고칠 것

앞의 다섯은 한 시간 안쪽이거나 반나절이다. 여섯 번째는 그보다 크지만, 앞의 셋이 왜 동시에 존재했는지를 설명하는 항목이라 같이 둔다.

1. 없는 강제청산이 계좌에 영구히 기록된다

근거 App.tsx:343-348 — 청산 이펙트 의존성에 barsReady가 없다. 바로 위 리플레이 훅은 같은 위험 때문에 그 가드를 받아 쓴다(App.tsx:287).

왜 문제인가 심볼을 바꿨다 돌아오는 한 렌더 동안 봉 배열은 아직 이전 심볼 것이다. 그 틈에 ETH 저가가 BTC 청산선을 뚫은 것으로 읽히면 잔고가 깎이고 포지션이 사라지고 체결 기록에 청산 한 줄이 영구히 남는다. 되돌리는 길은 판 초기화(전부 삭제)뿐이다.

고치는 방향 이펙트 첫 줄에 if (!barsReady) return;. 그리고 “옛 배열을 잘라도 해롭지 않다”는 주석은 계좌가 생긴 뒤로 거짓이 됐으니 같이 고친다.

크기 S확실직접확인

2. ntfy 발송이 실패하면 once 알림이 영구 소실된다

근거 server/main.ts:254-257 — 저장이 먼저, 발송이 나중. server/ntfy.ts:108-124publish는 실패를 삼키고 재시도가 없다.

왜 문제인가 조건에 닿는 순간 firedAt이 박히고 알림이 꺼진다. 그 5초 사이 ntfy가 5xx면 폰엔 아무것도 안 오는데 알림은 이미 꺼져 있다. 화면은 “울렸음”이라고 말한다.

고치는 방향 publish가 성공 여부를 돌려주고, 실패면 enabled:false와 원장 claim을 되돌린다. 최소 2~3회 재시도.

크기 S확실

3. 물러섬 사다리가 첫 막힘에 곧바로 5분 — 그동안 가격 알림도 안 돈다

근거 server/main.ts:99-108noteExchangeFailure가 실패 한 건마다 사다리를 올리는데, 호출 지점이 시세·봉·ABC 상위봉·다람쥐 넷이고 다람쥐는 스트림 8개 각각에서 부른다(main.ts:486).

왜 문제인가 30→60→120→300초로 점진적으로 쉬려던 설계가 한 tick 안에서 끝 칸까지 간다. 그리고 물러서는 동안 tickPrices까지 통째로 건너뛰므로 가격·추세선 알림이 5분간 한 번도 판정되지 않는다.

고치는 방향 tick 당 사다리를 한 번만 올린다(hitLimitThisTick을 이미 들고 있다). 시세 판정만은 물러섬에서 빼는 것도 검토.

크기 S확실직접확인

4. 거래소 일부만 막히면 백오프도 경고도 없이 조용히 건너뛴다

근거 server/exchanges.ts:140-145fetchTickersByVenue전부 실패했을 때만 던진다. 일부 실패는 로그만 남기고 부분 결과를 돌려준다.

왜 문제인가 바이비트만 한도에 걸리고 바이낸스는 멀쩡하면: 바이비트 심볼 알림은 가격이 없어 판정을 건너뛰고, 물러서지도 않으니 15초마다 계속 두들기고, 화면은 “감시 정상”이라고 말한다. 오늘 만든 stalled 표시가 정확히 이 경우에 안 먹는다.

고치는 방향 실패한 묶음 목록을 결과에 실어 돌려주고, 한도 때문이면 호출부가 백오프·stalled에 반영한다(어느 거래소인지까지).

크기 S확실

5. 리플레이 앞으로 감기가 합성 인터벌에서 배열 한가운데에 구멍을 낸다

근거 useBars.ts:204to = 마지막봉 + 1000 × 인터벌로 요청하는데, 합성 인터벌은 기반 봉 1000개만 돌아와 실제 창이 훨씬 짧다. pagination.ts:77appendPage는 “마지막보다 뒤”만 보고 연속성을 안 본다.

왜 문제인가 8시간봉 리플레이에서 한 장을 더 받는 순간 167일이 통째로 사라진다(3일봉은 더 크다). 캔들은 매끄럽게 이어져 보여서 눈으로는 절대 못 잡는다. 그 위에서 거래 연습을 하면 결과 전체가 거짓말이 된다. 네이티브 인터벌은 정확히 이어지므로 합성에서만 난다.

고치는 방향 appendPage가 새 페이지 첫 봉이 한 인터벌 넘게 떨어져 있으면 붙이지 않고 그 사실을 돌려준다. 조용한 구멍보다 “여기까지”가 낫다.

크기 M확실직접확인

6. 서버의 거의 전부가 테스트가 못 닿는 칸 안에 있다 — 위의 셋이 거기 산다

근거 server/main.ts 814줄 중 53~809줄이 main() 하나. main.test.ts 머리말이 직접 적는다 — “전부 main() 클로저 안에 있고, 그 파일을 import하면 즉시 서버가 뜬다”.

왜 문제인가 위 P0 중 셋이 이 구역 안이다(백오프 99-108, ntfy 순서 254-257, 부분 실패 처리). 우연이 아니다. M18 회고가 이미 같은 말을 적어 뒀다 — “불변식 하나에 좋은 테스트를 만든 것을 ‘테스트를 했다’로 착각했다. 결함 ①②③④⑦이 전부 그 빈 곳에 살았다.” 백로그에 08-30 최초 기록 → 08-31 재기술 → 09-16 또 기록, 17일 무변화. 이 레포에서 가장 오래 열려 있는 단일 항목이다.

고치는 방향 이미 절반은 했다 — 순수 판정을 밖으로 뺀 dueSquirrelIntervals·backoffAfter·isRateLimited가 그것이다. 남은 한 걸음은 백로그가 09-01에 이미 적어 둔 문장이다: “서버가 거래소 주소를 주입받을 수 있어야 한다.” 그러면 tickPrices·tickSignals를 박제로 돌릴 수 있고, 위 셋에 회귀 테스트가 붙는다.

크기 L확실직접확인

P1이번 주에 고칠 것

사용자가 체감하거나, 언젠가 반드시 물게 되는 것들. 대부분 S다.

항목근거크기
재시작하면 ABC 목표선이 전멸main.ts:67-76메모리 전용이라 다음 봉이 닫힐 때까지 실명 — 주봉이면 최대 7일. 화면·stalled 모두 정상이라 완벽하게 조용하다.M
원장 500칸이 주봉보다 짧다ledger.ts:7-9실측 하루 105건 → 4.8일치. 바쁜 주엔 월요일 주봉 키가 금요일에 밀려 나가 재시작 후 중복 푸시. 나이 기반(40일)으로 바꾸면 끝.S
봉이 0개면 스로틀을 안 세워 영구 재조회main.ts:308continue가 스로틀 갱신보다 앞이라, 역사가 짧은 쌍은 15초마다 영원히 조회된다. 알림은 안 울리고 한도만 태운다.S
옛 커서가 인터벌 전환마다 다시 풀린다useReplay.ts:65-69신고했던 “타임프레임 바꾸면 미래 캔들”이 마이그레이션 안 끝난 판에 그대로 남아 있다. 그 자리에서 체결까지 된다.M
월봉 카운트다운이 30일 고정ChartView.tsx:1758같은 레포가 “그렇게 셈하면 31일 달에서 깨진다”고 경고해 둔 계산을 카운트다운만 쓴다. 1월엔 하루 동안 00:00에 박힌다.S
마우스만 움직여도 MTF 지표 전량 재계산useMtfBars.ts:67-72반환 객체에 useMemo가 없어 다섯 개 메모가 프레임마다 깨진다. 구름대·RSI 표를 켠 데스크톱에서 즉시 체감.S
다람쥐 신호를 렌더마다 40~64번 다시 계산useSquirrelStreams.ts:67-114메모가 하나도 없다. 4× 재생이면 초당 25만 회 루프 — 폰에서 재생이 끊기는 주된 자리.S
번들이 압축 없이 나간다deploy/nginx.conf실측 644KB → gzip 203KB · brotli 173KB. LTE 첫 로딩 1.03초 → 0.28초. nginx 세 줄.S
수량 한 글자마다 지표 메모 전체 무효화usePersisted.ts:40read()가 매번 새 객체를 만들어 opt.* 의존 메모가 다 깨진다. 가림 토글·지표 옵션도 같다.M
리플레이 한 걸음마다 저장소 전체 직렬화useLayouts.ts:50-62setItem은 동기 디스크 쓰기다. 4× 재생 초당 4회 — 폰 끊김의 두 번째 원인. 디바운스면 해결.M
🔔과 ⍾의 접근성 이름이 글자까지 같다SquirrelAlertPopover.tsx:94“알림”이라는 낱말이 여섯 곳에서 다른 것을 가리킨다. “알림 걸기 / 알림 목록”으로 갈라 적으면 끝.S
“범위”가 두 곳에서 다른 것을 조종options.ts · SignalAlertSetup.tsx:151지표 옵션은 그리는 문턱, 🔔은 울리는 문턱인데 라벨이 같다. 차트엔 71개가 그려지고 알림은 9개 기준으로 운다.S
리플레이에 나가는 길이 없다ReplayBar.tsx들어가는 길도 시트 속 ▶ 하나였고(“유아이가 안보이는데?”), 끄려면 다시 그 줄을 찾아야 한다. 막대 끝에 ✕ 하나.S
알림 삭제만 한 번에 끝난다AlertList.tsx:105,126그림·판·계좌는 전부 두 번 눌러 확정인데, 되돌릴 수 없는 서버 레코드만 한 번이다. 가장 큰 과녁이 가장 위험한 자리에 있다.S
RSI 표가 세로 알약 뒤에 깔린다RsiTable.tsx:53-61bottom:82의 근거가 가로 알약 시절 값이다. 지금은 40×188이라 폰에서 표 머리 칸이 가린다.S
그림 앵커만 Infinity를 통과시킨다schema.ts:236-238알림 쪽은 Number.isFinite로 막아 두고 이유까지 적었는데, 정작 저장 왕복이 잦은 그림 쪽이 열려 있다. 저장되면 null로 굳어 그림 전체가 버려진다.S
constructor 키가 임베드 저장소를 초기화시킨다schema.ts:527__proto__만 막는다. 레이아웃 저장소는 같은 함정을 이름까지 적어 두고 Object.hasOwn으로 막았는데, 이쪽만 안 막혔다.S

P2나중에, 그러나 잊지 말 것

항목왜 지금은 아닌가 / 언제 물게 되나크기
version을 2로 올릴 길이 없다올리는 순간 몇 년치 그림과 모든 판·계좌가 사라진다. 게다가 양방향이라 롤아웃 중 두 탭이 서로를 지운다. 모양을 바꿔야 하는 날이 오기 전에 탈출로(순서 있는 업캐스터 또는 새 키)를 설계해 둔다.M
지운 알림이 파일에 되살아나는 경합DELETE의 await가 열린 사이 tick 저장이 끼면 메모리엔 없고 파일엔 있는 상태가 된다. 지금 실측은 일치하지만 구조적으로 가능하다.M
tick이 직렬이라 15초가 최악 150초로거래소가 죽지 않고 느려지기만 해도 가격 알림 샘플링이 몇 분이 된다. stalled는 한도에만 반응하므로 화면은 정상이라 말한다.M
findAbc가 피봇마다 배열을 잘라 지그재그를 다시 돈다20,000봉이면 틱마다 슬라이스 1,000개. 지표를 전부 켜고 과거를 당겨 둔 화면은 과거 성능 측정이 한 번도 안 덮은 구간이다.L
죽은 datafeed/bybit.ts·ws.ts가 두 번째 접기 구현으로 살아 있다주석은 “UI가 아는 유일한 출처”라고 말하지만 프로덕션 참조 0건. 규칙을 고칠 때 한 벌만 고쳐도 죽은 쪽 테스트가 초록이라 안 들킨다.S
다람쥐 8스트림만 어댑터를 안 탄다“거래소별 상한은 어댑터 안에서 끝난다”는 계약 밖에 있다. 3일봉 스트림은 지금도 333봉만 본다.M
라이브 시세를 그대로 쓰는 테스트 7개난이도 문제가 아니라 잔여 작업이다 — 픽스처 생성기 일가(fvgSeries·bandSeries·abcSeries·riseFallSeries)와 규칙이 이미 다 서 있는데 남은 7개에 안 발랐을 뿐.M
폰에서 차트 설정에 닿는 길이 두 손가락 탭뿐거래량·그리드·현재가 선·다음 그림 기본값이 폰에서 사실상 존재하지 않는다. 그 손짓을 적은 글자가 앱 어디에도 없다.M
가장 자주 누르는 줄만 40×40이 아니다인터벌 칩·TF 칩이 22~26px. 이 앱에서 제일 많이 눌리는 줄이다.S

축6테스트는 얼마나 믿을 수 있나

결함 목록과 별개로 물어야 하는 질문이다 — 초록불이 초록불인가. 백로그 5,300줄을 집계한 결과다.

초록불 자체는 진짜다. 다만 재시도로 넘긴 습관이 남아 있다

플레이크 언급 16회 중 근본원인까지 간 것 5 · 재시도로 넘긴 것 7 · 미해결로 남긴 것 4. 32849a2 이전 풀 실행은 188·187·188·188 — 실행마다 자리를 바꾸는 빨강 하나였고, 고친 뒤 188/188 3연속이다. 남은 넷(smoke:1554·smoke:1916·smoke:389·layouts:523)에 대해 레포는 “고쳤다고 말하지 않는다”고 정직하게 적었다.

⚠️ 다만 재시도 습관은 고친 지 4일 만에 재발했다(같은 mobileDraw 조준-배치 경로). 레포가 그 습관을 “모든 변경의 관문을 못 믿게 만든다”고 지목해 놓고도 아직 못 잡은 자리다.

가장 큰 두 카테고리는 “실패할 수 없는 단정”“같은 판정이 두 곳에”

각각 20회·25회 이상. 뒤엣것은 레포의 자기 카운터가 망가져 있다 — 네 번째 → 열 번째 → 열한 번째(서로 다른 둘이 같은 번호) → 열두 번 → 다시 “네 번”. 아무도 안 세고 있으니 레포가 아는 것보다 크다.

더 중요한 건 발견 경로가 거의 전부 사람이라는 점이다(최종 리뷰·적대적 리뷰·사용자 신고). 불변식을 실제로 지키는 테스트는 signalAlertSpecs.test.ts“home — 알림이 사는 곳” 딱 하나다. 중복을 합칠 때마다 그 한 줄을 같이 복제하는 것이 이 카테고리를 끝내는 유일한 길이다.

앞선 규율도 이미 글로 서 있다 — “Tailwind를 쓰는 한 클래스 문자열로 상태를 판정하면 안 된다”(3번 당한 뒤 규칙이 됨), 그리고 한 단계 위: “대조군 0을 확인하는 것으로는 부족하다 — 증명하려는 것만 끄면 빨개지는가를 재야 한다.”

백로그에 열린 채로 남은 네 줄

전부 “점검 안 했다”가 본문에 박힌 항목이다. 기록은 정직한데 후속이 없다.

· 다른 right-0 팝오버에 같은 함정이 있는지 미점검(3,800줄 지나도록 재방문 없음)
· ChartView.tsx 가격축 + 단추에 같은 transition-transform 함정을 남겨둔 채 기록
· 통합이 새 구멍을 만든 사례 — 목록은 RSI_LEVEL_ALERT_TF_CHOICES 한 곳인데 단정이 네 곳이라, 칩을 더하면 새 칩이 검증 밖에 남는다
· M16 이중 방어선에 가드가 없다 — “‘중복이니 하나 지우자’는 미래의 정리가 마지막 방어선을 조용히 없앨 수 있다”

정정검토가 틀렸던 것 — 고치기 전에 확인해서 살았다

세 건은 코드로 확인하니 전제가 무너졌다. 고쳤다면 순수한 손해였을 변경들이다.

findAbc 가 피봇마다 배열을 잘라 지그재그를 다시 돈다 — 사실이 아니다

abc.ts.slice()단 한 곳(505줄)이고, 완성된 패턴의 B→C 구간만 잘라 부속파동을 센다. 피봇마다 도는 것은 산술뿐인 readWindow 다. “틱마다 슬라이스 1,000개”는 없었다.

라이브 시세를 그대로 쓰는 테스트 7개 — 사실이 아니다

그 파일들은 전부 주입된 fetcher·픽스처를 쓴다(await fetch( 0건). 거래소 URL 문자열이 단정 안에 들어 있어 검색에 걸린 것이다 — 검색어가 만든 발견이었다.

다람쥐 8스트림만 어댑터를 안 탄다 — 구조는 맞지만 피해가 아니다

“3일봉이 333봉만 본다”는 합성 접기의 정상 동작이다 — 이번에 forwardReach 로 명문화한 바로 그 셈(기반 1000개 ÷ 배수). streamBars.ts 머리말이 왜 이 자리인지 이미 논증해 두었고, 심 위험 대비 실익이 없어 남긴다.

되돌린 것 — 저장소 쓰기 디바운스

리플레이 한 걸음마다 나는 동기 localStorage.setItem 을 250ms 로 모았더니 e2e 다섯이 저장소가 화면보다 늦다고 잡았다(커서 0, 포지션 미반영, 청산 기록 누락). 되돌렸다.

얻는 것은 “몇 KB JSON 을 초당 몇 번”이라는 실측 없는 추정이었고, 잃는 것은 확실했다 — 탭을 급히 닫으면 마지막 걸음이 사라진다. 저장소가 곧 그 판의 진실이라는 약속이 성능 추정보다 앞선다. 재생이 끊기던 진짜 원인(useSquirrelStreams 의 렌더마다 마흔~예순네 번 재계산)은 실측으로 따로 잡았다.

좋음여기는 튼튼하다 — 지켜야 할 것들

검토가 결함 목록만 되면 무엇을 지켜야 하는지가 안 보인다. 이 여섯은 근거와 함께 확인됐다.

닫힘 판정의 단일 진실원이 실제로 하나다

barCloseTime 하나를 경계 필터·알림 서버·신호 스로틀·리플레이 커서·날짜 고르기·마커 배치가 전부 부른다. 1M을 달력으로 셈하는 것도 거기 한 곳이다. 이탈자는 월봉 카운트다운 하나뿐이었다.

레이아웃을 별도 키로 가른 판단이 실물로 값을 했다

quant-trade의 벤더 사본은 9월 3일자인데, 그 뒤 M49·M50과 리플레이 전부가 그 사본을 한 글자도 안 건드리고 지나갔다. 임베드에서 심볼 키 마이그레이션을 끈 판단도 같은 결로 맞았다.

저장 원자성이 11일간 실전에서 버텼다

tmp + rename + 경로별 프로미스 체인. 알림을 하루 수십 번 만졌는데 /data.tmp·.bad 잔해가 0개. “200회 중 26회 파손”이던 옛 결함이 확실히 잡혔다.

과거 신호 홍수 방어가 이중이다

“마지막 봉에서 확정된 것만” + 파일에 남는 원장. 오늘 컨테이너를 서른 번 가까이 재생성했는데 중복 푸시가 없었던 이유다. 미지원 소스를 조용한 [] 대신 throw로 막은 것이 이 축에서 가장 값진 선택이다.

합성 봉 앵커가 거래소를 안 탄다

바이낸스·게이트가 8시간·3일봉을 네이티브로 줘도 안 받는다. 두 거래소의 마커가 같은 줄에 앉는 것이 우연이 아니라 구조다. 실시간 합성 봉의 누적 거래량 함정도 세 겹으로 막혀 있다.

콜백이 없으면 단추를 아예 안 그린다

지표 패널·그림 툴바·🔔이 모두 같은 규칙을 따른다. 날짜까지 판과 임베드에서 “켰는데 안 온다”가 구조적으로 불가능하다. 실패를 말하는 알약이 스스로 사라지지 않는 것도 같은 철학이다.

사보타주 복원 절차가 사고를 겪고 굳었다

에이전트가 git checkout --로 자기 구현을 통째로 날린 사고가 두 번 있었고, 그 뒤 대조군 복원은 스크래치패드 cp + diff/shasum 바이트 동일성 확인이 표준이 됐다. 변이 테스트도 두 갈래로 정착했다 — 이전 커밋 워크트리는 “이 기능이 정말 새로운가”, 사보타주는 “이 줄이 정말 그 테스트를 지키는가”.