2026-07-24 (금) 개발일지
오늘의 트러블슈팅: 포트폴리오 이미지 503과 사주 앱 공유카드 기능의 오해
오늘은 두 가지 이슈를 처리했다. 하나는 포트폴리오 사이트에서 GitHub stats 이미지가 안 뜨는 문제, 다른 하나는 사주 소개팅 앱의 공유카드 기능이 정확히 무엇인지 파악하는 과정이었다. 전자는 서드파티 서비스 장애였지만 후자는 코드를 직접 까보기 전까지 내가 잘못 이해하고 있었던 부분이었다.
포트폴리오 GitHub stats 이미지 503 — 공용 서비스 중단을 빌드 단계에서 차단
며칠 전부터 joyuno.io 포트폴리오에서 GitHub stats 카드와 Top Langs 이미지 두 개가 깨져 보인다는 피드백이 들어왔다. 처음엔 내 .github/README.md의 stats 엔드포인트가 잘못된 줄 알고 GitHub Action 로그를 뒤졌다. curl -I https://github-readme-stats.vercel.app/api?username=joyuno를 찔러보니 HTTP 503, 응답 본문은 99바이트짜리 평문 — “The deployment is currently unavailable”, 정확히 DEPLOYMENT_PAUSED 상태였다.
“당신 문제가 아닙니다. 공용 서비스가 통째로 죽었습니다.”
내 계정(joyuno)뿐 아니라 원저자(anurag)의 계정으로도 동일한 503이 떨어졌다. querystring 파라미터를 모두 제거해도, 다른 테마를 넣어도 똑같았다. 서비스 자체가 Vercel에서 중단된 것이었다. 이 서비스는 오픈소스지만 원저자가 Vercel에 무료 티어로 호스팅하고 있는 상태라, 통제 밖이었다.
처음엔 로컬에 self-hosted 인스턴스를 띄울까 고민했다. stats 카드 생성 로직은 단순한데, github-readme-stats를 클론해서 직접 Vercel/Cloudflare Workers에 올리면 해결될 문제였다. 하지만 이슈는 단순 복구가 아니라 — 내 포트폴리오 빌드가 외부 서비스 하나에 묶여 있다는 구조적 위험이었다. 오늘 503이 내일 다시 돌아올 수도 있고, 다른 날은 또 다른 서비스가 죽을 수 있다.
결국 선택한 해법은 빌드 단계에서 이미지 상태를 검증하고, 죽어 있으면 fallback 이미지로 대체하는 방향이었다. generate_pdf.py 스크립트를 만들 때 이미 Playwright로 페이지 캡처를 하고 있었으니, 같은 파이프라인에 이미지 헬스체크를 끼워넣었다.
바뀐 점
build.sh에_precheck_images단계 추가 — 모든 외부 이미지 URL에 대해 HTTP HEAD 요청으로 status 200 + content-typeimage/*검증- 실패한 이미지는
assets/fallback/에 미리 준비한 placeholder SVG로 대체 - 빌드 로그에
[WARN] external image 503, using fallback: github-readme-stats출력
검증: 재빌드 후 joyuno.io에서 stats 이미지가 placeholder로 정상 표시됨. PDF도 동일하게 fallback 이미지가 들어간 상태로 생성됨.
남은 과제: fallback 이미지가 단순 placeholder라 사용자 경험이 나쁘다. 주기적으로 외부 서비스 복구 여부를 체크하고, 복구되면 원본 이미지로 자동 교체하는 cron 작업을 고려 중. 또는 stats 정보를 직접 긁어와서 이미지를 생성하는 self-host 방안도 검토 중이다.
사주 앱 공유카드 — “궁합 결과 이미지”의 진짜 요구사항
두 번째 이슈는 사주 소개팅 앱의 공유카드 기능이었다. 프로젝트 기획서를 검토하다 보니 “공유카드”라는 용어가 자주 나왔는데, 정확히 어떤 기능인지 머릿속에 그려지지 않았다. 처음엔 단순히 궁합 점수를 텍스트로 보내는 기능일 거라 추측했다. 하지만 사용자가 “내 궁합 자랑/인증용 이미지 공유”라고 말한 걸 듣고, 이게 이미지 기반 공유라는 걸 깨달았다.
실제 코드를 열어봤다. share_service.dart에 showFateCardSheet 함수가 있었고, 그 안에서 RepaintBoundary로 특정 위젯을 캡처한 뒤 OS 공유 시트를 띄우는 로직이었다. 즉, 궁합 결과(운명의 한 문장 + 점수)를 한 장의 카드 이미지로 캡처해서 카톡·SNS에 공유하는 기능이었다.
내가 놓친 핵심은 이게 단순 공유 기능이 아니라 제품의 바이럴 엔진이라는 점이었다. 기획서에는 “공유카드”라고만 적혀 있었지만, 실제로는 사용자가 자신의 궁합 결과를 이미지로 저장하고 친구에게 자랑할 수 있는 기능이었다. 이 기능을 통해 자연스러운 입소문이 발생하고, 앱의 신규 유입 채널이 되는 구조였다.
원인: 용어의 모호함을 간과했다. “공유카드”라는 명칭이 UI 내에서 텍스트 기반 공유와 이미지 기반 공유를 구분하지 않고 사용되고 있었다. 실제 동작은 이미지 캡처였지만, 기획 문서에는 그 디테일이 생략되어 있었다.
해결: share_service.dart의 함수명을 showFateCardSheet → showFateCardImageShareSheet로 변경하고, 관련 주석과 기획서에 “이미지 캡처 후 OS 공유 시트”라고 명시했다. 또한 이미지 캡처 품질을 2x(레티나)로 고정하고, 공유 시트에서 “카카오톡”이 기본 액티비티로 뜨도록 플랫폼별 설정을 추가했다.
검증: 시뮬레이터에서 실제 궁합 결과 화면을 캡처해 카카오톡으로 공유했을 때 이미지 해상도(720x1280px)와 텍스트 가독성이 만족스러웠다.
남은 과제: 일부 저사양 기기에서 RepaintBoundary 캡처 시 검은 화면이 나오는 이슈가 보고되었다. WidgetsBinding.instance.addPostFrameCallback으로 프레임이 완전히 그려진 후에 캡처하도록 수정했지만, 아직 모든 기기에서 검증되지는 않았다.
오늘의 한줄
외부 서비스 장애는 막을 수 없지만 그 의존성을 빌드 파이프라인에서 차단하는 것과, 용어의 모호함이 코드를 직접 까보기 전에는 발견되지 않는 함정이라는 걸 다시 확인했다.