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

TIL. Aug 17, 2026

오늘 한 내용 PS LeetCode 1563. Stone Game V WWDC Swift Concurrency. Explore Structured Concurrency in Swift (2) - WWDC21 마이그레이션 Algorithm. Kadane’s Algorithm 마이그레이션 미방문록. 대표 색상 추출 Picker 만들기 마이그레이션 배운 내용 Unstructured Task WWDC21 Explore Structured Concurrency in Swift 세션을 정리를 마무리했다. 1편과 다르게 구조화된 task tree에 속하지 않는 unstructured task와 detached task를 정리했다. Task { ... }로 만든 unstructured task는 생성한 scope가 끝나도 계속 실행될 수 있다. 생성 지점의 actor, priority, task-local value를 상속한다. ...

August 17, 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. Repository의 save를 insert와 update로 나누기

유즈케이스를 구현하면서 Habit과 Completion을 저장할 Repository 계약을 좀 정리했다. 처음에는 create와 update 둘 다 save 하나로 저장하면 간단해 보였지만, 두 기능이 기대하는 실패 조건이 서로 다르다는 점에서 계약을 나눌 필요가 있었다. save는 어떤 동작인가 save를 upsert로 구현하면 호출하는 쪽은 편하다. ID가 없으면 삽입하고, 이미 있으면 갱신하면 된다. protocol HabitRepository { func save(_ habit: Habit) async throws } 문제는 이 동작이 실패해야 하는 상황까지 조용히 성공으로 바꾼다는 점이다. (물론 UUID라 현실적으로 겹칠 일은 없지만) Habit을 생성하는 작업은 같은 ID가 이미 있으면 실패해야 하고, 보관처럼 기존 Habit을 바꾸는 작업은 대상이 없으면 실패해야 한다. ...

August 14, 2026

TIL. Aug 13, 2026

오늘 한 내용 Data Structure. Segment Tree LeetCode 2213. Longest Substring of One Repeating Character StackDay. 책임 분리와 타입 분리 배운 내용 StackDay 통계 계산 HabitStatistics를 구현하면서 계산 결과를 표현하는 타입과 계산 로직을 분리했다. 다만 HabitStatisticsCalculator가 HabitStreakCalculator에 의존하게 되면서, 여러 Calculator가 같은 날짜 정규화와 완료 기록 필터링을 반복하는 문제가 보였다. 타입을 나누는 것만으로 항상 구조가 좋아지는 것은 아니고, 공유하는 계산 맥락까지 함께 살펴봐야 한다는 점을 배웠다. 자세한 고민은 StackDay 개발일지에 정리했다. ...

August 13, 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

TIL. Aug 12, 2026

오늘 한 내용 LeetCode 2958. Length of Longest Subarray With at Most K Frequency LeetCode 2996. Smallest Missing Integer Greater Than Sequential Prefix Sum StackDay. 엔티티와 계산 책임 분리하기 배운 내용 파생 값을 구현할 때의 책임 분리 StackDay의 HabitEntry와 HabitStreak을 구현했다. 모델링 단계에서 정의한 파생 값을 실제 코드로 옮기면서, 값을 표현하는 타입과 값을 계산하는 로직을 분리했다. AI 에이전트가 작성한 내용중에 별로인 것들을 몇개 직접 수정했는데 리스트로 정리하면 다음과 같다. HabitEntry의 이니셜라이저 전체 Completion 목록을 직접 탐색하고, 생성 실패/성공을 판정했다. HabitStreak의 이니셜라이저 내부에 Streak 계산 로직이 들어있었다. 둘 다 경계가 제대로 구분되지 않았다. HabitEntry 내부에서 전체 Completion을 탐색하면서 조건에 맞는 Completion을 찾아내는건 비효율적이기도 하고, 이건 유즈케이스에서 해야 될 일이다. ...

August 12, 2026

SniffMEET. 닉네임 검증 개선 고민

작업 내역 닉네임 TextField의 검증 방식을 다시 정리했다. 현재는 UITextFieldDelegate로 길이 검증과 버튼 활성화를 처리하고 있었는데, 중복 체크까지 같은 흐름에 넣을 경우 네트워크 요청이 너무 자주 발생할 수 있어 보였다. 현재 구현 현재는 Delegate 기반으로 다음 두 가지를 처리하고 있었다. extension ProfileCreateViewController: UITextFieldDelegate { func textFieldDidChangeSelection(_ textField: UITextField) { guard let textCount = textField.text?.count else { return } submitButton.isEnabled = (textCount > 1 && textCount < 9 ) } func textField( _ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String ) -> Bool { guard let text = textField.text else { return false } let newLength = text.count + string.count - range.length let inputTextValid = newLength <= 15 return inputTextValid } } 길이 검증 자체는 단순했고, 현재 사용성도 나쁘지 않았다. 문제는 중복 체크처럼 네트워크 요청이 들어가는 검증까지 같은 방식으로 처리하기에는 부담이 커진다는 점이었다. ...

February 13, 2025