할 일 앱은 많지만, 대부분은 너무 무겁거나(협업·프로젝트 관리 기능 과잉) 너무 가벼워서(단순 체크리스트) 개인의 하루를 관리하기에 애매합니다. 정작 필요한 것은 "오늘 무엇을 먼저 해야 하는가"를 빠르게 정리하고, 마감을 놓치지 않으며, 내가 얼마나 해냈는지 되돌아보는 가벼우면서도 충분한 도구입니다.
소두잇(SoDoIt) 은 이런 고민에서 시작된 프로젝트입니다. 우선순위·마감일·카테고리로 할 일을 정리하고, 마감 알림으로 놓치지 않으며, 통계 화면과 홈 화면 위젯으로 진행 상황을 한눈에 확인할 수 있는 개인용 할 일 관리 앱입니다. 외부 라이브러리 없이 Apple 프레임워크만으로 구축하여, 최신 SwiftUI·CoreData 생태계를 온전히 학습하고 활용하는 것을 목표로 삼았습니다.
- ⚡️ Focus — 우선순위와 스마트 필터로 "지금 할 일"에 집중
- 🗂 Organize — 커스텀 색상·아이콘 카테고리로 할 일을 체계적으로 분류
- 📈 Reflect — 통계와 위젯으로 성취를 시각화하고 동기 부여
📸 스크린샷은 추후
docs/screenshots/경로에 추가 예정입니다.
| 기능 | 설명 |
|---|---|
| ✅ 할 일 관리 | 할 일 생성·조회·수정·삭제(CRUD), 스와이프로 완료/미완료 토글 |
| 🚩 우선순위 | 높음 / 보통 / 낮음 3단계, 우선순위 색상 표시 및 정렬 |
| ⏰ 마감일 & 알림 | 날짜·시간 지정, 마감 임박·초과 시각적 표시, 로컬 푸시 알림 |
| 🗂 카테고리 | 커스텀 색상·SF Symbol 아이콘 카테고리로 분류·필터링 |
| 🔎 스마트 필터 | 전체 / 오늘 / 예정 / 완료 기준 빠른 필터링 및 정렬 옵션 |
| 📊 통계 | 완료율·주간 완료 추이·우선순위 분포를 Swift Charts로 시각화 |
| 📱 홈 화면 위젯 | WidgetKit 기반, 앱을 열지 않고도 오늘 할 일 확인 |
| ⚙️ 설정 | 알림·표시 옵션 등 앱 환경 설정 |
| 카테고리 | 사용 기술 |
|---|---|
| Language | Swift 6.0 |
| Minimum iOS | iOS 26.2 |
| UI | SwiftUI (선언형 UI) |
| Architecture | MVVM + Repository Pattern |
| State Management | @Observable (Observation 프레임워크) |
| Persistence | CoreData (NSPersistentContainer) |
| Widget | WidgetKit + App Group 공유 저장소 |
| Charts | Swift Charts |
| Notification | UserNotifications (로컬 알림) |
| Concurrency | MainActor 기본 격리 (SWIFT_DEFAULT_ACTOR_ISOLATION) |
| Dependency Manager | 외부 의존성 없음 (Apple 프레임워크만 사용) |
| 지원 기기 | iPhone (Portrait) |
| Xcode | 26.2 |
본 프로젝트는 MVVM(Model-View-ViewModel) 패턴에 Repository 패턴을 결합하여, 데이터 접근 로직과 UI 로직을 명확히 분리했습니다.
┌─────────────────────────────────────────────────┐
│ View Layer (SwiftUI) │
│ TodoListView · AddTodoView · EditTodoView │
│ StatsView · SettingsView · CategoryListView │
├─────────────────────────────────────────────────┤
│ ViewModel Layer (@Observable) │
│ TodoListViewModel · TodoFormViewModel(공통 기반) │
│ StatsViewModel · SettingsViewModel · ... │
├─────────────────────────────────────────────────┤
│ Service Layer (Repository Pattern) │
│ TodoRepository · CategoryRepository │
│ StatisticsRepository │
│ CoreDataManager · NotificationManager │
│ WidgetDataManager (싱글톤) │
├─────────────────────────────────────────────────┤
│ Data Layer (CoreData) │
│ TodoItem · Category 엔티티 │
└─────────────────────────────────────────────────┘
- View: SwiftUI 선언형 뷰. 사용자 입력을
ViewModel에 전달하고,@Observable상태 변화를 자동으로 반영합니다. - ViewModel:
@Observable로 상태를 노출하며,Repository를 통해 데이터를 읽고 씁니다. 폼 화면은TodoFormViewModel공통 기반 클래스를 상속해 추가/수정 로직을 재사용합니다. - Service:
Repository가 CoreData CRUD를 캡슐화하고,throws+context.rollback()으로 데이터 일관성을 보장합니다.CoreDataManager·NotificationManager·WidgetDataManager가 각각 저장소·알림·위젯 동기화를 담당합니다. - Data:
TodoItem·Category엔티티.NSManagedObject서브클래스는wrapped*계산 프로퍼티로 안전한 옵셔널 언래핑을 제공합니다.
모든 데이터 작업에 일관된 3단계 오류 처리를 적용합니다.
- Repository:
throws+ 실패 시context.rollback()으로 데이터 일관성 보장 - ViewModel:
catch에서 에러 상태로 전환 +Logger(OSLog)로 기록 - View:
.alert(item:)수정자로 사용자에게 한글 오류 메시지 표시
- SwiftUI + @Observable + CoreData 조합으로, 별도 반응형 라이브러리 없이도 데이터 변경이 UI에 자동 반영되는 선언형 흐름을 구성했습니다.
- Repository 패턴으로 CoreData 접근을 한 곳에 모아, ViewModel은 저장소 구현을 모른 채 도메인 로직에만 집중할 수 있습니다.
- 외부 의존성 제로 원칙으로, Apple 최신 프레임워크(Observation, Swift Charts, WidgetKit)를 온전히 학습·활용합니다.
| 속성 | 타입 | 설명 |
|---|---|---|
id |
UUID | 고유 식별자 (자동 생성) |
title |
String | 할 일 제목 (필수) |
memo |
String? | 메모 |
dueDate |
Date? | 마감일 |
priority |
Int16 | 우선순위 (0=높음, 1=보통, 2=낮음) |
isCompleted |
Bool | 완료 여부 |
completedAt |
Date? | 완료 시각 |
createdAt |
Date | 생성일 (자동 생성) |
category |
Category? | 소속 카테고리 (다대일) |
| 속성 | 타입 | 설명 |
|---|---|---|
id |
UUID | 고유 식별자 (자동 생성) |
name |
String | 카테고리 이름 (필수) |
colorHex |
String | 색상 Hex 코드 (기본: #007AFF) |
iconName |
String | SF Symbol 아이콘명 (기본: folder.fill) |
createdAt |
Date | 생성일 (자동 생성) |
todoItems |
NSSet? | 소속된 할 일들 (일대다) |
관계: TodoItem ↔ Category (다대일, optional, 삭제 시 Nullify)
SoDoIt/
├── SoDoItApp.swift # 앱 진입점, viewContext 주입
├── ContentView.swift
│
├── Models/ # CoreData 엔티티 · 도메인 열거형
│ ├── TodoItem / Category # NSManagedObject 서브클래스
│ ├── Priority # 우선순위 enum
│ ├── SmartFilter # 전체/오늘/예정/완료 필터
│ ├── SortOption # 정렬 기준
│ ├── WidgetTodoItem # 위젯 공유용 모델
│ └── SoDoIt.xcdatamodeld # CoreData 모델
│
├── Services/
│ ├── Manager/ # CoreDataManager, NotificationManager, WidgetDataManager
│ └── Repository/ # TodoRepository, CategoryRepository, StatisticsRepository
│
├── ViewModels/
│ ├── Common/ # TodoFormViewModel (공통 기반 클래스)
│ ├── TodoList/ · AddTodo/ · EditTodo/
│ ├── Category/ # CategoryListViewModel, AddCategoryViewModel
│ ├── Stats/ · Settings/
│
├── Views/
│ ├── MainTabView.swift # 탭 기반 루트 뷰
│ ├── Common/ # TodoFormBodyView, ChipButton, EmptyStateView
│ ├── TodoList/ # TodoListView, TodoRowView, SmartFilterBar, CategoryFilterBar
│ ├── AddTodo/ · EditTodo/
│ ├── Category/ # CategoryListView, AddCategoryView, Components/
│ ├── Stats/ # StatsView, Components/ (차트)
│ └── Settings/ # SettingsView
│
├── Extensions/ # Color+Hex, Font+Pretendard, View+DismissKeyboard, AppAnimation
├── Resources/Fonts/ # Pretendard Custom Font
└── Assets.xcassets/ # 앱 아이콘, AccentColor
SoDoItWidget/ # 홈 화면 위젯 (WidgetKit)
├── SoDoItWidgetBundle.swift
├── TodoListWidget.swift
├── TodoWidgetProvider.swift
├── TodoWidgetEntryView.swift
└── WidgetTodoItem.swift
할 일 추가(AddTodo) 와 수정(EditTodo) 화면은 입력 필드(제목·메모·마감일·우선순위·카테고리)가 사실상 동일했습니다. 두 화면을 각각 구현하면서 검증·바인딩 로직이 양쪽에 중복되어, 필드를 하나 추가할 때마다 두 곳을 동시에 수정해야 하는 유지보수 부담이 생겼습니다.
TodoFormViewModel공통 기반 클래스를 두어 폼 상태와 검증 로직을 한곳에 모으고,AddTodoViewModel·EditTodoViewModel이 이를 상속하도록 설계했습니다.- UI는
TodoFormBodyView공통 뷰로 분리해, 추가/수정 화면이 동일한 폼 UI를 공유하도록 했습니다. - 저장 시점의 동작(신규 삽입 vs 기존 업데이트)만 각 서브클래스에서 오버라이드했습니다.
- 폼 필드 추가·검증 규칙 변경이 단일 지점 수정으로 반영되어 유지보수 비용이 크게 줄었습니다.
- 추가/수정 화면의 UI·동작 일관성이 자연스럽게 보장되었습니다.
홈 화면 위젯이 "오늘 할 일"을 표시해야 하지만, 위젯 확장(Extension)은 앱 본체와 별도의 프로세스·샌드박스에서 동작하므로 앱의 CoreData 저장소에 직접 접근할 수 없었습니다. 그 결과 앱에서 할 일을 완료해도 위젯이 갱신되지 않는 문제가 있었습니다.
- App Group을 구성해 앱과 위젯이 공유하는 저장 공간을 마련했습니다.
WidgetDataManager가 위젯 표시에 필요한 데이터를 경량 모델(WidgetTodoItem)로 직렬화해 공유 저장소에 기록하도록 했습니다.- 할 일에 변경이 생기면
WidgetCenter로 타임라인 리로드를 요청해, 앱의 변경이 위젯에 즉시 반영되도록 했습니다.
- 앱을 열지 않고도 홈 화면에서 오늘 할 일을 확인할 수 있게 되었습니다.
- 앱과 위젯 사이의 데이터 정합성이 유지되어, 완료 처리 후 위젯이 실시간으로 갱신됩니다.
Git Flow 기반으로 운영합니다.
main ← 프로덕션 릴리스 (.gitignore 관리)
├─ develop ← 통합 개발 브랜치
├─ release ← 릴리스 후보
└─ feature/{기능명} # develop에서 분기하는 기능 브랜치
- 모든 기능 작업은
develop에서 브랜치를 분기하여 PR로 병합합니다. main은 릴리스 태그와 동기화됩니다.
타입 prefix를 사용합니다.
<type>: <간단한 설명>
| Prefix | 용도 |
|---|---|
feat: |
신규 기능 추가 |
fix: |
버그 수정 |
refactor: |
리팩토링 |
docs: |
문서 수정 |
- 네이밍: Swift API Design Guidelines 준수 (UpperCamelCase / lowerCamelCase)
- 동시성:
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor기본 격리 - CoreData:
NSManagedObject접근 시wrapped*계산 프로퍼티로 안전하게 언래핑 - 상태 관리: ViewModel은
@Observable로 선언, View는 상태를 구독만 함 - 오류 처리: Repository
throws→ ViewModelLogger기록 → View.alert표시 - 프리뷰:
CoreDataManager.preview(인메모리 저장소)로 SwiftUI 프리뷰 지원
외부 의존성이 없습니다. 모든 기능은 Apple이 제공하는 프레임워크만으로 구현되었습니다.
| 프레임워크 | 사용 목적 |
|---|---|
| SwiftUI | 선언형 UI 구성 |
| Observation | @Observable 기반 상태 관리 |
| CoreData | 로컬 영구 저장소 |
| WidgetKit | 홈 화면 위젯 |
| Swift Charts | 통계 화면 차트 시각화 |
| UserNotifications | 마감일 로컬 알림 |
| Combine | 이벤트 스트림 처리 |
| OSLog | 구조화된 로깅 |
커스텀 폰트로 Pretendard를 번들에 포함하여 사용합니다.
# 빌드 (사용 가능한 시뮬레이터는 `xcrun simctl list devices available`로 확인)
xcodebuild -project SoDoIt.xcodeproj -scheme SoDoIt \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' buildMade with ❤️ by haansohee