SYDESYDE
SYDESYDE
  • 피드
  • 쇼케이스
  • 블로그
  • 모임
SYDESYDE© 2026 SYDE. All rights reserved.
SYDE 소개문의하기
Kakao
SYDE 오픈채팅
Instagram
Instagram
Threads
Threads
커뮤니티 가이드라인협업문의이용약관개인정보처리방침
커뮤니티 가이드라인협업문의이용약관개인정보처리방침

© 2026 SYDE. All rights reserved.

내가 만든 식물이 안 자랐다

내가 만든 식물이 안 자랐다

l서연· 7일 전· 해프닝·회고
목차
  • 숫자랑 막대가 서로 다른 말을 하고 있었다
  • 세이브 파일이 어제에 멈춰 있었다
  • 저장할지 말지를 엉뚱한 데서 정하고 있었다
  • 그리고 하나가 더 있었다
  • 그날 로컬 토큰은 65만 개였다
  • 남은 것

목차

  • 숫자랑 막대가 서로 다른 말을 하고 있었다
  • 세이브 파일이 어제에 멈춰 있었다
  • 저장할지 말지를 엉뚱한 데서 정하고 있었다
  • 그리고 하나가 더 있었다
  • 그날 로컬 토큰은 65만 개였다
  • 남은 것

TokenPlant라는 걸 만들었습니다. 맥 메뉴바에 떠 있으면서 Claude Code나 Codex로 쓴 토큰을 물로 바꿔서 식물을 키워주는 앱이에요. 아무것도 안 눌러도 개발하다 보면 알아서 자랍니다. 다 자라면 정원으로 옮겨 심어지고요.


모이통이나 물받이를 놓으면 새가 날아와서 앉습니다.

그런데 어느 날 제 화분이 안 자랐습니다.



숫자는 68%쯤 찬 것처럼 보이는데 막대는 거의 비어 있습니다.

어제도 322,550. 오늘도 322,550. 한 자리도 안 움직였어요. 하루 종일 작업했는데요.

내가 만든 앱인데 왜 안 자라는지 모르겠더라고요.


숫자랑 막대가 서로 다른 말을 하고 있었다

일단 숫자부터 이상했습니다. 322,550 / 476,892면 68%쯤 찬 건데, 바로 밑의 막대는 10%쯤밖에 안 차 있었어요. 둘 중 하나는 틀린 거잖아요.

역산해봤습니다.


값

이식까지 총량

772,295 mL

Lv.8 만개 문턱

303,125 mL

Lv.9 열매 문턱

476,892 mL

지금 부은 물

322,550 mL

둘 다 맞는 숫자였습니다.

  • 왼쪽 322,550은 지금까지 부은 물 전체

  • 오른쪽 476,892는 다음 단계 문턱

  • 막대는 이번 단계 안에서의 진행도 — Lv.8에 막 들어왔으니 11%

각자는 다 맞는데, /로 이어 붙여놓으니 분수로 읽혀서 "68% 찼다"로 보였던 거예요. 그 옆에서 막대는 11%를 가리키고 있었고요. 두 개가 정반대를 말하고 있으니 "안 차고 있다"고 읽을 수밖에 없었습니다.

/를 없앴습니다.

/ 를 없애고 오른쪽을 「다음까지 남은 양」으로 바꿨습니다. (며칠 지난 화면이라 숫자는 더 자란 상태예요.)

분수로 안 보이면 오해할 여지가 없죠. 누적은 이 앱의 보람이라 남기고, 오른쪽은 문턱 대신 남은 양을 말하게 했습니다.

뿌듯하게 빌드하고 다시 열었습니다. 여전히 322,550이었습니다.


세이브 파일이 어제에 멈춰 있었다

화면 문제가 아니었던 겁니다. 세이브 파일을 직접 열어봤어요.

세이브 파일 수정 시각:  9월 23일 15:06   ← 오늘은 9월 24일
lastDate:              '2026-09-23'
pot.water:             322,550

앱은 멀쩡히 떠 있었습니다. 프로세스도 살아 있고요. 그런데 어제 오후 3시 이후로 파일에 아무것도 안 썼습니다. 하루가 통째로 날아간 거예요.

그 사이에 저는 그 앱을 계속 보고 있었고, 숫자가 그럴듯하게 떠 있으니까 "느린가 보다" 하고 넘겼습니다. 고장난 걸 몰랐어요.

뭐가 문제인지 보려고 유입 처리 쪽부터 뜯었습니다.

저장할지 말지를 엉뚱한 데서 정하고 있었다

if save.rawWallet != walletBefore || save.rawEarnedTotal != rawBefore {
    persist()
}

"지갑이 변했을 때만 저장한다." 합리적으로 보이죠. 쓸데없는 디스크 쓰기를 줄이려던 거였습니다.

그런데 유입 처리 함수는 지갑 말고 날짜도 바꿉니다. 그날 읽힌 사용량이 0이면 지갑은 그대로인데 lastDate는 오늘로 넘어가요. 그럼 저장이 안 되고, 파일엔 어제 날짜가 그대로 남습니다. 앱을 껐다 켜면 어제 날짜를 다시 읽어서 같은 일을 또 하고요. 무한히.

고치면서 생각이 좀 바뀌었습니다. 무엇이 바뀌었는지를 호출부가 판단하게 두면 이런 구멍이 계속 생겨요. 오늘은 지갑을 빼먹었고, 내일은 다른 걸 빼먹겠죠. 그래서 판단을 저장 함수 안으로 옮겼습니다.

private func persist() {
    guard let data = try? JSONEncoder().encode(save) else { return }
    guard data != lastWritten else { return }   // 바뀐 게 없으면 안 쓴다
    try? data.write(to: url, options: .atomic)
    lastWritten = data
}

호출부는 이제 마음껏 부르면 됩니다. 바뀐 게 없으면 여기서 알아서 안 써요.

그리고 하나가 더 있었다

30초마다 도는 갱신 함수가 이렇게 생겼었습니다.

func refresh() {
    guard !isRefreshing else { return }
    isRefreshing = true

    Task {
        ... 로그 읽고 물 적립하고 ...

        let limits = await LimitsReader.read()   // ← codex 프로세스를 띄운다
        ...

        isRefreshing = false                      // ← 여기서만 풀린다
    }
}

한도 조회가 codex app-server라는 외부 프로세스를 띄웁니다. 그게 한 번 안 끝나면 그 뒤의 isRefreshing = false까지 막혀요. 그럼 이후 모든 갱신이 첫 줄에서 되돌아갑니다. 30초 타이머도, 새로고침 버튼도 전부 무력화됩니다.

웃긴 건 제가 그 바로 위에 이렇게 주석을 달아놨다는 거예요.

한도 조회는 물 적립과 분리해 뒤에 돌린다. 실패가 흔하고 느리다 — 여기에 성장을 묶으면 한도를 못 읽는 날 화분이 멈춘다.

알고 있었는데, 같은 Task 안에 순서대로 넣어뒀습니다. 그건 분리한 게 아니라 묶은 거죠.

세 가지를 고쳤습니다. 한도 조회를 진짜 별도 Task로 떼고, defer로 어떤 경로로 빠져나가도 래치가 풀리게 하고, 90초 넘게 붙잡혀 있으면 놓아주는 감시견을 붙였습니다. 진짜 멈춘 await는 defer로도 못 푸니까요.


그날 로컬 토큰은 65만 개였다

고치고 다시 돌렸습니다. 이번엔 제대로 돌았어요.

lastDate:    2026-09-24    ← 날짜 넘어감
pot.water:   322,926       ← +376 mL
rawWallet:   +651,838

장부도 맞고 물도 들어왔습니다. 그런데 저 숫자를 보고 좀 멍했어요.

651,838. 그게 그날 제 로컬 토큰 전부였습니다. 65만 개요. 하루 종일 작업했는데.

TokenPlant은 ~/.claude/projects에 쌓이는 로컬 Claude Code 로그를 읽습니다. 그런데 저는 그날 웹에서 작업하고 있었어요. 그 토큰은 제 맥에 한 줄도 안 남습니다.

과거 기록을 보니 처음부터 티가 나 있었습니다.

2026-09-17   788,678,952
2026-09-18   491,616,038
2026-09-21   395,870,952
2026-09-22   359,832,775
2026-09-23    23,037,010   ← 하루 종일 작업한 날

7억 8천만 개를 쓴 날이 있고, 2천 3백만 개인 날이 있습니다. 차이는 그날 터미널에서 claude를 직접 돌렸는지 여부였어요.

버그 두 개는 진짜 버그였습니다. 고쳐야 했고 고쳤어요. 그런데 제가 처음에 이상하다고 느낀 그 현상은 버그가 아니었습니다. 앱은 제대로 읽고 있었고, 읽을 게 없었던 겁니다.


남은 것

버그 자체보다 하루 동안 몰랐다는 게 더 걸렸습니다.

앱은 하루 동안 죽어 있었는데 화면은 멀쩡했어요. 마지막 숫자를 그대로 띄우고 있었으니까요. "안 보고 있어도 자란다"가 이 앱의 전부인데, 그 말은 고장난 걸 알아챌 방법이 없다는 뜻이기도 했습니다.

그래서 몇 가지를 바꿨습니다.

  • 마지막 갱신 시각이 5분 넘으면 주황색으로 뜹니다. 그 전엔 흐린 회색으로 "1523분 전"이라고 적고 있었어요. 읽을 수 없는 숫자는 경고가 아니죠.

  • 빈 상태 문구를 바꿨습니다. "터미널에서 Claude Code · Codex를 쓰면 저절로 자라요. 웹·데스크톱 앱 사용량은 맥에 기록이 안 남아서 셀 수 없어요." 저도 몰랐던 걸 사용자가 알 리가 없으니까요.

  • 검사기에 규칙을 넣었습니다. 한도 조회가 성장 경로에 다시 묶이면, defer가 빠지면, 감시견이 없어지면 잡힙니다. 전부 일부러 망가뜨려서 잡히는지 확인했고요.

이 중에 제일 값어치 있는 건 마지막일 거예요. 두 번까지는 제 탓인데, 세 번째부터는 검사기를 안 만든 탓인 것 같더라고요.

TokenPlant은 여기 있습니다 — https://github.com/seoyeon1123/tokenplant

  • #사이드프로젝트
  • #tokenplant
18
서
서연

댓글 0

My avatar