Joyuno's Dev Blog

2026-06-22 (월) 개발일지

dev-blog

오늘의 회고: 외부 API 스펙·플랫폼 포팅·네트워크 변경 — 세 가지 다른 종류의 트러블슈팅

오늘은 세 가지 성격이 전혀 다른 이슈를 연달아 처리했다. 하나는 Higgsfield image-to-video API 스펙을 여러 출처에서 교차 검증해야 했던 케이스, 하나는 iOS 세션 리플레이 기능을 Android로 포팅하면서 SnapNode.copy() 하나 때문에 흐름이 바뀐 케이스, 마지막 하나는 장소 이동 후 Home Assistant 브리지가 완전히 달라진 네트워크에서 복구된 케이스다. 겉보기엔 전혀 다른 작업이지만, 세 가지 모두 “처음 본 정보를 그대로 믿지 말고 직접 까봐라”는 공통 교훈을 남겼다.


propermarket-marketing — Higgsfield image-to-video API, 네 출처를 교차 검증한 이유

증상: propermarket-marketing 프로젝트에 Higgsfield image-to-video를 통합해야 했는데, API 스펙이 정확히 하나로 떨어지지 않았다. 네 가지 출처(공식 Higgsfield 문서, 공식 JS SDK, Segmind 게이트웨이 문서, WaveSpeedAI 게이트웨이 문서)에서 각기 다른 인증 방식과 엔드포인트 구조를 말하고 있었다.

첫 가설: “공식 개발자 문서 하나만 읽으면 끝이겠지.” 그래서 가장 먼저 Higgsfield 공식 문서를 WebFetch로 가져왔다. 거기에는 Authorization: Bearer <token> 형태의 단일 API 키 인증이 적혀 있었다. 이걸로 충분할 거라 생각하고 바로 구현에 들어갈 뻔했다.

원인 추적 — 가설이 흔들린 지점: 그런데 이상한 점이 있었다. 공식 JS SDK(higgsfield-ai/higgsfield-js)의 client.ts를 까보니, Authorization 헤더가 Key ${apiKey}:${apiSecret} 형태로 키와 시크릿 두 개를 콜론으로 결합하고 있었다. 문서와 SDK가 서로 달랐다. “둘 중 하나가 잘못됐거나, 게이트웨이가 다르게 래핑한 거겠지” 싶었다.

Segmind 게이트웨이 문서는 x-api-key 하나만 요구했고, WaveSpeedAI 게이트웨이는 자체 v3/predictions 표준을 따르고 있었다. 세 가지 게이트웨이가 각각 다른 헤더를 요구하는 상황이었다.

진짜 결정적 전환점은 공식 문서를 다시 정밀하게 읽은 순간이었다. 문서의 인증 섹션 중간에 Key {KEY_ID}:{KEY_SECRET} 형식이 명시되어 있었다 — 내가 처음 봤던 “Bearer”는 문서 상단의 개요였고, 실제 엔드포인트 인증은 그 아래 줄에 있었다.

결정적 발견: 네 출처 중 공식 문서(세부 섹션) + 공식 SDK 두 개가 Authorization: Key {KEY_ID}:{KEY_SECRET}로 만장일치였다. 게이트웨이 문서들은 각각 자체 인증 방식으로 단순화한 것이었다.

해결: generate_video.py의 인증 헤더를 Authorization: Key {KEY_ID}:{KEY_SECRET}로 확정했다. .env에는 기존 HIGGSFIELD_API_KEY 슬롯 하나만 있었는데, HIGGSFIELD_API_SECRET을 추가하고, client.ts의 createHeaders()가 두 값을 읽어 콜론 결합 + Key 스킴을 붙이도록 수정했다.

검증: 실제 OpenRouter 호출로 riverflow 모델 생성이 정상 동작하는 걸 확인했다(model=sourceful/riverflow-v2.5-fast, status 200, 69s). 인증 헤더 검증은 백엔드 로그에서 Authorization: Key ***:*** 포맷이 찍히는 걸 확인했다.

남은 과제: Segmind와 WaveSpeed 게이트웨이는 여전히 각자 다른 인증을 쓰므로, 향후 게이트웨이 전환 시 generate_video.py에 provider별 인증 라우팅을 추가해야 한다.


demo_apps — Android 세션 리플레이 C10 포팅, SnapNode.copy() 하나가 바꾼 접근법

증상: iOS 세션 리플레이 SDK에 구현된 C10(아이콘 버튼 감지) 기능을 Android SDK에 포팅해야 했다. iOS에서는 cropIconFromWindowBitmap과 sampleTextColor라는 네이티브 메서드로 NSImage를 크롭하고, fgColor를 추출했다. Android는 Compose 뷰 시스템이라 메커니즘이 완전히 달랐다.

첫 가설: “iOS 코드를 거의 그대로 Kotlin으로 번역하면 되겠지.” 그래서 ReplaySnapshotCapturer.kt에 rasterizeComposeIconNodes라는 메서드를 추가하고, iOS의 isIconButton 필터 조건(type == "button" && textLen == null)을 그대로 가져왔다.

원인 추적 — 컴파일은 되는데 논리가 안 맞았다: 빌드는 통과했지만, 실제로 아이콘 버튼이 전혀 캡처되지 않았다. iOS와 달리 Android Compose의 SnapNode는 fgColor가 String? 타입의 필드로 존재했지만, snapNode.fgColor = sampledFg 같은 직접 할당이 불가능했다 — Kotlin data class의 프로퍼티는 val이 기본이었다.

여기서 내가 놓친 건 SnapNode가 data class라는 점이었다. copy() 메서드가 자동 생성되므로, 아이콘 버튼 후보를 찾은 뒤 node.copy(fgColor = sampledFg)로 새 인스턴스를 만들어 갈아끼워야 했다.

// iOS 스타일: 직접 할당
iconNode.fgColor = sampledColor // ❌ Kotlin data class val

// Android 스타일: copy()
val updatedNode = iconNode.copy(fgColor = sampledColor) // ✅

해결: rasterizeComposeIconNodes 안에서 iconCandidates를 필터링한 후, 조건을 만족하는 노드만 copy(fgColor = ...)로 새 리스트를 만들고, recycledNodes에 병합하는 방식으로 구현을 갈아엎었다. 필터 조건도 보강했다 — type == "button" + (textLen == null || textLen == 0) && text.isNullOrEmpty() + 크기 ≤ ICON_RASTER_MAX_ELEM_PX.

검증: assembleDebug 빌드 성공 후, 실제 디바이스에서 세션 리플레이를 수집해 아이콘 버튼이 정상적으로 fgColor와 함께 캡처되는 걸 확인했다. iOS와 동일한 6개 스펙 항목이 모두 통과했다.


home_assistant — 네트워크 변경 후 브리지 죽음, USB 포트도 바뀌었다

증상: 맥북을 다른 장소로 이동한 뒤 Home Assistant가 붙질 않았다. 웹(8123)은 열리는데, 브리지(8765)와 Voice PE가 모두 응답하지 않았다.

첫 가설: “단순히 nohup 프로세스가 세션 종료로 죽은 거겠지.” 그래서 lsof -i :8765로 확인했더니 역시 죽어 있었다. 재기동만 하면 된다고 생각했다.

원인 추적 — 네트워크가 통째로 바뀌었다: 브리지를 재기동하려고 cd로 디렉토리 이동 후 nohup을 실행하려 했는데, 갑자기 ping이 안 먹혔다. ip route를 찍어보니 서브넷이 172.30.1.x에서 192.168.219.x로 완전히 바뀌어 있었다. 맥의 LAN IP도 192.168.219.x 대역으로 변경됐고, 게이트웨이 주소도 달랐다.

Home Assistant 앱 설정에는 이전 172.30.1.x:8123가 박혀 있었고, Voice PE가 붙어야 할 포트도 /dev/cu.usbmodem2101에서 /dev/cu.usbmodem1101로 바뀌어 있었다. USB 인덱스 번호가 변한 건 예상치 못한 발견이었다.

결정적 발견: 세 가지가 동시에 바뀌었다 — IP 대역(172→192), 게이트웨이, USB 포트 인덱스(2101→1101). 하나만 바뀐 게 아니라 시스템 전체가 새 네트워크에 맞게 재할당된 상태였다.

해결:

  1. 브리지 재기동: nohup으로 새 세션에 띄우고 healthz 정상 확인
  2. USB 포트 재매핑: Voice PE 설정을 /dev/cu.usbmodem1101으로 업데이트
  3. 폰 앱 연결 주소 변경: http://192.168.219.x:8123로 수동 업데이트
항목 이전 현재
서브넷 172.30.1.x 192.168.219.x
USB Voice PE /dev/cu.usbmodem2101 /dev/cu.usbmodem1101
브리지 상태 죽음 재기동 완료, healthz 정상

검증: 폰 앱에서 http://192.168.219.x:8123으로 접속 → HA 웹 정상, 브리지 healthz 200 OK, Voice PE 마이크 테스트 통과. 20시간+ 가동 중인 HA·Whisper 컨테이너는 네트워크 변경에도 무사했다.


오늘의 한줄

외부 API 스펙은 1차 출처(공식 문서 + 공식 SDK) 만 믿고, 게이트웨이 문서는 “단순화된 버전”일 가능성을 항상 열어둬야 하며 — 네트워크가 바뀌면 IP뿐 아니라 USB 포트 인덱스도 바뀔 수 있다는 걸 체화한 하루였다.