2026-08-31 (월) 개발일지
오늘의 회고: 리플레이 충실도 결함부터 SK 130제 데이터 계층까지
오늘은 두 가지 프로젝트를 병행했다. 하나는 demo_apps 쪽 SwiftUI 리플레이 결함을 파고들어 DisplayList 구조까지 내려간 일, 다른 하나는 게임 프로젝트에서 130제 엑셀 데이터를 파싱해 빈칸 학습 모드의 데이터 계층을 설계한 일이다. 두 이슈 모두 로그와 소스를 직접 까보지 않았다면 절대 원인을 잡지 못했을 패턴이었다.
SwiftUI 리플레이 — pruneInvisibleNodes 가 SF Symbol을 통째로 지운 이유
payment_main과 pay_tab 화면의 리플레이 커버리지가 0.008이었다. 텍스트가 거의 없는 UI라는 걸 감안해도, 이 수치는 단순히 “텍스트가 적다”로 설명할 수 있는 범위를 넘었다. 처음엔 텍스트 추출 로직이 SwiftUI 뷰 트리를 제대로 순회하지 못하는 버그라 생각했다.
첫 가설은 텍스트 노드가 아닌 SF Symbol이 너무 커서 DisplayList에서 아예 생략된다였다. TmpKindProbe로 큰 SF Symbol(120pt qrcode)이 어떤 kind로 떨어지는지 찍어봤다. 결과는 kind="shape", 크기는 108x108 — shape 노드였다. 여기까지는 예상 범위였다.
문제는 그 다음이었다. shape는 현재 type:"view"로 분류되고 있었고, 여기에 색상 정보가 없으면 pruneInvisibleNodes가 해당 노드를 통째로 삭제하고 있었다. 검증을 위해 _self_host_image() 로직에서 shape 노드의 색상이 어떻게 전달되는지 직접 추적했다. HEAD 검증 로직을 따라가보니, content-type이 image/*이고 content-length가 5KB 이상이면 외부 URL을 그대로 쓰는데 — 여기서 shape 노드로 변환되는 단계에서 색상 메타데이터가 소실되고 있었다.
핵심 발견: shape 노드는
type:"view"+ 색상 없음 조합이면pruneInvisibleNodes가 통째로 삭제한다. 이게 결함 1의 진짜 뿌리였다.
해결은 두 가지 방향이 있었다. DisplayList 계층에서 shape 노드의 색상 정보를 강제로 주입하거나, 아예 렌더링 경로를 벡터에서 래스터로 폴백시키는 것. 나는 SwiftUIDisplayListReflector 를 통해 DisplayList Mirror 리플렉션으로 27개 노드(텍스트 21 + 이미지 5 + 배경 1)를 전부 복구하는 쪽을 택했다. 래스터 폴백은 해상도 의존성이 생겨서 피했다.
검증: iPhone 16 Pro(iOS 18.6)와 iPhone 17 Pro(iOS 26.5) 시뮬레이터에서 실측했다. 한글 포함 21개 텍스트 + 폰트 크기·이름, 절대좌표까지 정확하게 복구됐고, 래스터 폴백 없이 벡터 와이어프레임 상태를 유지했다.
남은 과제: 결함 4(캡처가 아니라 전송 유실 — SDK 307 emit → 서버 89 도착, 71%)는 아직 원인만 규명됐고 수정은 안 됐다. 다음 세션에서 서버 쪽 OTLP 엔드포인트를 직접 들여다볼 예정이다.
로컬 PC 디스크 폭발 — iCloud Drive 로그 15.4GB
평소 200GB 여유가 있던 C드라이브가 갑자기 7MB만 남았다는 메시지. 처음엔 Docker 이미지가 쌓였거나 VS Code 캐시가 터진 줄 알았다. diskcheck.ps1로 긴급 스캔을 돌렸다.
예상과 달랐다. Docker는 22.7GB, .cache는 7.9GB로 평소와 비슷했다. 진짜 범인은 iCloud Drive 로그 폭주였다. 로그 파일 하나(iCloudDrive.2026-08-28_1454.log)가 6.5GB, 110개 파일 합계가 15.4GB였다. 현재 로그(iCloudDrive.2026-08-28_1454.log)는 2.3GB로 계속 커지는 중이었다. 여기에 VS Code 업데이트 잔재가 매달 0.7GB씩 14개월치 쌓여 임계점을 넘은 상황.
처음엔 iCloud Drive 동기화 버그라 생각해 서비스 재시작만 시도했다. 하지만 로그를 까보니 ~\AppData\Local\Temp\iCloudDrive 경로에 로테이션 없이 단일 파일로 쓰고 있었다. 로그 레벨이 DEBUG로 고정돼 있었고, 파일 크기 제한도 없었다. 정리를 위해 iCloudDrive.*.log 패턴으로 구버전은 전부 삭제하고, 현재 로그만 백업 후 잘라냈다. VS Code 업데이트 잔재는 %TEMP%에서 날짜별로 추려서 14개월치를 정리했다.
검증: 삭제 후 C드라이브 여유 공간이 52GB로 복구됐다. iCloud Drive 동기화도 정상 작동한다.
SK 130제 — 엑셀 파싱을 stdlib만으로, 데이터 계층 설계에 계약을 박다
2026_SK_Junior_Talent_코딩테스트_130제.xlsx를 받아서 파싱해야 했다. 처음엔 openpyxl을 떠올렸지만, 1회성 변환에 의존성 추가는 과하다고 판단했다. stdlib의 zipfile + xml.etree로 파싱하는 xlsx_read.py를 직접 짰다.
파싱 결과는 깔끔했다. 시트 5개, 130제 전부 정상 — 요약·트리거·정답패턴 결측 0건. 분포도 유형 6종(DFS·그래프 25, 트리+DP 25, DP 25, 구현·시뮬 25, BFS·최단경로 15, 그리디·정렬·힙 15)으로 균형이 잘 잡혀 있었다.
진짜 작업은 데이터 계층 설계였다. 기존 제노랩 54제와 SK 130제를 2트랙으로 공존시키는 게 목표. 기존 문제 파일은 track 필드가 없으면 "xenolab"으로 기본값 처리하도록 해 수정을 0건으로 유지했다.
설계에서 가장 신경 쓴 부분은 콘텐츠 계약을 테스트로 강제하는 것이었다. test_problem_content.py의 기존 계약 4건(C1: 공개 필드 집합 정확 일치, C2: 레퍼런스 답안 파생 공유, C3: 표본 1:1 계약, C4: 챕터 레지스트리 등록)을 SK 트랙에 맞게 조정했다.
가장 까다로웠던 건 빈칸 예산이었다. 난이도별로 빈칸 개수와 커버리지를 정해야 했다.
| 난이도 | 빈칸 개수 | 커버리지 |
|---|---|---|
| basic | 7 ~ 10 | 25 ~ 45% |
| intermediate | 5 ~ 7 | 15 ~ 30% |
| advanced | 3 ~ 5 | 8 ~ 20% |
이 예산을 BUDGET 딕셔너리로 tests/test_blanks.py에 박았다. AST 비교 1차 + 실행 출력 2차의 2단 채점 구조도 문서화했다. 첫 단계를 AST 비교로 하는 이유는 문자열 정규화(공백 축약)가 리터럴 내부(" ".join vs " ".join)를 구분하지 못하기 때문이다.
검증: 파일럿 1번(BOJ 2178 미로 탐색)으로 _MARKER 정규식과 AST 비교 파이프라인을 실측했다. ⟦popleft‖FIFO여야 최단거리가 보장된다⟧ 같은 마커가 정상 파싱되고, 학습자 코드에서 popleft가 빠지면 AST 불일치로 정확히 잡아내는 걸 확인했다.
남은 과제: 파일럿 10제가 완성됐지만, 나머지 54제를 저작하기 위한 변수명 규약과 챕터 레지스트리 등록이 남았다. backfill cron도 검토 중이다.
오늘의 한줄
로컬 디스크 폭발의 범인은 언제나 로그였고, SwiftUI 노드가 사라진 이유는 항상 색상 정보의 소실이었다 — 두 문제 모두 “보이지 않는 것”을 추적할 때 코드보다 먼저 데이터의 흐름을 따라야 한다는 걸 다시 확인했다.