Skip to content

야곰 아카데미 iOS 일기장 프로젝트 저장소입니다

Notifications You must be signed in to change notification settings

lxodud/ios-diary

 
 

Repository files navigation

📓 일기장 프로젝트

  • CoreData를 활용한 일기 작성 보관 앱입니다.

📖 목차

  1. 팀 소개
  2. 기능 소개
  3. Diagram
  4. 폴더 구조
  5. 타임 라인
  6. 프로젝트에서 경험하고 배운 것
  7. 트러블 슈팅
  8. 참고 링크

🌱 팀 소개

미니 Hamo

그라운드 룰

  • 🙅‍♀️ 연락 불가능시간(회의 불가능 시간) 미니 : 월, 수 20:00 ~ 22:00(저녁 9시 이후에는 개인 공부시간)

  • 🏃‍♀️ 프로젝트 오전 10시부터 6시까지 프로젝트 진행!!월, 목은 오전에 학습활동 공부 후, 17시 30분 부터 19시까지

  • 📝 Git Commit Convention 깃모지 사용

    이모지 설명 이모지 설명
    🎉 프로젝트 시작 ♻️ 코드 리팩토링
    💄 UI 관련 업데이트 🐛 버그 수정
    새로운 기능 추가 🧱 프로젝트 구조 변경
    📝 문서 변경 🧪 테스트 코드 작성
  • 📢 학습 공유 프로젝트 시작 전 스크럼 하기스크럼 내용 : 오늘 하루 컨디션, 특이사항, 어제 학습한 내용 공유

  • 🏃‍♀️운동 시간 오후 9시 팔굽혀펴기 100개 하기

🛠 기능 소개

일기장 리스트 및 작성 화면 일기장 삭제 날씨 저장
일기장 공유 지역화 설정 변경 시 화면 다크 모드

👀 Diagram

CoreData 관련 UML

CoreDataUML

Network 관련 UML

NetworkUML

🗂 폴더 구조

├── Resources
│   ├── Assets.xcassets
│   ├── Base.lproj
│   ├── Diary.xcdatamodeld
│   │   ├── Diary V2.xcdatamodel
│   │   └── Diary.xcdatamodel
│   ├── Info.plist
│   ├── MappingModelV1ToV2.xcmappingmodel
│   ├── SceneDelegate.swift
└── Sources
│   ├── CacheManager.swift
│   ├── Controller
│   │   ├── DiaryDetailViewController.swift
│   │   └── DiaryListViewController.swift
│   ├── CoreDataManager.swift
│   ├── Extensions
│   │   ├── Date+.swift
│   │   ├── TimeInterval+.swift
│   │   ├── UIAlertAction+.swift
│   │   ├── UIAlertController+.swift
│   │   ├── UIFont+.swift
│   │   ├── UINavigationController+.swift
│   │   ├── UINavigationItem+.swift
│   │   ├── UITextField+.swift
│   │   ├── UITextView+.swift
│   │   └── UIView+.swift
│   ├── Model
│   │   ├── Diary.swift
│   │   ├── Networking
│   │   │   ├── ImageLoader.swift
│   │   │   ├── NetworkError.swift
│   │   │   ├── NetworkManager.swift
│   │   │   └── Requesting.swift
│   │   ├── Utility
│   │   │   ├── Constant.swift
│   │   │   └── LocalizedConstant.swift
│   │   └── WeatherEntity.swift
│   └── View
│       └── DiaryListViews
│           ├── DiaryContentConfiguration.swift
│           ├── DiaryListCell.swift
│           └── DiaryListCellContentView.swift
└── DiaryTests
     ├── StubNetworking
     │   ├── Mockable.swift
     │   ├── StubNetworkManager.swift
     │   └── StubURLSession.swift
     └── WeatherAPITests.swift

⏰ 타임라인

STEP 1

날짜 구현 내용
22.12.19 SwiftLint 적용, UITableViewDiffableDataSource 설정
22.12.20 DiaryContentConfiguration 구현, DateFormating 구현
22.12.21 DiaryWriteViewController 구현, 키보드 유무에 따른 동적인 레이아웃 구현, PR 발송
22.12.22 리뷰 피드백에 맞게 리팩토링(Observer remove, 메서드 기능에 맞게 내부 로직 분리)

STEP 2

날짜 구현 내용
22.12.26 셀이 눌렸을 때 화면 전환, 셀 삭제 기능 구현, UIAlertController rngus
22.12.27 코어데이터 구현, 공유 기능 추가
22.12.28 전체적인 컨벤션 수정, 메서드 역할 분리, PR 발송

STEP 3

날짜 구현 내용
23.01.02 메서드 네이밍 및 구조 리팩토링
23.01.03 Core Location 및 네트워킹 구현
23.01.04 네트워킹 테스트 코드 작성, 코어데이터 마이그레이션 구현
23.01.05 이미지 캐싱, 에러 핸들링 구현, PR 발송

✅ 프로젝트에서 경험하고 배운 것

  • UITableViewDiffableDataSource
  • DateFormatter를 통한 Localization
  • scheme를 편집하여 시뮬레이터의 언어 및 지역 변경
  • UIFontDescriptor
  • UIContentConfiguration
  • UISwipeActionsConfiguration
  • UIContextualAction
  • Core Data
  • Singleton Pattern
  • UIActivityViewController
  • Localizable

🤔 고민한 점

STEP 1

Massive ViewController에 대한 고민

AppleViewController를 개발하는 과정에서 View + Controller의 개념으로 ViewController가 생성된 것으로 알고 있습니다. 이로 인해서 다양한 문제점들이 있지만, 저희가 맞닥들인 문제는 ViewController가 비대해진다는 문제였습니다. ViewControllerView의 전환, DataSource의 역할 등을 수행하면서 비대해지는 것을 느끼게 되었습니다. 이를 해결하기 위해서 궁금한 점들이 발생하였습니다.

  • ViewController의 딜리게이트 메서드를 다른 타입으로 구현하여서 역할을 줄이는 방법을 사용하는 것도 방법이 될 수 있나요?
  • ViewController가 화면을 전환하는 역할을 가지고 있는 것이 적절한지 궁금합니다.
  • 만약, ViewController가 화면을 전환하는 역할을 가지고 있을 필요가 없다면, 다른 패턴을 활용하여서 구현하는 것도 괜찮을까요?
  • MVC 패턴의 단점을 보완하기 위해서 다른 패턴들을 활용하는 방법 외 에는 Massive한 Controller를 변경할 수 있는 방법이 없나요?

위와 같은 고민들을 하게 되었지만, 아직 명확한 해답을 찾지는 못한 것 같다. 이에 대해서 추가적으로 공부하고 발전 시킬 것입니다.

STEP 2

CoreDataManager의 구성

코어데이터 매니저를 구성하는 데 있어서 고민한 부분이 있었습니다. 앱 전역적으로 사용해야 하는 것이기 때문에 싱글턴을 사용해도 될까 라는 고민을 하게 되었습니다. 또한, 코어데이터 매니저에 대해서 유닛 테스트를 적용하는 것에 대해서도 고민하게 되었습니다.

코어 데이터의 유닛 테스트를 진행하기 위해서는 In-Memory 특성을 가지는 PersistentContainer를 이용하여 Stub한 코어 데이터 매니저를 구현할 수 있어야 합니다. 즉, 코어 데이터 매니저를 싱글톤 패턴으로 구현하지 못하고, initalizer를 가지고 있어야 하는 상황이 발생한 것입니다. 위와 같이 트레이드 오프한 상황에 대처하기 위해서 각각의 장점을 표로 정리하고 함께 토론 하면서 어떤 점을 중점으로 앱을 개발하는 것이 중요한가에 대해서 함께 고민하였습니다.

싱글톤 유닛 테스트
앱이 전반적으로 코어 데이터에 접근할 수 있다. 앱이 전반적으로 코어 데이터에 접근할 수는 없지만, 코드가 의도한 대로 동작하는 지 확인하기 용이하다.

위와 같은 표에서 싱글톤을 사용하는 것이 더 좋은 장점을 가지고 있다고 판단하였습니다.

UIContextualActionCompletion

SwipeAction을 구현하기 위해서는 UIContextualAction 타입을 구현해야 합니다. UIContextualAction타입은 사용자가 테이블 행을 스와이프할 때 표시할 동작으로 completionHandler를 가지고 있으며, 이를 통해서 액션이 정상적으로 동작하였는지에 대한 값을 반환해 주고 있습니다. 요구 사항에 따르면, 삭제 버튼을 누를 경우에는 다시 삭제에 대한 핸들러 내부에서 다음 동작을 할 수 있도록 구현하여야 합니다. 이에 대해서 많은 고민을 하게 되었습니다. 많은 컴플리션 핸들러를 통해서 코드의 가독성이 많이 떨어지기 때문입니다. 아래와 같이 completionHandler와 동일한 형식의 함수를 구성하고 그 내부에서 다음동작을 할 수 있도록 하였습니다. 또한, 많은 정보를 줄 수 없는 init를 대체하기 위해서 convenience init를 구성하고 이를 활용하여서 컴플리션 핸들러를 줄였습니다.

// before
let handler: UIContextualAction.Handler = { [weak self] _, _, handler in
    let alert = UIAlertController(
        title: NSLocalizedString("DeleteTitle", comment: "삭제 Alert 제목"),
        message: NSLocalizedString("DeleteMessage", comment: "삭제 Alert 본문"),
        preferredStyle: .alert
    )
    
	let deleteAction = UIAlertAction(type: .delete) { [weak self] _ in
	    guard let self = self else { return }
	    
	    let result = self.deleteSnapshot(item: item)
	    handler(result)
	}
            
    let cancelAction = UIAlertAction(type: .cancel) { [weak self] _ in
        handler(false)
    }
            
    alert.addAction(deleteAction)
    alert.addAction(cancelAction)            
    self?.present(alert, animated: true)
}
    
// after
let handler: UIContextualAction.Handler = { [weak self] _, _, handler in
    guard let self = self else { return }
    
    let alert = UIAlertController(
        title: LocalizedConstant.AlertController.deleteTitle,
        message: LocalizedConstant.AlertController.deleteMessage,
        diary: item,
        deleteCompletion: { [weak self] _ in
            guard let self = self else { return }
            
            let result = self.deleteSnapshot(item: item)
            handler(result)
        },
        cancelCompletion: { _ in
            handler(false)
        }
    )
    
    self.present(alert, animated: true)
}

지역화에 맞춘 앱 구성시 앱의 상수를 관리하는 방법

앱에 대해서 지역화를 구성하기 위해서 각 문자열에 대해서 고민하게 되었습니다. 많은 NSLocalizedString 타입을 직접적인 문자열을 활용하여서 구성하는 것이 다른 코더가 보았을 때, 수정해도 되는 문자열이라고 착각할 수 있을 것 같다는 생각이 들었습니다. 그래서 앱 전체에서 사용되는 지역화된 문자열 상수를 LocalizedConstant 타입으로 묶어서 관리하였습니다. 그래서 다음과 같이 앱에 필요한 상수들을 한개의 enum타입에서 모두 관리할 수 있도록 하였습니다.

// before
let alert = UIAlertController(
    title: NSLocalizedString("DeleteTitle", comment: "삭제 Alert 제목"),
    message: NSLocalizedString("DeleteMessage", comment: "삭제 Alert 본문"),
    ...
// after
let alert = UIAlertController(
    title: LocalizedConstant.AlertController.deleteTitle,
    message: LocalizedConstant.AlertController.deleteMessage,
        ...

STEP 3

Request 타입 구현에 대한 고민

enum 타입을 이용해서 다양한 API에 대해서 URLRequest를 생성해 주려고 하였습니다. 다만 이렇게 구현하였을 때 Requset가 추가되었을 때 기존의 코드를 변경하여야 하고 이는 개방 폐쇄 원칙 중 변경에 폐쇄적이지 못하다고 생각했습니다. 그래서 변경에 폐쇄적인 부분을 더 가질 수 있도록 protocol과 struct를 통해서 구현하였습니다.

네트워킹 객체에 대한 고민

네트워크에 대한 다양한 API를 사용할 수 있는 공통적인 함수를 구성하는데 많은 고민을 했습니다. 그래서 프로토콜을 구성하고, 이를 통해서 다양한 네트워크를 관리할 수 있는 타입을 구성할 수 있도록 했습니다. 또한 제네릭을 활용하여서 다양한 네트워크 매니저가 공통된 함수를 활용하여서 네트워킹을 할 수 있도록 하고, 이에 대해서 유연한 코드를 만들 수 있었습니다. 하지만, 이를 통해서 실질적인 네트워킹을 하는 않는 테스트 코드를 작성하는데 어려운 부분이 있었습니다. 테스트 코드를 기존 프로토콜을 채택하는 Stub한 타입을 구성하여서 테스트를 하려고 하였지만, 테스트 코드의 목적을 네트워킹을 하는 로직에 대해서 테스트하는 것이라고 생각하였기 때문에 Stub한 URLSession을 구현할 수 있도록 하여서 테스트 하였습니다.

🚀 트러블 슈팅

STEP 1

UITableViewDiffableDataSource를 왜 사용해야 하는가?

기존의 DataSource를 사용했을 때 삭제나 삽입 동작을 수행하였을 때 UI의 truthDataSource의 역할을 하는 컨트롤러의 truth가 일치하지 않아서 오류가 발생하기 쉽습니다. 또한, 사용자와의 상호작용하는 과정에서 reloadData 메서드를 자주 호출하게 됩니다. 위와 같은 행위들은 사용자의 UX를 손상시킬 수 있습니다. 다이어리 앱 같은 경우에는 사용자와의 상호작용이 많은 앱이라고 생각하였고, 위와 같은 문제들을 해결하기 위해서 DiffableDataSource를 활용하였습니다.

CustomUIContentConfiguration을 사용해야 하는가?

커스텀한 셀을 구현하기 위해서 클래스를 상속하여서 구현하게 되면, 새로운 데이터가 생성되거나 셀의 데이터를 변경될 때마다 셀을 다시 업데이트하도록 메서드를 통해서 업데이트 해야 합니다. 하지만, 업데이트를 위해서 메서드를 지속적으로 호출하는 것이 셀에게 데이터를 주는 것이라고 생각하였습니다. 추가적으로 사용자 이벤트에 대한 상태 변경을 더욱 쉽고 유연하게 할 수 있고, 데이터소스가 직접적으로 셀의 컨텐츠 뷰에 접근하지 않도록 할 수 있는 장점이 있어서 사용하게 되었습니다.

UIViewConstraint 메서드가 길어지는 것

뷰의 constraint를 걸어주기 위한 보일러플레이트 코드가 지속적으로 발생하였습니다. 이에 대해서 줄일 수 있는 방법에 대해서 고민해보았습니다. UIView를 확장하여 하나의 메서드로 구현하였습니다.

func anchor(
    top: NSLayoutYAxisAnchor? = nil,
    leading: NSLayoutXAxisAnchor? = nil,
    bottom: NSLayoutYAxisAnchor? = nil,
    trailing: NSLayoutXAxisAnchor? = nil,
    paddingTop: CGFloat = 0,
    paddingLeading: CGFloat = 0,
    paddingBottom: CGFloat = 0,
    paddingTrailing: CGFloat = 0
) {
    if let top = top {
        topAnchor.constraint(equalTo: top, constant: paddingTop).isActive = true
    }
    
    if let leading = leading {
        leadingAnchor.constraint(equalTo: leading, constant: paddingLeading).isActive = true
    }
    
    if let bottom = bottom {
        bottomAnchor.constraint(equalTo: bottom, constant: -paddingBottom).isActive = true
    }
    
    if let trailing = trailing {
        trailingAnchor.constraint(equalTo: trailing, constant: -paddingTrailing).isActive = true
    }
}

일기 작성 화면에서 제목 TextField와 본문 TextViewinset 맞추기

DiaryWriteViewControllerTextField TextView의 텍스트가 시작하는 inset이 일치하지 않아서 둘을 일치시키는 방법에 대해서 고민하였습니다. TextView의 경우 inputTextViewtextContainer가 감싸고 있는 구조였고 textContainerlineFragmentPadding이라는 textpadding 값을 담고있는 프로퍼티의 기본값이 5이기 때문에 일치하지 않은 문제가 있었습니다. 해당 프로퍼티를 0으로 할당하여 문제를 해결하였습니다.

STEP 2

많은 guard let의 사용

DiaryDetailViewController에서 textField, textView의 text를 다루는데 해당 프로퍼티의 타입이 optional이기 때문에 각 메서드들에서 두 text프로퍼티를 옵셔널 바인딩하는 코드가 중복되어서 나타났습니다. textField와 textView에 아무런 입력이 없을 때 text 프로퍼티는 nil이 아닌 빈 문자열을 가지고 있기 때문에 UITextField와 UITextView를 extension하여 text의 옵셔널을 해제하여 리턴하는 프로퍼티를 정의하여 DiaryDetailViewController에서 중복되는 옵셔널 바인딩을 줄여보았습니다.

코어데이터의 Response와 앱에서 사용할 데이터의 분리

코어 데이터를 데이터 베이스와 동일한 측면으로 생각하게 되면, 네트워킹을 통해서 받아오는 원천 데이터로 생각하였습니다. 원천 데이터를 직접적으로 뷰에 보여주는 것을 적절하지 않고 의도치 않은 수정이 바로 반영이 될 수 있는 점에서 위험하다고 생각하였습니다. 그래서 코어 데이터를 통해서 받아오는 데이터를 가공하여서 뷰에 보여줄 데이터를 따로 구성하였습니다. 또한, 위와 같이 구성하면서 모든 타입이 옵셔널을 사용할 수 없도록 하였습니다.

🔗 참고 링크

[공식문서] Implementing Modern Collection Views UITableViewDiffableDataSource UISwipeActionsConfiguration UIContextualAction Core Data Core Location

[WWDC] Modern Cell Configuration Advances in UI Data Source

About

야곰 아카데미 iOS 일기장 프로젝트 저장소입니다

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Swift 99.5%
  • Ruby 0.5%