[런칭스프린트] 지금고 개발

출발 의사결정 알람 앱 소개

매일 아침 신분당선으로 출근합니다. 밥 먹고·샤워하고·머리 말리고·옷 고르는 사이에 타야 할 열차는 이미 떠나 있고, 배차 간격이 길어서 하나 놓치면 10분 이상을 애매하게 기다립니다. 지도 앱으로 "다음 열차 07:42, 그다음 07:54"를 확인해도, 결국 "그럼 몇 분 안에 준비를 끝내야 하지?"를 제가 머릿속으로 계산해야 했죠. 이 계산을 매일 반복하는 게 진짜 피로였습니다.

그래서 만든 게 출발 의사결정 알람 앱 "지금고"입니다. 핵심은 하나예요.

기존 앱은 '정보'를 준다("다음 열차 07:42"). 나는 '결론'이 필요하다 — "지금 나가세요."

핵심 가치를 '정보 제공'이 아니라 '의사결정 대행'으로 잡았습니다. 사용자가 "언제까지 어디 도착"만 정해두면, 앱이 타야 할 열차를 역산해서 준비 단계별(10분·5분·0분 전)로 음성+풀스크린 알람을 주고, 출발 직전엔 실시간 도착정보로 시간을 보정합니다. 알람을 주니까 깜빡하지 않고 딱 맞춰 나갈 수 있다 — 이게 만든 이유의 전부입니다.

설계 원칙 3가지:

- Action over Information — 화면에서 가장 큰 건 항상 "지금 나가세요" 또는 출발 시각. 정보 나열 금지.

- Never Silent — 알람은 어떤 실패에도 침묵하지 않는다. TTS·풀스크린이 다 막혀도 기본 알람음은 무조건 울린다.

- Explainable — 추천 시각엔 항상 근거를 붙인다("08:22 열차, 강남 도착 08:44, 여유 3분").

개발 방법론과 진행 방법

도구는 Claude Code(터미널에서 돌리는 Anthropic 공식 CLI)를 썼습니다. 코드만 짠 게 아니라 기획 → 문서 체계 → 설계 → 구현 → 검증 → 기록 전 과정을 같이 굴렸어요. 1인 개발자라 Claude Code를 "짝 개발자 + 문서 담당 + QA"로 동시에 쓴 셈입니다. "코드부터 짜"가 아니라 매번 설계·계획을 먼저 시키고 구현은 그다음, 이 순서를 고정했습니다.

작업 순서

1) 기획을 시키고 PRD를 뽑게 함 — 코드가 아니라 "무엇을·왜"부터. 역산 공식, 알람 오프셋, 실시간 보정 우선순위, 성공 지표까지 문서로 합의.

2) "구조부터 설계해라" — 기능 구현 전에 문서 체계 + 스캐폴드부터 세팅(아래 트리).

3) 기능마다 같은 사이클 반복 — 설계(DESIGN) → 계획(PLAN) → 구현(TDD) → 리뷰 → 실기기 검증 → 기록(HANDOFF/MISTAKES).

docs 폴더 구조:

docs/

├─ product/ 제품이 무엇/왜 (PRD, STRATEGY) — 안정적 스펙

├─ engineering/ 어떻게 만드나 (ARCHITECTURE, RULES, CODE_STYLE) — 안정적 규칙

├─ journal/ 작업하며 쌓는 기록 (HANDOFF, PLAN, MISTAKES) — 살아있는 기록

└─ features/NN/ 기능 단위 (DESIGN → PLAN → NOTES)

실제로 쓴 핵심 프롬프트 3개

(공통 원칙: AI에게 일을 시키기 전에 '판단 기준'과 '산출물 형식'을 먼저 준다.)

[프롬프트 ① 기획 — 답을 받지 말고 먼저 반박당하기]

너는 통근 앱을 설계하는 PM이자 시니어 개발자야.

내 아이디어를 검증부터 해줘: "신분당선 통근자에게 '지금 나가세요' 한 문장만

주는 알람 앱." 바로 동의하지 말고, 이게 왜 안 팔릴지 · 기존 앱(네이버지도,

카카오지하철)으로 이미 해결되는지부터 반박해봐.

반박에서 살아남은 핵심 가치만 남겨서, 그걸 PRD로 정리해줘.

제약: 확실하지 않은 건 지어내지 말고 "열린 질문" 섹션으로 따로 빼둘 것.

▸ 의도 — 역할 부여 + "동의하지 마"로 예스맨 방지 → 아이디어를 먼저 스트레스 테스트. "모르는 건 지어내지 말고 열린 질문으로" → 환각 방지 + 리스크 가시화.

[프롬프트 ② 착수 — 설계와 계획을 갈라서 시킨다]

이제 알람 엔진(unit 02)에 착수하자. 코드 짜기 전에 두 단계로 나눠서:

먼저 DESIGN.md 하나 — 무엇을·왜·어떤 구조로. "무언가를 시작하는 기능이면

멈추는 수단도 같은 설계에" 넣어(풀스크린이 막히면? 소리도 막히면?).

설계가 OK되면 그때 PLAN.md — 테스트까지 포함해서 한 번에 리뷰 가능한

작은 단위로 쪼개줘. 한 태스크가 커지면 더 쪼개.

▸ 의도 — 설계 승인 → 계획 분리(2-step)로 큰 실수를 앞단에서 거른다. "시작하는 기능엔 멈추는 수단도 같이"를 설계 단계에서 강제 → 나중에 "알람이 안 꺼지는" 사고를 미리 막는다.

[프롬프트 ③ 구현 후 — 스스로 적대적으로 리뷰시키기]

방금 구현한 TrainResolver를 커밋하기 전에 네가 직접 리뷰해줘.

세 관점으로: (1) PRD 5.1 명세와 실제 코드가 어긋나는 곳 (2) 경계값 —

자정 넘김, 막차, 마감까지 열차가 없는 경우에 로직을 머릿속으로 실행해봐

(3) 이 파일 하나가 책임을 하나만 지는지.

문제를 못 찾겠으면 "왜 문제없는지" 근거를 대. 그냥 통과시키지 마.

▸ 의도 — 자기 리뷰를 '관점 3개 + 경계값 시뮬레이션'으로 구조화. "근거 없이 통과 금지"가 핵심 — 실제로 이 방식으로 자정 넘김 버그를 잡았다.

핵심 로직(열차 역산)은 Android 의존이 하나도 없는 순수 Kotlin으로 짜서 JVM에서 바로 테스트했습니다(테스트 20건 통과). 아래가 "마감 시각 → 탈 열차 → 집 출발 시각"을 역산하는 실제 코드입니다.

Open optionskotlin

  /**
   * PRD 5.1: 마감 전에 도착하는 가장 늦은 열차를 골라 집 출발 시각 D를 역산한다.
   * 제약: v1은 자정을 넘는 스케줄 미지원 (출근용 — 같은 날 안에서만 계산).
   */
  class TrainResolver {
      fun resolve(schedule: Schedule, timetable: Timetable, date: LocalDate): ResolveResult {
          val trains = timetable.trainsBetween(
              schedule.originStation, schedule.destStation, DayType.from(date),
          )
          if (trains.isEmpty()) return ResolveResult.NoTrain(lateByMin = 0, earliestTrain = null)

          // 조건: 하차 + egress + buffer ≤ deadline. LocalTime.plusMinutes는 자정에서 감싸 돌므로
          // (예: 23:58 + 7분 = 00:05) 감싸 도는 경우는 도메인 미지원인 자정 넘김이므로 후보에서 제외.
          val slackMin = (schedule.egressTimeMin + schedule.safetyBufferMin).toLong()
          val target = trains.lastOrNull {
              val arriveWithSlack = it.arriveAt.plusMinutes(slackMin)
              !arriveWithSlack.isBefore(it.arriveAt) && !arriveWithSlack.isAfter(schedule.deadline)
          }
              ?: return ResolveResult.NoTrain(
                  lateByMin = minutesBetween(schedule.deadline, trains.first().arriveAt.plusMinutes(schedule.egressTimeMin.toLong())),
                  earliestTrain = trains.first(),
              )

          val next = trains.firstOrNull { it.departAt.isAfter(target.departAt) }
          return ResolveResult.Success(
              train = target,
              departHomeAt = target.departAt.minusMinutes((schedule.accessTimeMin + schedule.paceAdjustMin).toLong()),
              nextTrainGapMin = next?.let { minutesBetween(target.departAt, it.departAt) },
              arriveAt = target.arriveAt.plusMinutes(schedule.egressTimeMin.toLong()),
          )
      }
  }

신분당선 공식 홈페이지의 "역별 시각표" PDF를 받아, 역별 게시 시각을 열차(run) 단위 전 구간 배열로 복원하는 파이썬 스크립트를 만들었어요. 원본에 실제로 인쇄된 시각만 쓰고 임의의 시각은 만들지 않는 게 규칙이었습니다.

[이미지 3장 — 어제(7/25) 지피터스 모임 가는 길 실제 화면]

한국 시간표 앱 스크린샷
한국어 GPS 앱 - 스크린샷
아이폰의 한국어 앱 스크린샷

결과와 배운 점

어제 지피터스 오프라인 모임 가는 날, 아직 스토어 배포 전이라 노트북에 폰을 연결해서 앱을 띄웠는데 — 실시간 데이터로 탈 열차를 잡아 알람을 줬고, 그 시간에 맞춰 나가서 열차를 탔습니다. 데모가 아니라 실제로 그 알람을 믿고 몸을 움직인 첫 순간이었어요.

개발 과정에서의 배운 점

1. 첫 프롬프트에 '왜'를 최대한 담아라. 문제의식·핵심 가치·판단 기준을 PRD로 먼저 합의하니 이후 지시가 짧아지고 방향이 안 흔들렸다. 코드를 시키기 전에 판단 기준을 먼저 주는 게 제일 중요.

2. 규칙 파일(CLAUDE.md)에 '최소 코드 + 실수 기록'을 박아라. 미리 추상화하는 걸 막고, 삽질을 그 자리에서 MISTAKES.md에 남기니 같은 실수를 두 번 안 하게 됐다.

3. '시작하는 기능'엔 '멈추는 수단'을 같은 단계에 넣어라.

4. 도메인 로직은 실기기 없이 도는 순수 코드로. JVM에서 즉시 테스트가 돌아 검증이 빠르다.

도움 받은 글 (옵션)

참고한 지피터스 글이나 외부 사례를 알려주세요.

(내용 입력)

1개의 답글
지피터스 뉴스레터

이번 주 AI, 딱 쓸 것만

매주 월요일 오후,
읽고 바로 써먹을 AI 소식만