깃허브 프로필에 예쁜 코딩 통계 박기 (feat. 마켓플레이스 액션까지 만들어버림)

깃허브 프로필에 WakaTime 통계를 걸어두면 일종의 "나 요즘 이렇게 코딩하고 삽니다" 증명서가 됩니다. 최근에 무슨 언어를 얼마나 만졌는지가 한눈에 보이거든요.

문제는 그 흔한 방식 — 최근 7일 통계를 텍스트로 README에 그냥 박아넣는 것 — 이 좀 성에 안 찼다는 겁니다.

TypeScript 24 hrs 26 mins ██████████░░░░░░░░░░░░░░░ 40.05 %
Dart 10 hrs 57 mins ████▒░░░░░░░░░░░░░░░░░░░░ 17.96 %
Markdown 6 hrs 50 mins ██▓░░░░░░░░░░░░░░░░░░░░░░ 11.22 %

작동은 합니다. 하는데... 못생겼어요. (제 눈엔 그랬습니다.) 그래서 결국 통째로 다시 만들었습니다. 투명 배경 SVG 카드로 그리고, 깃허브 액션으로 매일 자동 갱신하고, 전용 브랜치에 올리고, 남들도 쓸 수 있게 마켓플레이스 액션으로 포장까지. 시작은 "예쁜 카드 하나 갖고 싶다"였는데 끝은 오픈소스 배포였습니다. 개발이 늘 그렇죠 뭐. ㅎㅎ

이 글은 그 과정과, "혼자 쓸 때는 안 보이다가 남한테 배포하려니까 갑자기 튀어나온" 마켓플레이스 규칙들 이야기입니다.

Before: 텍스트 통계의 한계

텍스트 방식은 세팅이 빠른 대신 딱 이 정도가 한계입니다. 제가 원한 건 이런 거였어요.

  • 다크모드 해킹 없이도 예쁜 비주얼
  • 모바일에서도 안 깨지는 레이아웃
  • 언어 통계 + "무슨 프로젝트를 만졌나" 두 가지 뷰
  • 다른 레포·프로필에도 재사용
  • README 손수 편집 안 하기 (이게 제일 중요)

네. 하셨죠? 하나하나 보면 별거 아닌데 다 합치면 은근히 큰 그림입니다.

After: 투명 SVG 카드 + 매일 자동화

최종 그림은 단순합니다.

  • 스크립트가 generated/languages.svggenerated/projects.svg 두 장을 그림
  • 깃허브 액션이 매일(그리고 원할 때 수동으로) 실행
  • 생성물은 output 이라는 별도 브랜치로 push
  • README는 그 브랜치의 raw SVG를 임베드
![WakaTime Languages](https://raw.githubusercontent.com/<USER>/<REPO>/output/generated/languages.svg)
![WakaTime Projects](https://raw.githubusercontent.com/<USER>/<REPO>/output/generated/projects.svg)

왜 굳이 output 브랜치로 뺐냐고요? 생성 파일을 main에서 치워버리면 커밋 히스토리가 지저분해지지 않고(매일 자동 커밋이 main을 도배하는 참사를 막습니다), 어디든 임베드할 수 있는 안정적인 URL이 생기거든요. 창고는 창고대로 따로 두는 거죠.

1단계 — WakaTime 통계 가져와서 SVG로 그리기

스크립트가 가져오는 건 두 개입니다.

  1. 최근 7일 통계
  2. 언어별 공식 색상

WakaTime은 개발자용 REST API를 제공합니다(공식 문서). 제 생성기는 이 두 엔드포인트를 씁니다.

API_BASE = "https://wakatime.com/api"

# 최근 7일 통계
r = requests.get(f"{API_BASE}/v1/users/current/stats/last_7_days",
                 headers={"Authorization": f"Basic {api_key}"}, timeout=30)
r.raise_for_status()

# 언어별 공식 색상 (Dart는 파랑, TS는 진파랑... 그 색)
r = requests.get(f"{API_BASE}/v1/program_languages",
                 headers={"Authorization": f"Basic {api_key}"}, timeout=30)
r.raise_for_status()

그리고 SVG를 그리는데, 신경 쓴 디자인 포인트는 이렇습니다.

  • 투명 배경 — 배경 사각형을 fill="none"에 얇은 stroke만 줍니다. 그래야 깃허브 라이트/다크 테마 어디에 얹어도 자연스럽게 녹아들어요.
  • 기본 너비 360px — 모바일에서 읽히는 폭
  • 동적 높이 — 행 개수에 따라 카드가 늘었다 줄었다
  • 진행 바 — 언어 비율이 눈에 즉시 박히도록
  • 프로젝트 뷰의 "활동 신호" — 추가/삭제 줄 수 비율을 보여줍니다. "AI가 짠 코드냐 사람이 짠 코드냐"를 판별하려는 시도(논란만 부르는)는 일부러 피했어요. 덜 논쟁적이고 더 보편적인 쪽으로요.

카드 본문은 <foreignObject> 안에 HTML/CSS를 심어서 그립니다. 행마다 슬라이드-인 애니메이션도 넣었고요. (없어도 되는데, 있으면 괜히 잘 만든 것처럼 보이잖아요.)

삽질 포인트: 모바일에서 빈 이미지가 뜬다

처음에 깃허브 모바일 앱에서만 SVG가 하얀 빈 이미지로 떴습니다. 웹에선 멀쩡한데요.

당연히 <foreignObject> 호환성 문제라고 의심했죠. "아 이거 순수 SVG 버전이랑 애니메이션 버전을 따로 만들어서 분기해야 하나..." 하고 머리를 싸맸습니다. (일 두 배 되는 소리가 벌써 들렸어요.)

그런데 범인은 훨씬 시시했습니다. viewport 설정이었어요. 불필요한 viewport 속성을 걷어내고 너비를 모바일 친화적인 360px로 통일했더니, 모바일 앱에서도 멀쩡하게 떴습니다. 렌더링 경로를 둘로 쪼갤 필요가 애초에 없었던 거죠. 이럴 때 허탈하면서도 좋잖아요. 코드가 줄었으니까. 헤헤.

2단계 — 자동화하고 깨끗한 브랜치로 배포

액션을 쓰는 쪽 워크플로우는 일부러 단순하게 짰습니다. (아래 항목들은 composite 액션 안이 아니라 이 워크플로우 파일에 들어갑니다 — composite 액션의 스텝에는 concurrency 같은 걸 못 넣거든요.)

  • cron으로 매일 예약 실행
  • 동시 실행 방지 — concurrency: { group: wakatime-svg, cancel-in-progress: true }
  • 딱 필요한 만큼의 권한만: contents: write
  • output 브랜치로 배포

배포 방식은 이렇습니다. output 브랜치를 worktree로 붙여서 기존 내용을 싹 비우고, 새로 그린 SVG를 넣고 커밋한 뒤 force-push 합니다.

git -C "${workdir}" add "${images_dir}"
git -C "${workdir}" commit -m "${COMMIT_MESSAGE:-chore: update wakatime svg}" || true
git -C "${workdir}" push origin "${branch}" --force

커밋 뒤에 붙은 || true가 은근히 중요합니다. 통계가 어제랑 똑같아서 바뀐 게 없으면 git commit이 "커밋할 거 없다"며 실패하는데, || true가 그 실패를 삼켜서 워크플로우 전체가 빨간불로 죽지 않게 해줍니다. (매일 도는 액션이 하루 안 걸었다고 실패 알림을 쏘면... 그거 은근 스트레스거든요.)

솔직하게 짚자면, 이건 "바뀐 내용만 골라 커밋하는 정교한 캐싱"은 아닙니다. 그냥 브랜치를 매번 새로 갈아끼우고, 바뀐 게 없을 때만 커밋이 얌전히 넘어가는 방식이에요. output을 별도 브랜치로 뺀 덕에 main 히스토리는 안 더러워지지만, output 브랜치 자체는 force-push로 계속 덮어씁니다. 과대포장은 안 할게요.

3단계 — 재사용 가능한 composite action으로

여기까지 하면 프로필에선 잘 돕니다. 근데 이걸 마켓플레이스 액션으로 만들면, 남들은 fork도 유지보수도 없이 워크플로우 10줄로 갖다 쓸 수 있게 되죠.

이런 경우엔 composite action이 딱입니다. Node 패키징도, Docker 이미지도 필요 없이 그냥 Python이랑 git 명령을 순서대로 엮은 거니까요.

action.yml은 이런 입력들을 노출합니다. (전부는 아니고 일부만)

inputs:
  WAKATIME_API_KEY: # 필수. WakaTime API 키
  WAKATIME_LANG_LIMIT: # 보여줄 언어/프로젝트 개수 (기본 5)
  WAKATIME_CHART_WIDTH: # 카드 너비 px (기본 360)
  WAKATIME_CHART_DYNAMIC_HEIGHT: # 행 수로 높이 자동 계산 (기본 true)
  BRANCH_NAME: # 배포 브랜치 (기본 output)
  COMMIT_MESSAGE: # 커밋 메시지
  IMAGES_FOLDER: # 생성물 폴더 (기본 generated)

사용자는 기본값 그대로 쓰거나, 제 코드를 한 줄도 안 건드리고 레이아웃을 완전히 커스텀할 수 있습니다. 그리고 마켓플레이스에 제대로 된 항목으로 뜨도록 branding 메타데이터도 달았어요(아이콘 bar-chart, 색 blue). 참고로 깃허브는 여기 쓸 수 있는 색과 Feather 아이콘을 정해진 목록 안에서만 허용합니다. 아무 아이콘이나 못 넣어요. (응 네가 원하는 그 이모지 우리는 모름~)

4단계 — 마켓플레이스 현실 점검 (규칙 안 읽으면 놓치는 것들)

마켓플레이스에 올리려면 깃허브가 걸어둔 하드한 제약들이 있습니다. 저는 이걸 만들고 나서야 하나씩 읽으며 "아 이건 또 뭐야" 했어요.

  • 레포가 public이어야 함
  • action.yml레포 루트에 있어야 함
  • 액션에 꼭 필요한 것만 담을 것
  • 액션 name이 유일해야 함
  • 배포는 release를 만들면서 "Publish this action…"을 체크하는 방식(공식 문서)

그래서 정리가 자연스럽게 이렇게 갈립니다.

  • 액션 레포: 액션 코드 + 문서
  • 사용자 레포: 그 액션을 소비하는 워크플로우

내 프로필 통계를 실제로 갱신하는 워크플로우는 액션 레포가 아니라 쓰는 쪽 레포에 있어야 한다는 거죠. 처음엔 이 분리가 귀찮았는데, 지나고 보니 깔끔한 강제였습니다.

5단계 — 버저닝: 남한테 default 브랜치 물리게 하지 마세요

남들이 쓸 거라면 버전 관리가 필수입니다. 깃허브 가이드대로 하면 이렇게 됩니다.

  • v1.2.0 같은 semver 태그를 배포
  • v1 같은 롤링 메이저 태그를 최신 안정 릴리즈로 유지
  • 원하면 커밋 SHA 고정도 허용

그러면 사용자가 골라 쓸 수 있어요.

  • @v1 → 깨지는 변경 없이 버그픽스만 따라감
  • @v1.2.0 → 정확히 이 버전
  • @<전체 SHA> → 불변 + 감사 가능

만약 남들이 여러분의 default 브랜치(@main)를 물게 두면, 여러분이 실험용 커밋 하나 올릴 때마다 남의 프로필이 같이 흔들립니다. 그거 진짜 민폐예요.

그래서, 남한테 쓰라고 만드니까 좋아진 것들

솔직히 저 혼자 쓸 거였으면 여기까지 안 왔습니다. "public 레포 강제, 제대로 된 버저닝, 문서 정비" 같은 마켓플레이스의 제약들이, 결과적으로 저 혼자 만들었을 때보다 훨씬 나은 물건을 만들게 밀어붙였어요.

  • 어디서 봐도 예쁨 (투명 + 테마 친화)
  • README를 손댈 일 없음 (생성물은 별도 브랜치)
  • 레포 넘나들며 재사용
  • fork 없이 입력값만으로 커스텀

앞으로 더 하고 싶은 것

  • Wakapi / Hackatime 같은 대체 서버를 위한 커스텀 API base URL 지원 (지금은 wakatime.com으로 고정)
  • 워크플로우 로그의 더 세밀한 에러 리포팅
  • SVG 출력 스냅샷 테스트로 레이아웃 회귀 잡기 (린트보다 테스트를 믿읍시다)

바로 써보기

기본값만으로 최소 구성은 이렇게 짧습니다.

name: Waka Charts

on:
  workflow_dispatch:
  schedule:
    - cron: "0 0 * * *"

permissions:
  contents: write

# 매일 도는 액션이 겹쳐 돌지 않도록 (앞선 실행은 취소)
concurrency:
  group: wakatime-svg
  cancel-in-progress: true

jobs:
  update-charts:
    runs-on: ubuntu-latest
    steps:
      - uses: AndrewDongminYoo/wakatime-svg@v1.2.0
        with:
          WAKATIME_API_KEY: ${{ secrets.WAKATIME_API_KEY }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

레이아웃을 만지고 싶으면 아래처럼 입력값을 덧붙이면 됩니다(전부 선택 사항).

WAKATIME_LANG_LIMIT: 7
WAKATIME_CHART_WIDTH: 420
WAKATIME_CHART_HEIGHT: 200
WAKATIME_CHART_ROW_HEIGHT: 28
WAKATIME_CHART_BAR_HEIGHT: 10
WAKATIME_CHART_DYNAMIC_HEIGHT: false
BRANCH_NAME: output
COMMIT_MESSAGE: "chore: update wakatime svg"
IMAGES_FOLDER: generated

마치며

이 프로젝트는 "프로필 통계 좀 예쁘게 하고 싶다"는 개인적인 근질거림에서 출발해서, 유지보수 가능한 재사용 도구로 자랐습니다. 재밌는 건, 남이 쓸 물건으로 만드는 규율이 결과물의 질을 끌어올렸다는 거예요. 입력 검증, 문서, 버저닝, 실사용 테스트 — 나만 쓸 땐 대충 넘어갔을 것들을 강제로 챙기게 되니까요.

혹시 개발자 도구를 만들고 계신다면, 한번 마켓플레이스 배포까지 가보시길 권합니다. "진짜 남이 쓸 수 있게" 만드는 데 드는 그 규율이, 코드 품질과 설계 감각으로 고스란히 돌아옵니다. 그리고 혹시 제 SVG 카드에서 이상한 렌더링을 발견하셨다면 — 네, 그 viewport 또 말썽이면 — 이슈로 알려주세요. 저도 아직 배우는 중이라 진심으로 반갑습니다.