---
# System prepended metadata

title: 다이어리 PR

---

# 다이어리 PR


@uuu1101
안녕하세요 태태! 🙇🏻‍♀️🙇🏻‍♀️
Step1 PR드립니다.
잘부탁드립니다..!

## 📝 STEP 진행 중 경험하고 배운 것
- 스토리보드없이 코드로만 View구현하기
	 - [X] 스토리보드를 삭제하고 SceneDelegate를 수정해 navigationController가 embeded된 View를 구현했습니다.
- swiftLint 라이브러리 사용하기
	 - [X] pod 을 이용해 라이브러리를 적용했습니다.
	 - [X] `swiftLint` 라이브러리로 코드컨벤션을 통일했습니다. 
- modernCollectionView를 이용한 List구현
	 - [X] `UICollectionViewCompositionalLayout.list`를 이용해 ListView 구현했습니다.
     - [X] DiffableDataSource, snapShot을 이해하고 적용했습니다.
- readableContentGuide 사용하기
     - [X] app adaptivity 및 가로모드의 레이아웃에 readableContentGuide를 사용했습니다.

## ⚒️ 코드 구현내용 
### 1️⃣ DiaryListViewController
- 일기장 목록화면인 DiaryListView를 담당하는 뷰컨트롤러입니다.
- setupNavigationBar() : 내비게이션바를 정의하는 메서드입니다.
- collectionView(collectionView:didSelectItemAt:) : UICollectionViewDelegate프로토콜을 채택하여 목록 중 하나의 항목을 선택했을 때 다음화면으로 전환하는 메서드입니다.
- ModernCollectionView로 일기장목록을 구현하였습니다.
    - convertDiaryData() : JSON파일을 Data타입으로 디코드 후 Diary요소를 갖는 배열타입으로 저장합니다.
    - configureDiaryListDataSource() : 정의해놓은 커스텀셀인 DiaryCell을 등록합니다.
    - snapShot() : 변경된 데이터를 스냅샷형태로 저장한 후 apply()메서드로 뷰에 다시 그려달라 요청합니다.
    - Section 열거형 : diaryListView의 Section을 열거하고, 하나의 main case를 갖습니다.
    
### 2️⃣ DiaryDetailViewController
- 일기장 목록 중 하나의 일기상세화면인 DiaryDetailView를 담당하는 뷰컨트롤러입니다.
- 이전화면(일기목록화면)에서 현재화면(일기상세화면)으로 데이터를 전달받기 위해 diary 변수를 선언하였습니다.
- configureDiary() : 일기상세화면에 제목,내용,일자 각각 항목별 데이터를 바인딩하는 메서드입니다.
- setupNotification() : 키보드가 보이거나 숨겨질 때의 노티피케이션에 Observer를 추가해서 controlKeyboard() 메서드를 호출합니다.
- controlKeyboard(_:): 키보드 보이기와 숨기기시 키보드 높이만큼 ScrollView의 Bottom에 inset을 추가/삭제 합니다.

### 3️⃣ Diary
- JSON파일(sample.json)을 decode하기위한 DTO(데이터전송객체) 입니다.
- CodingKey 프로토콜을 채택하였고, DataFormat 양식을 따르기 위한 createDate 연산프로퍼티를 갖고 있습니다.

### 4️⃣ DiaryCell
- 일기장목록화면 `DiaryListView`에서 항목 하나를 정의하는 클래스입니다.
- configureDiaryCellLayout() : `stack view`에 넣을 뷰를 `addArrangedSubview()`메서드를 이용하여 `embed`해주는 메서드입니다.

### 5️⃣ DecodeManager
- JSON형식의 데이터를 decode하는 클래스입니다. do-catch문을 이용해 에러처리를 해주었습니다.

### 6️⃣ CustomUI
- CustomLabel
    - 일기장목록화면 `DiaryListView`에서 Label들이 공통으로 갖는 서식 코드가 중복된다는 문제가 있었습니다. 코드의 중복을 줄이기 위해 초기화 할 때 공통 서식을 부여해주는 클래스입니다.
- CustomTextView
    - 일기상세화면 `DiaryDetailView`에서 공통 서식을 부여하여 초기화하는 클래스입니다.

---

## 💭 고민한 부분
### DiffableDataSource사용할 때, Value값이 똑같은 데이터로 인한 문제 해결
- 프로젝트에 포함된 견본 JSON 데이터를 사용해 List Cell을 구성할 때, 값이 같은 데이터가있어 정상작동 되지않는 문제가 있었습니다.
- 원인: Diffable datasource를 사용하려면 `ItemIdentifier`가 각각 고유한 Hash값을 갖고 있어야하는데 아래 캡쳐 화면 처럼 value가 완전히 같을 때는 두 개가 같은 Hash값을 갖기 때문입니다.
	![](https://i.imgur.com/PpWdKev.jpg)
	</br>
- **✅ 수정: UUID 사용**
	- DTO에 `UUID`를 생성해 각 value들이 각각 고유한 Hash값을 갖도록 했습니다.
		```swift
		struct Diary: Decodable, Hashable {
			let title: String
			let body: String
			let createdAt: Double
			let id = UUID() // 👈
			// ...
		}
		```
		
### 키보드가 보여지고 사라질 때, 컨텐츠를 가리지않도록 하는 방법
- 키보드가 보여질 때는 키보드 뒤에 가려진 컨텐츠를 스크롤 해서 볼 수 있도록 ScrollView로 구현했습니다. TextView에도 Scroll기능이 있지만, title과 body가 같이 스크롤 될 수 있도록 따로 ScrollView에 title과 body의 TextView를 담았습니다.
- 키보드가 보여지고/사라질 때 ScrollView의 Bottom방향으로 키보드 높이만큼 Inset을 추가/삭제해서 컨텐츠가 가려지지 않도록 구현했습니다.
- 입력 완료 후 키보드를 내리는 방법으로 UIScrollView의 keyboardDismissMode 속성을 사용해 사용자가 스크롤하여 키보드를 다시 내릴 수 있도록 구현했습니다.

---
## 🧐 조언을 얻고 싶은 부분
### 👉 DiaryCell 이 MVC에서 View인지 / Model인지 궁금합니다.
DiaryCell은 UICollectionViewListCell을 상속 받고, 이어서 상속받는 클래스를 보면 UIView가 최상위 클래스입니다. 
UICollectionViewListCell ⬅️ UICollectionViewCell ⬅️ UICollectionReusableView ⬅️ UIView
그렇기에 MVC 중 View에 해당된다고 이해했습니다.
Cell타입은 View가 담당하는 UI요소 중 하나이고, Cell의 내용을 채울 데이터는 Controller에서 정해준다고 이해한 것이 맞을까요?

---

@uuu1101
안녕하세요 태태! 🙇🏻‍♀️🙇🏻‍♀️
Step2 PR드립니다..! 

## 📝 STEP 진행 중 경험하고 배운 것
- CoreData 사용하기 
	 - [X] CoreData 모델을 생성했습니다.
	 - [X] CRUD 메서드를 구현했습니다.
	 - [X] 코어데이터를 다루는 모델을 싱글톤으로 구현했습니다.
- 상속으로 등록/수정 과정의 공통기능을 구현.
	 - [X] VC1와 공통기능을 갖는 VC2를 Class상속을 통해 구현했습니다.
- 컬렉션뷰 리스트에서 스와이프를 액션 구현
	 - [X] 테이블뷰에서 스와이프로 데이터를 공유/삭제합니다.
- Text View Delegate의 활용
	 - [X] TextView의 변화를 감지해 PlaceHolder를 설정합니다.

## ⚒️ 코드 구현내용 
### 1️⃣ CoreDataManager
- CRUD
    - `save(diaryPage:)` : Create
    - `fetchDiaryPages()` : Read
    - `update(diaryPage:)` : Update
    - `delete(diaryPage:)` : Delete
    - `deleteAllDiary()` : 계속되는 테스트로 쌓인 데이터를 초기화하는 메서드입니다.
- `deleteAllNoDataDiaries()` : 새로운 일기 생성 후 아무것도 입력하지 않았을 경우, 저장 후 fetch해올 때 삭제합니다.
- `searchDiary(id:)` : 수정 시 해당 일기만 찾아 데이터를 가져옵니다.
- `saveContext()` : 수정 또는 삭제 후 최종버전을 저장합니다. 중복코드를 줄이기위한 메서드입니다.

### 2️⃣ RegisterDiaryViewController
- 새로운 일기 생성화면을 담당합니다.
- 비어있는 `DiaryDetailView`를 보여줍니다.
- `UITextViewDelegate`
    - `textViewDidChange(textView:)` : 텍스트뷰에 변화가 감지되면 PlaceHolder를 제거합니다.
    - `textViewDidEndEditing(textView:)` : 텍스트뷰의 편집이 끝나면 PlaceHolder를 보여줍니다.
- `setupNotification()` : 입력상태에 따라 키보드를 보여주거나 숨깁니다.
    - `controlKeyboard(notification:)` : 텍스트뷰가 키보드로인해 가려지지 않도록 높이를 조정합니다.
    
### 3️⃣ DiaryDetailViewController
- 일기 상세화면인 `DiaryDetailView`를 담당합니다.
- 이미 작성한 일기가 입력되어있는 `DiaryDetailView`를 보여줍니다.
- `RegisterDiaryViewController`를 상속받은 후 초기화 시 뷰만 `DiaryDetailView`로 재정의한 컨트롤러입니다.
- `configureDiary()` : 일기제목과 내용을 구성합니다.
- `setupNavigationBar()` : 내비게이션바에 공유버튼을 구성합니다.
- `showActionSheet()` : 공유, 삭제옵션을 가진 액션시트를 뷰 하단에 띄웁니다.
    - `showActivityView()` : 공유 선택 시 `CustomActivityViewController`가 담당하는 뷰를 보여줍니다.
    - `showDeleteAlert()` : 삭제 선택 시 한번 더 확인받는 Alert를 띄웁니다.

### 4️⃣ CustomActivityViewController
- 공유 시 뷰 하단에서 올라오는 `ActivityView`를 담당하는 컨트롤러입니다.
- 공유할 채널(메일,문자,메모,카톡,기타SNS 등)에 포함시킬 것과 제외할 것을 정해줍니다.

---

## 💭 고민한 부분
### 1️⃣ 상속으로 공통기능 구현시, 주요 프로퍼티에 접근제어(private)를 설정해주기 위해 이니셜라이저에서 데이터를 갈아끼우는 로직 구현
- `RegisterDiaryViewController`(다이어리 등록화면 VC)를 상속받는 `DiaryDetailViewController`(다이어리 수정화면 VC)를 구현하면서 필수로 private을 설정해주어야 할 프로퍼티들에 접근제어를 설정할 수 없는 문제가 있었습니다. VC 두 개가 똑같이 `diaryPageView`, `diaryPage` 데이터를 갖고있는데 외부에서 접근할 수 없게 Private설정을 해주어야 했습니다.
- 상속받은 프로퍼티를 사용하지 않고 새로운 프로퍼티를 생성해서 사용하게되면, 이 프로퍼티들을 사용하는 메서드들을 모두 `override`해서 새로 생성한 프로퍼티를 사용하도록 모두 `override`후 재구현 해야했습니다.
- 결론적으론 접근제어설정과, 메서드 `override` 없이 상속받은 메서드를 사용할 수 있도록 이니셜라이저에서 데이터를 갈아끼우는 로직을 구현했습니다

### 2️⃣ 텍스트뷰 PlaceHolder
- `PlaceHolder`를 `TextView`에 입력한 채로 구현해보니 아무것도 입력하지 않았을 때 `PlaceHolder`의 텍스트를 입력값으로 인식하여 `PlaceHolder`로만 구성된 일기가 생성되는 문제가 있었습니다.
- `CustomLabel`타입으로 된 `titlePlaceHolder`와 `bodyPlaceHolder`를 만들고 `text`속성에 `PlaceHolder` 내용을 담았습니다.
- 텍스트뷰의 내용이 없을때만 `PlaceHolder`를 보여주는 `removePlaceHolder()` 메서드를 `DiaryDetailView`에 구현하였습니다.

### 3️⃣ 데이터가 없는 페이지는 자동 삭제되는 로직
- 처음엔 다이어리 등록 시 작성내용이 있는지 확인한 후 저장을 결정하는 로직을 구현하려했는데, 그러면 저장할때도 확인해야하고, 한글자를 써서 이미 저장이 된 데이터들에 대해서는 업데이트시 확인 후 삭제해야하고, 등록화면 뿐 아니라 이미 저장된 다이어리를 보는 화면에서도 내용을 모두 지우면 업데이트가 아닌 삭제를 해야하는데, 이곳저곳에서 계속 확인하느니 일단 저장 후 `fetch`해 올 때(저장된 리스트를 보여줄 때) 빈 값의 데이터를 자체적으로 삭제한 후 `fetch`하는 로직으로 구현했습니다.

---
## 🧐 조언을 얻고 싶은 부분
### 👉 CoreDataManager를 싱글톤 VS CoreData CRUD 메서드 Unit Test
CoreDataManager는 객체가 하나만 생성될 수 있도록 싱글톤으로 구현했는데, 싱글톤으로 구현하니 상속기능을 사용할 수 없어 Mock을 사용하기도 힘들고, coreData의 persistentContainer를 테스트용 컨테이너로 바꿔서 사용할 수가 없어서 CRUD 메서드들에 대한 unit Test를 할 수가 없었습니다...
많은 문서들을 찾아보고 참고해보려했는데 싱글톤생성의 단점이 Unit Test가 어려워진다는 점이라고해서 그럼 이제 선택차이겠구나 싶었습니다.
유닛테스트가 중요하다고 생각되면 싱글톤 작성을 풀고 유닛테스트를 작성하면되고, CoreDataManager의 유일성이 중요하다면 유닛테스트를 포기해야겠구나! 로 정리했는데
혹시 태태라면 CoreDataManager의 싱글톤 VS CRUE 유닛테스트 중 어떤걸 선택하셨을지 개인적인 의견이 궁금합니다..!

### 👉 CoreData의 Diary타입이 있는데 DiaryPage라는 DTO를 따로 두어야할 지 궁금합니다
코어데이터의 Attribute종류와 타입, DTO의 프로퍼티와 타입이 완벽히 동일하고, CoreData의 codagen도 Class Definition을 선택해 내부적으로 타입을 갖고있는 상황에서
DTO 사용해서 눈에 보이는 모델로 보인다는 장점 외에는 다른 장점을 찾지 못하겠는데 삭제하고 코어데이터의 Diary타입만 사용해도 될지 궁금합니다.. 🤔

### 👉 fetchResultController를 사용하고 싶은데요..
현재 로직은 리스트를 보여줄 때, 코어데이터의 모든 데이터들을 **모두** 가져온 후 저장한 후 보여주는 로직인데, 다이어리 데이터가 아주 많~아졌을 때는 리스트에서는 당장 20개만 보이면 될 것을 굳이 모두 갖고와 저장하고 있을 필요가 있을까 싶었습니다.. `nsfetchedresultscontroller`를 사용해서 가져올 데이터의 양을 batchSize로 정할 수 있고 delegate를 이용해 리스트에 보이는 셀의 갯수만큼씩 가져와 리스트로 보여줄 수 있다고해서 적용을 해보려했는데.... 결과적으론 실패했습니다...ㅎㅎ(다시 도전해볼 예정이고... 이걸 사용하면 DiaryDTO 모델을 사용하지 않고, 오히려 사용하려면 코드 구현이 더 까다로워져서 위의 질문이 생긴 이유이기도합니다) 그래서 혹시나 `nsfetchedresultscontroller` 사용 외에 다른 방식으로 코어데이터를 조금씩 가져오는 방법이 있는지 궁금합니다. 


---

@uuu1101
안녕하세요 태태! 🙇🏻‍♀️🙇🏻‍♀️
Step3 PR드립니다.
잘 부탁드립니다..!

## 📝 STEP 진행 중 경험하고 배운 것
- CoreData Migration 적용하기
	 - [X] 날씨정보를 추가하면서 코어데이터의 모델 변경을 위해 Lightweight Migration 했습니다.
- Open API의 활용 
	 - [X] [Open Weather](https://openweathermap.org/current) 서비스를 사용했습니다.
	 - [X] URLSessionDataTask를 활용해 기기의 위치를 기반으로 현재 날씨정보와 날씨 아이콘을 요청해서 받아옵니다.
- Core Location 프레임워크의 활용
	 - [X] 기기의 위치 정보에 대한 권한을 요청합니다.

---
## ⚒️ 코드 구현내용 
1️⃣ DiaryInfo
- 코어데이터에 다이어리 내용을 전달해주는 데이터전송객체 입니다.

2️⃣ WeatherInfo
- 날씨API에 있는 json 중첩타입데이터를 전역타입으로 변환하여 저장하고, `DiaryInfo`타입의 속성 중 하나의 타입을 담당하는 구조체입니다.

3️⃣ WeatherManager
- fetchWeatherInfo(completion:) : 서버로부터 날씨정보를 받아오기위해 URL을 만들고,  `URLSessionProvider()`를 통해 데이터를 얻습니다.
- fetchWeatherIcon(icon:,completion:) : 매개변수로 받은 `icon`의 문자열에 해당하는 날씨아이콘이 위치한 URL을 이용하여 `URLSessionProvider()`로부터 이미지를 얻습니다.

4️⃣ WeatherURL
- 위치정보를 기반으로 날씨에 맞는 날씨아이콘을 가져올 때 해당 아이콘이 위치한 URL을 생성하는 열거형입니다.

5️⃣ LocationManager
- 위치정보 접근을 담당하는 클래스입니다. 위치 정확도가 지정된 객체인 `manager`를 `CLLocationManager`타입으로 생성하여 관련된 일들을 처리합니다.
- 위치값을 담은 `location` 변수는 위도와 경도로 받아오므로 튜플타입으로 정의하였습니다.
    - fetchLocation() : 사용자의 현재위치 획득은 배터리효율을 위해 `requestLocation()`로 한번만 위치를 요청합니다.
    - CLLocationManagerDelegate 관련 메서드
        - locationManagerDidChangeAuthorization(manager:) : 앱에 대한 위치 접근권한 설정이 변경되면 호출됩니다. 사용자의 선택에 따라 위치 접근권한을 획득하거나 결정해달라는 요청을 보냅니다.
        - locationManager(manager:didUpdateLocations:) : 사용자의 위치를 성공적으로 가져왔을 경우 호출됩니다.
        - locationManager(manager:didFailWithError:) : 사용자의 위치를 가져오지 못했을 경우 호출됩니다.
6️⃣ URLSessionProvider
    - `URLSession`을 이용하여 매개변수로 받은 `url`에 있는 데이터를 받아옵니다. 


---
## 💭 고민한 부분
### 일기장 목록의 셀 높이가 작았다가 클릭 시 정상높이가 되는 문제 해결
- 다이어리 리스트화면에서 `DiaryCell`의 높이가 아래GIF처럼 작았다가 클릭 시 정상높이로 돌아오는 문제가 있습니다.
- 원인은 셀의 레이아웃을 잡아주는 `configureDiaryCellLayout()` 메서드가 Cell의 제목, 날씨아이콘, 내용을 담아주는 `configureCell()` 메서드 내부에서 호출이 되는데, 이 `configureCell()` 메서드의 호출 시점이 쓰레드관련 문제로 가장 마지막에 호출되는 문제였습니다. 
- 💡 `configureCell()`,`configureDiaryCellLayout()` 메서드는 main쓰레드에서 호출해서 먼저 레이아웃을을 잡고 제목과 내용만 담겨있게 되고, 서버 통신을 위해 다른 쓰레드에서 작업이 필요한 날씨아이콘 fetch 작업만 기능을 분리해 작업이 끝나면 다시 main쓰레드로 보내는 `DispatchQueue.main.async`로 처리해 정상적으로 레이아웃이 잡혀있는 Cell에 icon을 넣을 수 있었습니다.

### 다이어리 저장 시 2개씩 저장되는 문제 해결
- 다이어리 한 개 저장 시 두 개가 저장되어 목록에 나오는 문제가 있었습니다. 하나는 사용자가 입력한 내용의 다이어리이고, 다른 하나는 내용없는 다이어리가 생성되어 목록에 보였습니다.
- 💡 `Weather` 속성 설정을 Xcode 우측에 있는 `Data Model Inspector` - `Entity` - `Parent Entity`옵션에서 `No Parent Entity`로 설정하여 해결하였습니다.

### Error를 print문으로 처리하였으나 사용자가 DecodingError를 마주했을 때의 문제
step1 리뷰에서 말씀해주셨던 질문인 Error를 print문으로 처리하였으나 사용자가 DecodingError를 마주했을 때 어떻게 될지에 고민해보았습니다.
Alert로 알려주는 방법을 선택하였으나 alert 이후 새 다이어리 작성화면으로 넘어가지 못하는 오류가 있어 커밋하지 못했습니다. 이 외에도 날씨API에 있는 json 데이터를 수정할 수 없는 문제, 에러 원인별 테스트 해보는 문제가 있습니다. 테스트용 JSON데이터를 만들어 Unit Test를 해보는것이 지금의 계획이온데- 추후 리팩토링 과제로 남겨두었습니다.😅
