TIL. Aug 18, 2026

오늘 한 내용 Stack Day LocalDay 날짜 모델 도입 Clock과 FixedClock 추가 완료 기록·취소·보관·삭제 UseCase를 날짜 모델에 맞게 수정 InMemoryHabitRepository, InMemoryCompletionRepository 구현 및 테스트 추가 StackDay. Date로 하루를 표현하면 생기는 문제 배운 내용 LocalDay 도입하기 Stack Day에서는 Date 하나로 특정 시점과 특정 날짜를 모두 표현하고 있었다. 그래서 달력상의 날짜만 필요한 값을 LocalDay로 분리했다. LocalDay: startedOn, archivedOn, completedOn Date: createdAt, updatedAt, recordedAt 현재 시각은 Clock이 제공하고, 그 시각을 어느 날짜로 해석할지는 TimeZone이 결정하도록 분리했다. 전체 설계와 구현 코드는 개발일지에 정리했다. ...

August 18, 2026

StackDay. Date는 날짜가 아니다

StackDay는 일정 주기의 습관 수행 여부를 기록하는 앱이고, MVP에서는 그 주기를 매일로 고정했다. 지금까지 구현할 때, “어느 날에 수행했는가"와 “정확히 언제 기록했는가"를 모두 Foundation의 Date로 표현했다. 두 정보는 비슷해 보이지만 다르다. 이 차이를 타입으로 구분하지 않으면 날짜 검증, 중복 기록 검사, 통계 계산마다 같은 해석을 반복하게 된다. 이번 글에서는 그 문제가 실제 코드에서 어떻게 드러났고, LocalDay·Clock·TimeZone을 어떤 경계로 나눴는지 정리한다. Date타입이 저장하는 것은 순간 Date는 연, 월, 일이나 자정 같은 달력 개념을 저장하지 않는다. ...

August 18, 2026

StackDay. 비즈니스 규칙과 Entity Identity 분리하기

현재 StackDay MVP에서는 하나의 Habit에 대해 하루 한 번만 Completion을 기록할 수 있다. 그래서 처음에는 (habitID, completedOn) 조합을 Completion의 identity처럼 사용해도 될 것 같았고, 실제로 그런 코드도 있었다. 하지만 Completion 삭제를 구현하면서 이 둘을 분리해야 한다는 점을 알게 되었다. struct Completion: Equatable, Identifiable { let id: UUID let habitID: UUID let completedOn: Date let recordedAt: Date } 같은 날의 기록을 찾는 조건 같은 Habit의 같은 날 Completion을 찾는 데에는 habitID와 completedOn이 필요하다. 현재 정책에서는 이 조건으로 중복 기록도 막을 수 있다. 하지만 이것은 현재 정책에서 필요한 조회 조건이다. 이후 시간 단위로 습관을 기록하거나, 하루에 여러 번 수행하는 Habit이 생기면 같은 조합으로 여러 Completion이 존재할 수 있다. ...

August 16, 2026

StackDay. UseCase와 Entity는 각각 어디까지 책임져야 할까

CreateHabitUseCase를 시작으로 여러 유즈케이스를 구현하면서, 도메인 규칙을 어디에 두어야 할지 계속 고민하게 됐다. Habit을 생성할 때 이름의 유효성을 누가 검증해야 하는지, Completion을 남길 때 해당 날짜에 기록할 수 있는지는 누가 판단해야 하는지처럼 비슷한 문제가 반복해서 나타났다. 처음에는 유즈케이스가 사용자의 작업을 처리하니 이런 검증도 함께 담당하면 된다고 생각할 수 있었다. 하지만 구현을 진행할수록 엔티티가 스스로 보장해야 하는 규칙과 유즈케이스 작업의 흐름을 위해 판단해야 하는 규칙을 구분할 필요가 있었다. 이 포스트에서는 Habit의 생성과 Completion 기록을 중심으로 어떤 고민을 했는지와 어떻게 해결했는지를 정리한다. ...

August 14, 2026

StackDay. 책임 분리와 타입 분리

이전 작업에서 HabitStreak을 구현하면서 계산 결과와 계산 로직을 분리했다. HabitStreak은 현재 스트릭과 최장 스트릭이라는 값만 표현하고, 계산은 HabitStreakCalculator가 담당한다. struct HabitStreak: Equatable { let current: Int let longest: Int } struct HabitStreakCalculator { func calculate( habit: Habit, completions: [Completion], referenceDate: Date, calendar: Calendar = .current ) -> HabitStreak { // ... } } 같은 원리로 HabitStatistics도 파생 값과 계산 책임을 분리해서 구현했다. HabitStatistics HabitStatistics는 Habit의 수행 기록을 요약한 파생 값이다. MVP에서는 다음 값을 제공한다. struct HabitStatistics: Equatable { let totalCompletedDays: Int let eligibleTrackingDays: Int let completionRate: Double let streak: HabitStreak } totalCompletedDays는 추적 기간 안에서 완료한 날짜의 수이고, eligibleTrackingDays는 시작일부터 기준일까지 추적 대상이 된 날짜의 수다. 아카이브된 Habit은 archivedOn을 마지막 추적일로 사용한다. ...

August 13, 2026

StackDay. 데이터 모델링, Entry가 아닌 Completion을 관리해야 하는 이유

StackDay의 MVP 범위와 정책을 가지고 앱의 도메인과 데이터를 모델링한다. 도메인 모델링 -> 데이터 모델링 -> 유즈케이스 검증 순서로 진행하고, 각각의 단계가 이전 단계를 검증하는 방향이다. 앱의 핵심 동작 이전 포스트에서 정한 앱의 컨셉과 정책에서부터, 이 앱이 어떤 동작을 해야 할지 먼저 정리했다. 사용자가 습관을 만들면 그날부터 습관의 추적이 시작된다. 사용자는 매일 습관을 수행했는지 기록하고, 앱은 누적된 기록을 바탕으로 현재 스트릭과 통계를 보여준다. 더 이상 이어가지 않을 습관은 추적을 종료할 수 있다. ...

August 11, 2026

StackDay. 0. Concept

시작하기 애플 리마인더나 마이크로소프트 To do를 쓰면 매일 해야하는 일들을 관리할 수 있다. 나도 매일 LeetCode Daily를 풀고, Bing 출석체크를 하고, 매일 TIL 을 적으려고 한다. 하지만 기본적으로 Reminder, Todo 앱과 습관 형성 앱은 차이가 있다. 첫 번째로 기능 요구사항이 다르다. 애플 리마인더는 매일 반복해야 하는 태스크를 하지 않았을 경우에, 날짜가 새로 갱신되지 않고, 그 날짜에 계속 남아있는다. 마이크로소프트 To do는 하루를 빠뜨리면 그 태스크가 사라지지 않고 누적된다. 내가 원하는 습관 형성 앱은 리마인더의 방식, 마이크로소프트 투 두의 방식 등등을 포괄할 수 있는 앱이었다. ...

August 8, 2026