반응형

커밋할수록 성장하는 벚꽃나무 개발기

 

GitHub 프로필 README를 꾸미다가 문득 이런 생각이 들었습니다.

배지, 스탯 카드, 뱀이 기여 그래프를 먹는 애니메이션 등등.. 다들 비슷하기도 하고 나만의 무언가가 있으면 좋겠는데. 

 

그러다 gitanimals 를 보면서 이런거 직접 만들어보자, 되지 않을까 싶었습니다. 커밋할수록 벚꽃나무가 자라고, 레벨 시스템을 붙여서,

 

그렇게 LevelUp Tree 가 시작되었습니다.

 


 

아이디어 

 

- 씨앗(Lv.1)에서 시작해서 총 10단계, 만렙은 100입니다. 

- 커밋 수가 곧 경험치입니다. 매일 코딩하면 나무가 자라야 합니다.

- 레벨 91이상이 되면 벚꽃이 흩날리는 애니메이션 

- README에 이미지 한 줄로 삽입 가능. (설치, 토큰 필요없이) 

 

마지막 조건이 핵심인데, 사용하기 어려우면 아무도 사용하지 않을 거 같아서  ![](URL) 하나면 끝나야 합니다. 

 


 

기술 결정

 

깃허브 리드미에 넣을 때 CDN 이미지를 사용하면 스타일이 깨지거나 캐시 이슈가 생기는 경우가 많았습니다. 

반면 SVG는 텍스트 기반이라 서버에서 동적으로 생성해서 그대로 응답하면 되는 구조입니다.

외부 이미지 의존도 없고, 폰트도 시스템 폰트 fallback으로 처리하면 어디서든 일관되게 보입니다. 

 

특히 CSS 애니메이션(@keyframes)을 SVG내부에 <style>태그로 박아넣을 수 있다는게 결정적입니다. 

벚꽃이 흩날리는 효과를 추가 JS없이 순수하게 구현할 수 있었습니다. 

 


 

레벨 공식

 

커밋 수를 레벨로 환산하는 공식이 중요했습니다. 

너무 빨리 만렙에 도달하거나, 반대로 수천 커밋을 해도 낮은 레벨에 머물면 재미가 없습니다.

 

여러 공식을 시뮬레이션 해봤는데, 결국 이거로 정했습니다.

 

// lib/level.ts
export function calculateLevel(totalContributions: number): number {
  if (totalContributions <= 0) return 1
  return Math.min(Math.max(Math.floor(Math.sqrt(totalContributions * 2)), 1), 100)
}

 

직관적으로 어떤 느김인지 표로 보면 

커밋 수 레벨 단계
0-1 1 씨앗
~50 10 새싹
~450 30 묘목
~1,300 50 꽃봉오리
~3,200 80 만개
~5,000 100 완전만개

 

루트 함수의 특성 덕분에 초반에는 레벨이 빠르게 오르고, 고레벨로 갈수록 커밋이 더 많이 필요해집니다.

RPG 경험치 곡선과 유사한 느낌입니다.

 

레벨 내 진행도(progress bar)는 현재 레벨의 커밋 구간 내 위치를 구합니다.

 

export function getLevelProgress(level: number, totalContributions: number): number {
  if (level >= 100) return 1
  const currentFloor = (level * level) / 2
  const nextFloor = ((level + 1) * (level + 1)) / 2
  const progress = (totalContributions - currentFloor) / (nextFloor - currentFloor)
  return Math.min(Math.max(progress, 0), 1)
}

 


 

GitHub GraphQL API로 전체 기간 커밋 합산

 

REST API 로는 최근 1년 커밋밖에 못가져옵니다. 5년 넘게 깃허브를 써온 유저라면 이게 치명적입니다. 

GraphQL API를 사용하면 연도별 기여 데이터를 모두 합산할 수 있습니다.

 

// lib/github.ts
export async function getTotalContributions(username: string): Promise<number> {
  // 1단계: 유저가 활동한 연도 목록 가져오기
  const yearsResult = await graphql(
    `query($login: String!) {
      user(login: $login) {
        contributionsCollection { contributionYears }
      }
    }`,
    { login: username }
  )

  const years = yearsResult.data?.user?.contributionsCollection?.contributionYears ?? []

  // 2단계: 각 연도별 커밋 수를 병렬로 fetch
  const yearResults = await Promise.all(
    years.map((year) =>
      graphql(
        `query($login: String!, $from: DateTime!, $to: DateTime!) {
          user(login: $login) {
            contributionsCollection(from: $from, to: $to) {
              totalCommitContributions
            }
          }
        }`,
        {
          login: username,
          from: `${year}-01-01T00:00:00Z`,
          to: `${year}-12-31T23:59:59Z`,
        }
      )
    )
  )

  return yearResults.reduce(
    (sum, r) => sum + (r.data?.user?.contributionsCollection?.totalCommitContributions ?? 0),
    0
  )
}

 

Promise.all로 병렬 요청을 날리기 때문에 10년치 데이터도 빠르게 처리됩니다.

그리고 Next.js의 fetch에 next: {revalidate: 3600} 옵션을 붙여 1시간 단위로 캐시합니다.

같은 유저 요청이 반복되어도 GitHub API를 매번 때리지 않습니다. 

 


 

SVG 생성 구조 

 

나무는 stage 1~10으로 나뉘고 레벨 10마다 stage가 하나씩 올라가는 방식입니다.

// 스테이지별 설정값
const CONFIGS: Record<number, StageConfig> = {
  1:  { trunkW: 0,  trunkH: 0,   branches: 0, clusters: 0,  glow: false, petals: false, seedStage: true },
  // ...
  9:  { trunkW: 21, trunkH: 135, branches: 6, clusters: 17, glow: true,  petals: false },
  10: { trunkW: 22, trunkH: 140, branches: 7, clusters: 20, glow: true,  petals: true  },
}

 

각 스테이지 설정에 따라 줄기 두께/높이, 가지 수, 벚꽃 클러스터 수와 반지름이 달라집니다.

Stage 9부터는 발광 효과 feGaussianBlur 기반 SVG filter, 

Stage 10은 떨어지는 꽃잎 애니메이션이 추가됩니다. 

 


 

벚꽃 흩날리기 

 

이 부분이 가장 핵심이였습니다. 

function fallingPetals(w: number): string {
  const petals = [
    { x: 72,  delay: 0.0, dur: 4.2 },
    { x: 138, delay: 0.9, dur: 5.1 },
    // ... 총 15개 꽃잎
  ]

  const css = `@keyframes petal-fall {
    0%   { opacity: 1; transform: translateY(0px) rotate(0deg) translateX(0px); }
    25%  { transform: translateY(55px) rotate(120deg) translateX(-12px); }
    50%  { transform: translateY(110px) rotate(240deg) translateX(8px); }
    75%  { transform: translateY(165px) rotate(360deg) translateX(-6px); }
    100% { opacity: 0; transform: translateY(220px) rotate(540deg) translateX(4px); }
  }`

 

CSS 애니메이션을 SVG <style> 태그에 직접 임베드합니다.

각 꽃잎마다 animation-delay와 animation-duration을 다르게 줘서 동시에 뚝뚝 떨어지는 것처럼 보이지 않게 했습니다.

translateX로 좌우 흔들림도 추가했더니 자연스럽게 흩날리는 느낌이 살았습니다.

 


 

결과물

 

![LevelUp Tree — 완전만개 (Lv.100)](./public/preview/stage-lv100.svg)
*Lv.100 완전만개 — 벚꽃이 흩날리는 최종 단계*

| 씨앗 (Lv.1) | 성장 (Lv.50) | 완전만개 (Lv.100) |
|:-----------:|:-----------:|:----------------:|
| ![Stage Lv.5](./public/preview/stage-lv5.svg) | ![Stage Lv.45](./public/preview/stage-lv45.svg) | ![Stage Lv.100](./public/preview/stage-lv100.svg) |

 

깃허브 리드미에 추가하는 방법은 정말 한 줄입니다. 

 

[![LevelUp Tree](https://leveluptree.vercel.app/api/tree?username=YOUR_USERNAME)](https://leveluptree.vercel.app)

 

YOUR_USERNAME을 본인 깃허브 아이디로 바꾸면 끝입니다. 토큰 발급도 없고, 워크플로우 설정도 없습니다. 

 


 

마무리

 

완성하고 나서 내 README에 붙여봤는데 생각보다 만족스럽진 않지만 마음에 든다. 

아직 Lv 50대라 어린 나무 수준이지만 커밋을 쌓으면 언젠가 Lv 100 만개 상태를 볼 수 있겠지 하는 소소한 동기부여가 생긴다. 

 

사이드 플젝은 일주일이면 충분히 만들 수 있고, 이렇게 블로그 포스트 하나 쓸 거리고 생겼다. 

무엇보다 내가 매일 쓰는 깃허브 프로필에 직접 붙어있다는게 가장 큰 보람이다. 

 

코드는 깃허브에 올라가 있으니 한번씩 관심을 가져주시면 좋겠습니다. 👨🏻‍💻

 


 

링크

 

- 데모 : https://leveluptree.vercel.app/

 

Create Next App

🌸 LevelUp Tree 커밋할수록 성장하는 벚꽃나무를 GitHub 프로필에 추가하세요

leveluptree.vercel.app

- GitHub : https://github.com/changhui98/leveluptree

 

GitHub - changhui98/leveluptree: GitHub 커밋을 나무 성장으로 시각화하는 SVG 배지 🌱

GitHub 커밋을 나무 성장으로 시각화하는 SVG 배지 🌱. Contribute to changhui98/leveluptree development by creating an account on GitHub.

github.com

728x90
반응형