DEV Community

김이더
김이더

Posted on

내 AI 코딩 연말결산을 직접 만들었다 — Memradar Code Report

앱은 memradar.vercel.app에서, 코드는 GitHub에 있다.
더 많은 글은 radarlog.kr에서.


매년 연말이 되면 Cursor가 "올해 너는 몇 시간 코딩했고, 어떤 언어를 얼마나 썼고" 하는 회고를 띄운다. GitHub도 Year in Review를 낸다. Discord도 프로필에 연간 통계가 뜬다.

숫자는 내 것인데, 페이지를 넘기다 보면 "아 나 이랬구나" 하고 웃게 된다. 이게 연말결산의 힘이다. 정보 전달이 아니라 한 장면에 한 숫자씩 꺼내놓는 의식이다.

나도 그게 필요했다. Claude Code와 Codex가 내 홈 디렉터리(~/.claude/projects/, ~/.codex/sessions/)에 매일 JSONL 로그를 쌓는데, 정작 그 안을 들여다본 적이 없다. 내 대화가 내 디스크에 있는데, 내가 못 읽는다.

Memradar는 그 JSONL을 회고로 바꿔주는 로컬 툴이다. npx memradar 한 줄이면 브라우저가 뜨고, 대시보드와 함께 풀스크린 슬라이드 회고 — Code Report가 같이 들어있다.

이 글은 Memradar를 만들면서 집요하게 매달린 디테일에 대한 기록이다. 아이디어는 단순하지만, 사람이 "재밌다"고 느끼게 만드는 데는 결정 하나하나가 다 필요했다.

Wrapped가 아니라 Code Report여야 했다

처음 풀스크린 회고 기능을 만들면서 내부적으로 "Wrapped"라고 불렀다. 코드 폴더 이름도 src/components/wrapped/로 시작했다. Spotify Wrapped가 레퍼런스였으니까 자연스럽게 그렇게 됐다.

근데 제품 이름으로 Wrapped를 그대로 쓰면 문제가 있다. 남의 브랜드 용어를 빌려 쓰는 셈이다. 또 Wrapped라는 말은 "연말결산"이라는 범주로만 읽힌다.

이름을 다시 잡으면서 docs/UI-UX-PRINCIPLES.md에 이런 원칙을 못 박았다.

9. 대시보드와 Code Report는 같은 제품의 다른 무드다
- 대시보드는 정보 탐색, Code Report는 감정적 회고와 공유를 담당한다.
- Code Report는 독립 팔레트와 전용 타이포,
  풀스크린 내러티브를 사용한다.
Enter fullscreen mode Exit fullscreen mode

같은 데이터를 다루지만, 대시보드는 "분석 툴"처럼 침착해야 하고 Code Report는 "회고 경험"처럼 감정선을 강화해도 된다고 적었다. 두 화면이 무드에서 섞이지 않도록 팔레트와 타이포그래피를 분리했다.

그래서 이름이 Code Report가 됐다. 코드(내 AI 코딩 로그)에 대한 리포트. 연말결산이라는 고정된 시점이 아니라 언제 열어도 회고가 되는 화면이라는 걸 이름에 담고 싶었다. Wrapped는 1년에 한 번이지만, Code Report는 내가 필요할 때마다 열 수 있다.

코드상의 폴더명(wrapped/)은 그대로 뒀다. 내부 변수명까지 갈아엎는 건 리팩터링 리스크가 크고, 외부에 노출되는 제품 이름만 Code Report로 고정했다. 이 구분 자체가 "같은 기능의 안과 밖을 다르게 부를 수 있다"는 작은 교훈이다.

왜 대시보드만으로는 부족했나

당연히 대시보드부터 만들었다. 히트맵, 시간대별 차트, 워드클라우드, 세션 브라우저. 모든 지표가 한 화면에 다 있다.

완성하고 내 로그를 띄워봤다. 숫자가 다 있었다. 근데 아무 감정도 안 들었다.

이게 대시보드의 한계다. 정보는 많은데, 보는 사람은 "음 그렇구나" 하고 창을 닫는다. 같은 데이터로 Cursor 연말결산이 사람들을 웃게 만드는 건, 한 화면에 한 숫자만 올라와서다. 그 숫자를 둘러싼 공간이 다 비어있기 때문이다.

Code Report의 구성 원칙을 "한 장면 한 메시지"로 잡았다. docs/UI-UX-PRINCIPLES.md에 이렇게 적어뒀다.

- 여기서는 "대시보드 규칙"보다 "한 장면 한 메시지"가 더 중요하다.
- 풀스크린, 강한 여백, 큰 타이포,
  전용 다크 스토리 팔레트를 사용한다.
- 마지막은 공유 가능 이미지로 닫는다.
  즉, 감정선의 끝이 곧 행동 유도여야 한다.
Enter fullscreen mode Exit fullscreen mode

슬라이드는 여덟 장이다. 인트로에 첫 세션 날짜, 다음은 총 프롬프트 수, 자주 쓴 모델, 코딩 시간대, 자주 부른 툴 순위, 성격 유형, 사용량, 마지막에 공유 카드.

각 슬라이드엔 숫자 하나 또는 메시지 하나만 올라간다. "내가 Claude를 3,200번 불렀다"는 걸 대시보드 구석에서 숫자로 보는 것과, 빈 슬라이드에 3,200까지 카운트업으로 올라오는 걸 보는 건 완전히 다르다. 같은 데이터인데.

이게 첫 번째 집착이었다. 숫자를 어떻게 "느끼게" 만들까.

성격 유형을 4개에서 8개로 갈아엎은 이유

Code Report의 하이라이트는 성격 유형 슬라이드다. Architect, Speed Runner, Explorer, Night Sage. 네 개로 시작했다.

근데 이틀 돌려보니 전부 "어 나 저거 아닌데" 소리가 나왔다. MBTI가 재밌는 건 네 축이 각각 독립적으로 계산되기 때문이다. "I이면서 N이면서 F이면서 P"라서 INFP가 나온다. 축이 하나(4타입 중 하나)면 거칠 수밖에 없다.

그래서 3축 시스템으로 다시 짰다.

style × scope × rhythm
 ↓       ↓        ↓
꼼꼼함   깊이     속도
  8 조합
Enter fullscreen mode Exit fullscreen mode

이렇게 조합하면 "심해 잠수부", "번개 해결사", "카오스 크리에이터" 같은 이름이 나온다. 각 축이 따로 계산되니까 결과가 더 개인적으로 느껴진다.

이거 짜는 데 반나절 날렸다. 4타입이 틀린 게 아니라, 결과를 읽는 순간의 느낌이 약했다. 이게 Code Report에서 제일 중요한 지점이다. 숫자가 맞고 틀리고 전에, 읽는 사람이 "오 이거 나네" 하고 웃어야 한다.

성격 계산 로직도 실제 데이터로 계속 튜닝했다. 내 로그에 돌려봤을 때 나한테 맞는 결과가 나오는지, 친구 로그에 돌렸을 때 친구한테 맞는 결과가 나오는지. 임계값 하나를 바꾸면 결과가 흔들린다. 이걸 수십 번 맞췄다.

마지막 슬라이드에 2.5초를 박은 이야기

커밋 제목: Fix last slide dashboard prompt timing.

무슨 문제였냐면, 공유 카드 슬라이드에서 화면을 탭하면 "대시보드로 넘어가시겠습니까?" 모달이 뜨게 해뒀다. 근데 Code Report를 쭉 넘기던 손가락 리듬으로 마지막 슬라이드에서도 무심코 탭해서, 공유 카드를 보기도 전에 모달이 떠버린다.

대부분은 "괜찮지 뭐" 하고 넘어갈 디테일이다. 근데 한 번 걸리니까 계속 신경 쓰였다. 그래서 고쳤다.

useEffect(() => {
  setDashboardPromptReady(false)
  if (slideIndex !== lastSlideIndex) return

  const timer = window.setTimeout(() => {
    setDashboardPromptReady(true)
  }, 2500)

  return () => window.clearTimeout(timer)
}, [lastSlideIndex, slideIndex])
Enter fullscreen mode Exit fullscreen mode

마지막 슬라이드에 들어가면 2.5초 동안 dashboardPromptReady가 false다. 그동안 탭해도 모달은 안 뜬다. 심지어 커서도 default로 바꿨다. "아직 누를 수 없다"는 걸 마우스 커서로도 알려준다.

이거 쓰면서 스스로도 생각했다. 이게 필요한 최적화일까. 사용자는 눈치 못 챌 것 같은데.

근데 필요한 건 맞다. 게임에서 컷씬이 끝난 직후 입력을 몇 프레임 막는 거랑 똑같다. 플레이어가 "스킵 버튼 누르는 리듬"으로 다음 장면에서 뭔가를 잘못 누르는 걸 막는다. 이런 건 걸리면 기분이 나쁘고, 막아두면 아무도 모른다. 근데 모르는 게 맞다.

테마가 20개인 이유

Memradar에는 테마가 20개 있다. 배경 4종(Dark, AMOLED, Light, Warm), 액센트 색 5종(Indigo, Violet, Teal, Rose, Amber). 4 × 5 = 20.

왜 이렇게 많냐. 이것도 "느낌"의 문제다.

Code Report를 넘기는 경험은 사적이다. 내 코딩 로그를 내가 보는 거다. 남이 만든 다크 테마를 그냥 쓰는 게 아니라, 내 분위기를 내가 고르고 싶다. Cursor 연말결산이 보라색 하나로 고정되어 있는 것과 다르게, 내가 만드는 건 내가 고를 수 있어야 한다.

배경 네 개로 나눈 것도 계산된 거다. Dark는 기본, AMOLED는 OLED 화면용 진짜 검정, Light는 밝은 카페용, Warm은 저녁 무드. 상황이 다르면 고르는 게 다르다.

폰트도 Noto Sans KR + Noto Serif KR로 고정했다. 한글이 엄청 많이 들어가기 때문에, 한글이 예쁘게 나오는 게 첫째다. 영어 폰트 안 썼다. 내가 주로 한국어로 코딩해서 프롬프트도 한국어 비중이 높고, 그걸 읽는 Code Report도 한국어가 예뻐야 한다.

localStorage에 저장해서 다음번에도 고른 테마가 유지된다. 이건 당연한데, 당연한 걸 빼먹으면 바로 티난다.

히트맵 한 개에 들어간 결정들

GitHub 스타일 일별 활동 히트맵. 이 위젯 하나에 들어간 결정이 이 정도다.

첫째, 반응형 셀 크기. 창 너비에 따라 셀이 자동으로 커지고 작아진다. ResizeObserver로 컨테이너 크기를 추적하다가, 셀 크기를 다시 계산해서 리렌더한다. 처음엔 고정 크기로 뒀는데, 화면 작은 노트북에서 히트맵이 잘려서 다시 짰다.

둘째, 클릭해서 날짜 선택. 셀을 클릭하면 그날 세션 요약이 사이드에 뜬다. 처음엔 hover만 있었는데, hover는 모바일에서 안 되고, 클릭해서 "고정"할 방법이 있으면 더 자세히 볼 수 있다.

셋째, streak 카운터. "연속 며칠 코딩했는지." 이걸 어디에 넣을까 고민하다가 히트맵 옆 사이드바에 박았다. Duolingo처럼 게임화하려는 게 아니라, 히트맵 보면서 자연스럽게 "아 나 이번 달 15일 연속이네" 하고 알게 되는 정도.

넷째, 요일 패턴. 월화수목금토일 중 어느 요일에 가장 많이 코딩했는지 사이드에 작게. 이건 넣을까 말까 고민했는데, 내가 주말에 생각보다 많이 코딩한다는 걸 이걸 보고 알았다. 이런 게 Code Report의 맛이다. 몰랐던 내 패턴이 보이는 거.

위젯 하나에 결정이 네 개. 히트맵뿐 아니라 워드클라우드, 시간대별 차트, 토큰 차트 다 이렇게 만들어졌다.

DropZone wording을 몇 번 바꿨는지

가장 집요했던 건 랜딩 페이지 DropZone이다. 커밋 로그에 DropZone wording 수정만 네 번 있다.

- Replace copy-paste wording with Ctrl+C/V shortcut
- Use Ctrl+C/V/Enter wording consistently in DropZone
- Allow .claude/.codex root folder drop and add install guide link
- Fix DropZone wording: shorten Ctrl+C/V label
Enter fullscreen mode Exit fullscreen mode

하나씩 뜯어보면 이렇다. 처음엔 "복사해서 붙여넣기"였다. 그러다가 Ctrl+C / Ctrl+V로 바꿨다. 왜냐면 한국어로 "복사해서 붙여넣기"는 길고, 영어로 "copy and paste"도 장황하다. 숏컷 두 개 나란히 보여주면 0.5초 만에 "아 저거"가 꽂힌다.

두 번째 변경은 "일관성"이었다. 어떤 곳엔 "Ctrl+C/V" 있는데 다른 곳엔 "복사/붙여넣기"가 남아있었다. 용어가 섞이면 불편하다.

세 번째, .claude 폴더 자체 드롭 허용. 원래는 .claude/projects/ 안쪽을 드롭해야 파싱됐다. 근데 사용자 입장에선 .claude를 홈에서 드래그하는 게 자연스럽다. 그래서 드롭 핸들러가 .claude 안의 projects 자식을 자동으로 찾아 들어가도록 고쳤다.

if (droppedFolder.name === '.claude') {
  const projectsDir = findChild(droppedFolder, 'projects')
  if (projectsDir) return scanDir(projectsDir)
}
Enter fullscreen mode Exit fullscreen mode

네 번째는 다시 wording. "Ctrl+C/V" 레이블이 너무 길어서 앞에 붙어있던 중복 prefix를 뺐다.

같은 컴포넌트에서 표현 하나 가지고 네 번 고쳤다. 과하다 싶지만, DropZone은 사용자가 제일 먼저 보는 화면이다. 여기서 5초 안에 뭘 해야 하는지가 안 꽂히면, 나머지가 아무리 예뻐도 소용없다.

한영 동시 지원이 생각보다 큰 결정이었다

i18n (ko/en), theme presets, and hash routing. 한 커밋에 세 가지가 같이 들어갔다.

한영 동시 지원을 넣은 건 블로그를 한영 동시 발행하는 나 자신을 위해서다. 근데 더 큰 이유가 있다. Code Report에 들어가는 문구는 감정적이다. "너는 Night Owl이야", "이번 달에만 3,200번 대화했어" 같은 문장들. 이게 영어로만 있으면, 한국어 유저는 "재미"가 반감된다.

// src/i18n.tsx
const translations = {
  ko: {
    'personality.nightSage': '새벽의 현자',
    'personality.speedRunner': '번개 해결사',
    // ...
  },
  en: {
    'personality.nightSage': 'Night Sage',
    'personality.speedRunner': 'Lightning Fixer',
    // ...
  }
}
Enter fullscreen mode Exit fullscreen mode

Night Sage를 "밤의 현자"가 아니라 "새벽의 현자"로 번역한 것도, "Lightning Fixer"를 "번개 해결사"로 맞춘 것도, 그냥 직역이 아니라 한국어로 읽었을 때 같은 온도가 되도록 골랐다. 성격 유형 여덟 개 이름을 한영 각각 다시 쓰는 데 몇 시간 걸렸다.

해시 라우팅을 같이 넣은 이유는, Code Report 링크를 공유할 수 있게 만들려면 URL에 상태가 담겨야 하기 때문이다. #wrapped/5 같은 식으로 슬라이드 번호가 URL에 들어간다. 새로고침해도 돌아올 수 있다.

단일 HTML을 버린 순간

기술적인 결정 중에 제일 컸던 건 CLI 구조를 단일 HTML에서 로컬 서버로 바꾼 거다.

원래는 CLI가 JSONL을 다 파싱해서, JSON으로 직렬화하고, <script> 태그에 박아서 거대한 단일 HTML 파일을 만들었다. 오프라인에서도 돌고 서버도 필요 없고, 깔끔했다.

근데 내 .claude/projects/가 커지면서 그 HTML이 18MB를 넘겼다. 브라우저가 JSON.parse 하는 동안 4~5초 멈춘다. Code Report 시작하기도 전에 끊긴다.

그래서 로컬 서버(http.createServer로 3939 포트) + /api/sessions 스트리밍으로 뒤집었다. 브라우저는 0.6MB짜리 앱 번들만 먼저 받고, 세션은 10개씩 서버에서 당겨온다. 세션 본문은 클릭했을 때만.

이건 UE5에서 맵 전체 로딩 vs 레벨 스트리밍이랑 똑같다. 예전엔 월드 전체를 RAM에 올렸다. 지금 오픈월드는 플레이어 주변만 스트리밍으로 로드한다. 디스크엔 다 있는데, 메모리엔 필요한 만큼만. 정확히 그 이동이다.

단일 HTML 모드는 --static 플래그로 살려뒀다. 세션이 몇 개 없을 땐 여전히 단일 HTML이 제일 간단하다. 상황에 따라 고를 수 있게.

Memradar라는 이름, 그리고 한 줄

처음 이름은 Promptale이었다. 프롬프트 + 이야기. 감성적이긴 한데 뭐 하는 툴인지 안 읽힌다.

Memradar로 바꾸고 나니 의미가 명확해졌다. Mem(memory) + Radar. 내 기억(로그)을 레이더로 훑는다. 내 블로그(radarlog.kr)와도 엮여서 브랜드가 이어진다. 그리고 그 안에 Code Report라는 화면이 들어있다. 제품명과 기능명이 각자 자기 역할을 한다.

로컬에서 돌려보고 싶으면 한 줄이면 된다.

npx memradar
Enter fullscreen mode Exit fullscreen mode

자동으로 ~/.claude/projects/~/.codex/sessions/를 스캔하고, 브라우저에서 대시보드를 띄워준다. 거기서 Code Report를 열면 풀스크린 회고가 시작된다. 데이터는 전부 로컬에서만 처리된다. 서버에 뭐 안 보낸다.

웹 버전도 있다. memradar.vercel.app에서 .claude 폴더를 직접 드래그해도 된다.

내 로그를 이걸로 처음 봤을 때 들었던 생각은, Claude한테 생각보다 훨씬 많이 말했다는 거였다. 그리고 새벽에 훨씬 집중했다는 것도. 숫자로 보고 나서야 "아 내가 이렇게 살고 있구나"가 와닿았다.

이게 Code Report의 목적이다. 내가 남긴 대화를, 내가 회고할 수 있게.

"내 로그는 내 디스크에 있었다. 내가 안 봤을 뿐이다."

Top comments (0)