본문 바로가기
Project/ReceiptMind

📱 iOS 가계부 앱 데이터 모델 설계하기

by 밤새는 탐험가89 2025. 7. 30.
728x90
SMALL
enum + class를 활용한 깔끔하고 확장성 높은 데이터 구조 설계

💡 어떤 앱인가요?

제가 만들고 있는 이 앱은 지출이나 수입을 이미지(카메라/앨범)와 함께 기록할 수 있는 iOS 가계부 앱입니다.

예를 들어:

  • 영수증을 찍어서 저장하거나
  • 감성적인 쇼핑 사진을 남기거나
  • 어떤 항목에 얼마를 썼는지를 사진과 함께 기록하는 목적이에요.

그래서 금액, 카테고리, 메모 외에도 사진(image) 필드가 꼭 필요했고, 수입/지출을 명확하게 분리하면서도 관리하기 쉽도록 데이터 모델을 설계했습니다.


🧱 데이터 구조 설계

1. enum TransactionType: 수입 / 지출 타입

  • 수입(income)과 지출(expense)을 구분
  • 각각에 따른 카테고리 목록아이콘 맵핑 제공

2. class ExpenseModel: 실제 기록되는 데이터 모델

  • 수입/지출 타입
  • 카테고리 이름
  • 금액
  • 이미지 (선택)
  • 날짜
  • 메모

🧾 전체 코드 보기

// 수입/지출 타입을 정의하는 열거형
enum TransactionType: String {
    case income
    case expense

    var categoryOptions: [String] {
        switch self {
        case .income:
            return ["월급", "성과금", "용돈", "보너스", "금융소득"]
        case .expense:
            return [
                "식비", "교통비", "문화생활", "생활품", "의류", "보험", "미용",
                "의료/건강", "교육", "통신비", "회비", "세금", "경조사", "저축",
                "가전", "공과금", "카드대금", "기타"
            ]
        }
    }

    var categoryImageMap: [String: String] {
        switch self {
        case .income:
            return [
                "월급": "dollarsign.circle.fill",
                "성과금": "gift.fill",
                "용돈": "hands.sparkles.fill",
                "보너스": "gift.fill",
                "금융소득": "chart.line.uptrend.xyaxis"
            ]
        case .expense:
            return [
                "식비": "fork.knife",
                "교통비": "car.fill",
                "문화생활": "music.note.house.fill",
                "생활품": "cart.fill",
                "의류": "tshirt.fill",
                "보험": "shield.lefthalf.fill",
                "미용": "scissors",
                "의료/건강": "cross.case.fill",
                "교육": "book.fill",
                "통신비": "antenna.radiowaves.left.and.right",
                "회비": "person.3.fill",
                "세금": "doc.plaintext.fill",
                "경조사": "gift.fill",
                "저축": "banknote.fill",
                "가전": "tv.fill",
                "공과금": "bolt.fill",
                "카드대금": "creditcard.fill",
                "기타": "ellipsis.circle.fill"
            ]
        }
    }
}

 

// 지출/수입 항목 데이터 모델
class ExpenseModel {
    let id: UUID
    var transaction: TransactionType
    var category: String
    var amount: Int
    var image: UIImage?
    var date: Date
    var memo: String?

    init(id: UUID = UUID(),
         transaction: TransactionType,
         category: String,
         amount: Int,
         image: UIImage? = nil,
         date: Date = Date(),
         memo: String
    ) {
        self.id = id
        self.transaction = transaction
        self.category = category
        self.amount = amount
        self.image = image
        self.date = date
        self.memo = memo
    }
}

📚 모델 구성 설명

🔹 TransactionType enum

열거형은 가계부 항목이 수입(income) 인지 지출(expense) 인지를 명확히 구분합니다. 그리고 각 타입에 따라 선택 가능한 카테고리 목록(categoryOptions)시스템 이미지 이름(categoryImageMap)을 제공합니다.

 

장점:

  • 유형에 따라 동적으로 UI 구성 가능
  • 예: .income이면 "월급", .expense면 "식비" 등의 항목만 보여줌
  • 카테고리별 아이콘을 SF Symbol로 매핑 가능 → 사용자에게 직관적인 UI 제공

 

🔹 ExpenseModel class

하나의 수입/지출 데이터를 나타내는 모델입니다. 사용자가 기록하는 모든 항목은 이 모델의 인스턴스로 관리됩니다.

속성 설명
id UUID, 고유 식별자
transaction 수입/지출 유형 (TransactionType)
category 항목 카테고리 이름
amount 금액 (Int)
image 첨부된 이미지 (선택사항)
date 기록 날짜
memo 간단한 메모 내용

 


💡 MVVM에서 init() 초기화의 장점

위 ExpenseModel은 init(...) 생성자를 통해 즉시 속성을 초기화하도록 설계되어 있습니다. 이 방식은 MVVM 패턴과 매우 잘 어울리며, 다음과 같은 이점을 가집니다:

✅ 1. 인스턴스 생성 시점에 데이터 주입

ViewModel에서 입력값을 받아 바로 모델을 생성할 수 있습니다.

final class TransactionViewModel {
    ...
    
    // MARK: - Variable
    @Published var transaction: ExpenseModel?
    
    
    // MARK: - Init
    init(mode: AddTransactionMode? = nil) {
        self.mode = mode
        
        if let mode = mode {
            switch mode {
            case .create:
                self.transaction = ExpenseModel(
                    id: UUID(),
                    transaction: .expense,
                    category: "",
                    amount: 0,
                    image: nil,
                    date: Date(),
                    memo: ""
                )
            case .edit(let id):
                readByIDTransaction(by: id)
            }
        } else {
            // 기본 초기화 (예: HomeViewController 등에서 사용)
            self.transaction = nil
        }
    }
	...

 

✅ 2. 실시간 상태 변경 가능

 ViewModel에서 @Published var transaction = ExpenseModel(...) 형태로 관리하면, 사용자가 입력을 바꿀 때마다 실시간으로 모델이 업데이트되고, View와 데이터가 항상 동기화됩니다.

 

✅ 3. 작성과 수정에서 동일한 모델 사용

신규 작성과 기존 항목 수정 시 모두 ExpenseModel 인스턴스를 재사용하면, 코드와 UI를 통일되게 관리할 수 있습니다.


🎯 결론

이번 모델 구성은 다음과 같은 핵심 목적을 가지고 설계되었습니다:

  • 사용자 경험을 고려한 카테고리 시스템
  • MVVM 기반 실시간 데이터 처리에 최적화된 모델 구조
  • 아이콘 매핑을 통한 직관적 UI 구현

가계부 앱의 핵심인 "빠르고 정확한 기록"을 위해 이처럼 모델 구조부터 정교하게 설계하는 것이 중요합니다.

728x90
LIST