GitHub README에서 움직이는 이미지를 만드는 법: JS 없이 인터랙션을 구현하는 방법
GitHub README에서 움직이는 이미지를 넣고 싶은데, JS도 CSS도 안 된다면?
처음에는 정적 이미지로 타협했습니다. 하지만 gitanimals 프로젝트를 보고, “움직임은 실행이 아니라 인지의 문제”라는 사실을 깨달았습니다.
본 글에서는 GitHub README의 제약 안에서 애니메이션을 구현하기 위해 GIF, SVG, APNG를 비교하고, 최종적으로 APNG를 선택한 판단 과정을 정리합니다.
문제 상황
저는 git style이라는 오픈소스를 운영하고 있습니다.
GitHub README에 사용자의 커밋 기록을 시각화해주는 도구로,
단순한 수치나 정적인 히트맵이 아니라 하나의 장면처럼 보이게 만드는 것이 목표였습니다.
제가 그리고 싶었던 이미지는 예를 들면 이런 것들이었습니다.
- 꽃과 풀이 바람에 흔들리는 정원 컨셉
- 머리카락이 흔들리는 탈모 방지 컨셉
- …
즉, “데이터 시각화”라기보다는 살아 있는 상태를 암시하는 표현에 가까웠습니다.
하지만 여기에는 명확한 제약이 있었습니다.
GitHub README는 웹 페이지가 아니라 Markdown 렌더러입니다.
GitHub Community 논의에 따르면, 보안상의 이유로 다음과 같은 제약이 존재합니다.
- JavaScript 실행 불가 (SVG 내부 스크립트 포함)
<embed>,<object>태그 차단- SVG sanitizer가 일부 CSS 속성을 제거
- 결국
<img>태그를 통한 정적 이미지만 허용
이 제약 안에서 “동적으로 보이는 무언가”를 만들어야 했습니다.
내가 처음 한 생각
초기 MVP 단계에서는 이 제약을 그대로 받아들였습니다.
서버에서 정적인 PNG 이미지를 생성하고, 이를 README에 삽입하는 방식으로 구현했습니다.

이 방식은 빠르게 결과를 확인하기에는 충분했지만,
제가 처음에 그리고 있던 “살아 있는 장면”과는 거리가 있었습니다.
실제로 해 본 선택
가장 먼저 참고한 오픈소스는 gitanimals였습니다.
이 프로젝트는 GitHub README라는 동일한 제약 안에서 “움직이는 것처럼 보이는 이미지”를 이미 구현하고 있었습니다.
gitanimals의 핵심 접근 방식은 다음과 같았습니다.
- README에서 실행 가능한 코드는 없다는 점을 전제로 함
- 대신 서버(
render.gitanimals.org)에서 결과물을 생성 - 결과물 자체를 SVG 애니메이션으로 제공
여기서 중요한 인사이트를 얻었습니다.
README에서의 인터랙션은 실제 인터랙션일 필요가 없다.
사용자가 ‘움직인다’고 인지하면 된다.
이 판단을 기준으로, 동적 이미지 포맷들을 비교하기 시작했습니다.
포맷 비교
후보 1: GIF
| 장점 | 단점 |
|---|---|
| GitHub에서 완벽하게 지원 | 색상 256개 제한 |
| 구현 사례가 많음 | 용량 대비 품질이 떨어짐 |
| 투명도가 on/off만 가능 (반투명 불가) |
후보 2: SVG 애니메이션
| 장점 | 단점 |
|---|---|
| 벡터 기반, 해상도 독립적 | GitHub sanitizer가 일부 속성 제거 |
| SMIL/CSS 애니메이션 가능 | 동작이 불안정한 케이스 존재 |
gitanimals가 SVG를 선택한 이유는 펫 캐릭터가 벡터 기반이기 때문입니다.
하지만 제 경우 커밋 히트맵은 래스터 기반 렌더링이 더 적합했습니다.
후보 3: APNG (Animated PNG) ← 최종 선택
| 장점 | 단점 |
|---|---|
| PNG와 완벽히 호환 | 인코딩 관련 자료가 적음 |
| 24비트 컬러 + 8비트 알파 채널 | 구현 난이도가 GIF보다 높음 |
| GitHub Markdown에서 재생 지원 |
Mozilla APNG 스펙에 따르면, APNG는 기존 PNG와 역호환됩니다.
APNG를 인식하지 못하는 디코더는 첫 프레임만 정적 이미지로 표시합니다.
GitHub Community 논의에서도 확인했듯이,
GitHub은 APNG를 지원하지만 확장자가 .png여야 합니다. (.apng는 동작하지 않음)
구현 방식
// lib/theme/generator/generate-plant-apng.ts
export async function generatePlantAPNG(
username: string,
quality: AnimationConfig["quality"] = "low",
): Promise<Uint8Array> {
const frames: APNGFrame[] = [];
const { frameCount, frameDelay } = QUALITY_PRESETS[quality];
const weeks = chunkIntoWeeks(await fetchContributions(username));
for (let i = 0; i < frameCount; i++) {
const canvas = createCanvas(width, height);
const ctx = canvas.getContext("2d");
renderPlantFrame({
canvas,
ctx,
elements: createPlantElements(weeks, getCommitLevel),
frameIndex: i,
totalFrames: frameCount,
windEffect: getWindEffect(width), // 바람 효과
username,
});
frames.push({ data: canvas.toBuffer("image/png"), width, height });
}
return encodeAPNG(frames, frameDelay);
}
핵심 구조는 다음과 같습니다.
- @napi-rs/canvas로 프레임 단위 PNG 렌더링
- WindEffect로 잔디가 흔들리는 효과 적용
- upng-js로 PNG 프레임들을 APNG로 인코딩
- /api/[username]/animation.ts 엔드포인트에서 결과 제공
중요한 점은, 이 구현이 “README에서 인터랙션을 실행한다”가 아니라 “서버에서 인터랙션의 결과를 만들어서 전달한다”는 구조라는 점입니다.
결과

제가 원하는 인터랙티브한 애니메이션이 구현되었습니다. https://github.com/anonymousRecords
비고
SVG (Vector) vs APNG (Raster)
| 구분 | SVG (Vector) | APNG (Raster) |
|---|---|---|
| 방식 | 코드로 그리는 그림 | 픽셀로 구성된 이미지 |
| 특징 | 캐릭터 등 단순한 형태에 유리 | ”질감, 그림자 등 복잡한 묘사에 유리” |
| 안전성 | GitHub 필터링으로 깨질 가능성 있음 | 이미지라 원본 그대로 노출됨 |
참고 자료
- https://github.com/git-goods/gitanimals - GitHub README 애니메이션 구현 참고
- https://wiki.mozilla.org/APNG_Specification - APNG 공식 스펙
- https://github.com/orgs/community/discussions/127623 - .png 확장자 필수
- https://github.com/orgs/community/discussions/151372 - 보안상 제약 설명
- https://github.com/github/markup/issues/1160 - SVG sanitizer 이슈