2026.06.22 — Present
Loutine
매일 다른 컨디션을 코드로 다루는 법: Loutine 개발기
왜 Loutine을 만들었는가
대부분의 루틴 앱은 사용자가 매일 비슷한 상태일 것이라고 가정한다. 월요일에 할 수 있었던 30분 운동을 화요일에도 그대로 요구하고, 수행하지 못한 날은 빈칸이나 끊어진 스트릭으로 남긴다.
하지만 사람의 에너지는 그렇게 일정하지 않다. 어떤 날에는 30분을 쓸 수 있지만, 지친 날에는 매트 위에 서는 것조차 어렵다.
2026년 상반기 회고에서 적었듯, Loutine은 루틴을 지키기 어려운 사람의 문제에서 출발했다.
매일 같은 사람이 아니어도, 지금의 컨디션에 맞는 방식으로 루틴을 이어갈 수 없을까?
Loutine은 하나의 루틴을 고정된 행동으로 저장하지 않는다. 사용자가 이루고 싶은 의도를 Routine Pack으로 두고, 그 안에 컨디션별 Routine을 배치한다. 사용자는 현재 컨디션을 선택한 뒤 그 순간 실행할 수 있는 루틴을 시작한다.
이 모델은 단순한 기능 차이가 아니었다. 데이터 구조, 추천 시스템, 알림, 세션 상태와 UI까지 프로젝트 전체를 결정한 도메인 규칙이었다.
제품 전제를 코드로 옮기기
Loutine의 중심 모델은 Routine Pack이다. 하나의 Pack은 공통 실행 시각과 요일, 리마인더 설정을 가지며, 그 아래에 컨디션별 Routine이 존재한다.
예를 들어 “저녁 운동”이라는 의도는 하나지만 실제 행동은 컨디션에 따라 달라질 수 있다.
- 컨디션이 좋은 날에는 30분 운동
- 평범한 날에는 10분 스트레칭
- 완전히 지친 날에는 매트 위에 서서 몸을 한 번 펴기
사용자가 세션을 시작하면 Routine Attempt가 만들어진다. 완료 여부뿐 아니라 시작 당시의 컨디션과 완료 후 체감 난이도도 선택적으로 기록한다. 수행하지 못한 시도는 실패가 아니라 중단했거나 다시 돌아와야 하는 상태로 다룬다.
완료 기록도 스트릭이나 달력 점수로 표현하지 않는다. Routine Trace는 날짜별 성공 여부보다 작은 수행이 축적된 밀도를 보여준다. 미래와 비어 있는 날을 실패처럼 강조하지 않고, 실제로 남은 흔적만 시각화한다.
제품의 언어가 달라지자 코드의 상태 모델도 자연스럽게 달라졌다.
Loutine은 iOS 26.0을 대상으로 하는 SwiftUI·SwiftData 앱이다. 프로젝트 그래프는 Tuist로 관리하며 다음 경계로 나뉜다.
- Domain: Entity, UseCase, Repository Protocol
- Data: SwiftData Repository, AlarmKit·Notification Scheduler, HTTP Client
- Features: Home, Routine, Trace, Settings, Onboarding
- Shared: DesignSystem, Networking, Localization, RouteSupport, Services
- App: 의존성 주입과 앱 생명주기
각 모듈은 Interface와 Implementation 타깃을 구분한다. 화면은 UseCase만 호출하고 Domain은 SwiftUI나 SwiftData를 알지 못한다. 실제 구현은 FactoryKit을 통해 App Module에서 조립한다.
화면 전환에는 LinkNavigator의 타입화된 경로를 사용한다. AI 추천을 제외한 핵심 데이터와 세션 흐름은 로컬에서 동작하며, CloudKit 동기화도 현재는 의도적으로 비활성화했다.
네트워크나 AI가 응답하지 않더라도 루틴을 만들고 실행하는 핵심 경험은 유지해야 하기 때문이다.
AI에게 맡긴 것, 맡기지 않은 것
Loutine에서 AI 활용은 두 영역으로 나뉜다. 제품 안에서 AI의 권한은 제한했지만, 개발 과정에서는 AI를 적극적으로 활용했다.
제품 AI: 결정자가 아닌 초안 작성자
Loutine은 Gemini API를 두 가지 추천에 사용한다.
- 현재 컨디션에 맞는 Routine 초안 추천
- 루틴을 시작하기 쉽게 만드는 Routine Hook 추천
Routine Hook은 막연한 동기부여 문구가 아니다. “냉장고 문을 열면 물 한 잔을 꺼내기”처럼 예측 가능한 사건과 작은 시작 행동을 연결하는 규칙이다.
그러나 처음 루틴을 설정할 때 사용자는 어떤 Hook이 효과적인지 판단하기 어렵다. Gemini는 이 빈 입력 화면에서 편집 가능한 초안을 제안한다.
추천 요청에는 Routine Pack의 이름, 현재 컨디션, 실행 시각, 요일, 예상 수행 시간과 응답 언어가 포함된다. iOS 앱은 이를 전용 UseCase와 URLSession 기반 네트워크 계층을 통해 Fastify 서버로 전송한다.
SwiftUI
→ Recommendation UseCase
→ Domain Client Protocol
→ URLSession Transport
→ Fastify / Cloud Run
→ Gemini
→ JSON Schema 검증
→ 사용자의 검토와 명시적인 저장
Gemini 모델은 기본적으로 gemini-2.5-flash를 사용하며 환경 변수로 교체할 수 있다. 출력은 자유 텍스트가 아니라 1~3개의 후보를 담는 JSON Schema로 제한한다.
응답은 런타임에서 다시 검증한다. 이름이 비어 있거나 시간이 잘못된 후보는 통과시키지 않는다.
프롬프트도 코드처럼 버전 관리한다. Routine Hook 추천과 Condition Routine 추천은 각각 별도의 프롬프트 버전을 가진다. 사용자 입력은 명령이 아니라 신뢰할 수 없는 데이터로 취급하고, 정해진 JSON 이외의 출력은 허용하지 않는다.
백엔드 호출은 Firebase App Check로 보호한다. 실제 기기에서는 App Attest를 사용하고, 디버그와 시뮬레이터에서는 Debug Provider로 같은 요청 경로를 테스트한다. 추천은 하루 두 번으로 제한하되 실패한 요청은 사용량에서 되돌린다.
AI 추천은 바로 로컬에 저장되지 않는다. 사용자는 후보를 검토하고 수정하거나 무시할 수 있으며, AI가 실패해도 직접 입력할 수 있다. AI는 핵심 상태를 소유하거나 사용자의 결정을 대신하지 않는다.
개발 AI: Codex
개발 과정에서는 Codex를 요구사항 정리, 코드 탐색, 구현 계획, 테스트 작성, 리팩터링, Preview 구성과 PR 리뷰 검증에 활용했다. 다만 기능 전체를 AI에게 맡기지는 않았다.
저장소 안에 아키텍처, SwiftData, SwiftUI, 테스트와 로컬라이징 규칙을 문서화하고, AI가 작업 전에 관련 지침을 읽도록 했다. 이 과정은 Guidance 시리즈 1편과 2편에 정리했다.
코드 변경 후에는 실제 테스트가 실행됐는지, 잘못된 Scheme에서 0개의 테스트가 통과한 것은 아닌지까지 확인했다. 사람과 AI의 역할도 다음처럼 나누었다.
- AI는 호출 경로와 상태 흐름을 추적한다.
- AI는 작은 구현안과 테스트를 제안한다.
- 나는 제품 의도와 시각적 방향, 허용할 복잡도를 결정한다.
- 결과는 빌드, 테스트, Preview, 시뮬레이터 또는 실제 기기에서 검증한다.
AI 활용도는 높았지만 최종 판단까지 자동화하지는 않았다. 이 원칙은 앱 안의 추천 AI와 개발 과정의 AI 모두에 동일하게 적용됐다.
상태를 보여주는 UI
Loutine은 일반적인 밝고 둥근 습관 관리 앱과 다른 방향을 선택했다. 모노스페이스 타이포그래피, 터미널 코너, 문자 reveal, grain과 blur를 사용해 자신의 상태를 카메라로 바라보듯 관찰하는 인터페이스를 만들고 있다.
대표적인 컴포넌트는 LoutineGrid다. 11×11 그리드의 두 축으로 컨디션과 체감 난이도를 표현한다. 사용자가 드래그하면 점이 반응하고 햅틱이 발생한다. 단순한 5단계 Picker보다 상태의 미세한 차이를 직접 만지는 느낌을 주기 위한 UI다.
DesignSystem 모듈에는 다음과 같은 자체 구현이 포함되어 있다.
- Metal Shader 기반 grain effect
- 문자 단위 scramble·reveal 애니메이션
- 문맥에 따라 문장이 바뀌는 loading indicator
- Variable Blur와 inverse mask
- matched geometry 기반 inline popover
- 루틴과 리마인더 상태를 표현하는 Stack·Card·Toggle 컴포넌트
AI 추천 응답을 기다리는 화면에서도 Spinner 대신 “현재 컨디션을 확인하는 중”, “세부 정보를 살펴보는 중”과 같은 문장이 순차적으로 나타난다.
Reduce Motion이 활성화되면 반복 애니메이션을 줄이고, 장식 요소는 VoiceOver에서 제외한다. 시각적 개성만큼 접근성과 상태 전달도 DesignSystem의 책임으로 두었다.
UI 버그의 원인이 항상 애니메이션 코드는 아니었다. Pack Detail에서 컨디션별 Routine을 삭제하면 placeholder가 나타나야 했지만, 기존 구현은 자식 View를 제거한 뒤 새 View를 만들었다.
SwiftUI 입장에서는 하나의 상태가 변한 것이 아니라 서로 다른 View가 교체된 것이므로 기대한 transition이 나타나지 않았다.
해결 방법은 컨디션별 슬롯의 identity를 유지하는 것이었다. 모든 Routine Condition에 대응하는 안정적인 슬롯을 만들고, 값이 없으면 같은 슬롯이 placeholder를 표시하게 했다.
이 경험은 SwiftUI에서 애니메이션을 조정하기 전에 데이터와 View identity가 안정적인지 먼저 확인해야 한다는 교훈을 남겼다.
앱과 OS 상태를 함께 보존하기
Loutine에서 가장 어려운 기술 문제는 예쁜 UI보다 리마인더의 일관성이었다. 사용자가 알림을 Alarm으로 바꾸거나 실행 요일을 수정하면 여러 상태를 함께 갱신해야 한다.
- SwiftData에 저장된 Routine Pack
- 앱이 관리하는 Notification·Alarm 모델
- UNUserNotificationCenter에 예약된 알림
- AlarmKit에 예약된 실제 Alarm
- 화면에 표시되는 리마인더 상태
OS Scheduler와 SwiftData는 하나의 트랜잭션에 참여할 수 없다. 그래서 리마인더 변경을 보상 가능한 트랜잭션으로 설계했다.
현재 예약 스냅샷
→ OS 예약 교체
→ SwiftData 상태 커밋
→ 실패 시 새 예약 제거
→ 이전 예약 복원
AlarmKit의 최대 예약 개수에 도달한 상태에서는 새 Alarm을 먼저 만들 수 없다. 기존 슬롯을 해제한 뒤 교체를 시도하고, 실패하면 이전 Alarm을 다시 예약해야 한다.
단순한 CRUD로 보였던 설정 변경이 실제로는 여러 외부 상태를 조율하는 UseCase가 되었다.
AlarmKit 알람에 “루틴 시작” 액션을 추가하는 작업도 예상보다 깊었다. 버튼을 표시하는 것만으로는 충분하지 않았다.
App Intent가 Framework 안에 선언되어 있다면 Framework용 AppIntentsPackage와 이를 포함하는 App Module의 패키지가 모두 필요하다. 실제 앱 번들의 Metadata.appintents에 Intent가 추출됐는지도 확인해야 했다.
사용자가 Alarm 액션을 누를 때 앱이 종료된 상태일 수도 있다. 따라서 Alarm의 Intent는 다음 과정을 거쳐 동작한다.
- alarmID로 현재 Alarm을 정지한다.
- routinePackID를 pending request로 저장한다.
- 앱을 foreground로 전환한다.
- Navigator가 준비된 뒤 pending request를 소비한다.
- 해당 Pack의 Routine Session을 연다.
Intent 실행 시점과 Navigation 준비 시점을 분리해야 했다. NotificationCenter만 사용하면 cold launch에서 이벤트를 잃을 수 있어 pending 상태를 별도로 보존했다.
주도권을 사용자에게
AI 추천을 붙이는 것보다 추천을 안전하게 제한하고, 취소와 실패를 처리하며, 사용자 검토 단계를 넣는 작업이 더 많았다. 프롬프트 캐싱과 예상하지 못한 응답을 막는 Guardrail도 앞으로 개선해야 한다.
Loutine의 기술적 차별점은 사용자의 컨디션이 달라진다는 제품 전제를 데이터 모델, 세션 상태, AI 추천, 리마인더 트랜잭션, Trace 시각화와 UI에 끝까지 반영한 데 있다.
Loutine을 개발하며 가장 많이 고민한 질문은 “이 기능을 어떻게 구현할까?”가 아니었다. 개발 방향이 서비스의 핵심 메시지를 제대로 전달하는지를 판단하는 일이 더 중요했다.
이 기술이 사용자의 주도권을 빼앗지 않으면서, 지친 날에도 다시 시작할 수 있게 하는가?
앞으로 Routine Trace와 완료 후 신호를 추천에 활용하더라도 이 원칙은 유지하려 한다. AI는 더 많은 결정을 내리는 방향이 아니라, 사용자가 자신의 다음 행동을 더 쉽게 결정하도록 돕는 방향으로 확장되어야 한다.
