---
# System prepended metadata

title: "\U0001F3AA 만국박람회"
tags: [올라프, 만국박람회, project, idinaloq, hoon]

---

![](https://hackmd.io/_uploads/ByCvdPL_3.png)

## Ground Rules

### 규칙
- TIL, 일일 회고 작성 시간(매일 00시부터 1시간 작성 진행)
- 페어프로그래밍 시간 제한 최대 30분

### 스크럼
- 오전 11시 디스코드에서 진행
- 금일 진행 사항 공유하기(오늘의 할일)

### 프로젝트 규칙
- 네이밍 준수하기(가이드 라인)
- 커밋 메시지 규칙 진행
- 코드에 대한 기록 그때그때 하기


## 일일 스크럼

### 🙌 06/26
- 오늘의 컨디션
    - idinaloq: 비가와서 너무 습해요 💦
    - hoon: 방학이 행복했습니다.😆
- 특이사항
    - idinaloq: 방학때 공부를 안했어요😭
    - hoon: 방학이 더 필요해요...🤣
- 오늘 할 일
    - [x] 활동학습 예습
    - [x] JSON 파일 분석
    - [x] STEP 1 모델 타입 구현

### 🙌 06/27
- 오늘의 컨디션
    - idinaloq: 속이 안좋아요🍦🍦🍦
    - hoon: 푹 잤습니다.
- 특이사항
    - idinaloq: 오늘은 2~3시전에 자는걸로...!
    - hoon: 오후에 운동을 다녀오겠습니다. 🏃🏻
- 오늘 할 일
    - [x] STEP 1 PR 코멘트 답변 달기
    - [ ] 오토레이아웃 정복하기 공부
    - [ ] 코드베이스로 UI 그리기 공부

### 🙌 06/28
- 오늘의 컨디션
    - idinaloq: 푹자서 컨디션이 좋아요!
    - hoon: 조금 피곤합니다🤣
- 특이사항
    - idinaloq: 밀린 TIL 쓰기 
    - hoon: 오토레이아웃 더 공부해야 해요....
    - 공통: 오후 8시 리뷰어 만남 👀
- 오늘 할 일
    - [x] STEP2 첫 번째 화면 구현하기
    - [x] 리뷰어 면담

### 🙌 06/29
- 오늘의 컨디션
    - idinaloq: 컨디션만 좋습니다..
    - hoon: 비가 오니 꿉꿉해요⛈️
- 특이사항
    - idinaloq: 지각했습니다..죽겠습니다 😭🙇‍♂️
    - hoon: 가능하다면 낮잠을...🤣
- 오늘 할 일
    - [x] STEP2 TableView 구현하기
    - [x] 세 번째 뷰 구현하기

### 🙌 06/30
- 오늘의 컨디션
    - idinaloq: 최고입니다👍
    - hoon: 낫배드~🙂
- 특이사항
    - idinaloq: 일찍 PR할 수 있다면 안경을 새로 맞추기!
    - hoon: 토요스터디 예습 및 팀원들과 잠깐 스크럼을 할 수 있습니다
- 오늘 할 일
    - [x] 리팩토링
    - [x] STEP2 PR
    - [x] README 작성

### 🙌 07/03
- 오늘의 컨디션
    - idinaloq: 2일동안 잘 쉬어서 좋아요 ㅋㅋㅋ
    - hoon: 주말이 너무 행복했습니다😆
- 특이사항
    - idinaloq: 잠깐 볼일 보러 다녀오겠습니다
    - hoon: 활동 학습 예습👀
- 오늘 할 일
    - [ ] PR 코멘트 답변 남기기
    - [x] 활동 학습 예습 및 개인 공부

### 🙌 07/04
- 오늘의 컨디션
    - idinaloq: 요즘 너무 잘 쉬는거같아요...좋네요 ㅋㅋ😅
    - hoon: 장마라 습해요🌧️
- 특이사항
    - idinaloq: PR수정 완료하고 오토레이아웃 공부하기
    - hoon: 비가 오지 않는다면 저녁에 운동을 다녀오겠습니다🤣
- 오늘 할 일
    - [x] PR 코멘트 답변 남기기

### 🙌 07/05
- 오늘의 컨디션
    - idinaloq: 그저그래요😅
    - hoon: 피곤합니다😭
- 특이사항
    - idinaloq: 오늘할일 끝내기
    - hoon: 가능하다면 낮잠과 운동을...
- 오늘 할 일
    - [x] STEP 3 구현
    - [ ] STEP 3 PR 보내기

### 🙌 07/06
- 오늘의 컨디션
    - idinaloq: 졸리네요..
    - hoon: 아침이 힘들어요🤣
- 특이사항
    - idinaloq: 빨리끝내고 낮잠자기!!(오후5시이후 예상)
    - hoon: 낮잠이 필요합니다🥺
- 오늘 할 일
    - [x] STEP 3 PR 보내기
    - [x] 활동 학습 예습

### 🙌 07/07
- 오늘의 컨디션
    - idinaloq: 빨리...빨리내일...😵
    - hoon: 끝내고 잘 생각에 행복합니다🥳
- 특이사항
    - idinaloq: 프로젝트 마무리하기!
    - hoon: 오늘이 마지막 날😆
- 오늘 할 일
    - [x] STEP 3 PR 답변
    - [x] README 작성


# 🎪 만국박람회

## 📖 목차
1. [소개](#-소개)
2. [팀원](#-팀원)
3. [타임라인](#-타임라인)
4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조)
5. [실행 화면](#-실행-화면)
6. [트러블 슈팅](#-트러블-슈팅)
7. [참고 링크](#-참고-링크)
8. [팀 회고](#-팀-회고)

</br>

## 🍀 소개
훈, 이디나로크(`hoon`, `idinaloq`) 팀이 만든 만국박람회 가이드 앱입니다. 1900.04.14 ~ 1900.11.12 까지 개최된 파리 만국박람회 중 한국 출품작을 소개합니다. 각각의 테이블 뷰 셀을 클릭하면 자세한 출품작 내용을 확인할 수 있습니다.

* 주요 개념: `UITableView`, `UITableViewCell`, `JSON`, `JSONDecoder`, `Dynamic Type`, `AutoLayout`, `appDelegate`, `sceneDelegate`

</br>

## 👨‍💻 팀원
| idinaloq | hoon |
| :--------: | :--------: |
| <Img src = "https://user-images.githubusercontent.com/109963294/235301015-b81055d2-8618-433c-b680-58b6a38047d9.png" width = "200" height="200"/> |<Img src="https://i.imgur.com/zXoi5OC.jpg" width="200" height="200"> |
|[Github Profile](https://github.com/idinaloq) |[Github Profile](https://github.com/Hoon94) |



</br>

## ⏰ 타임라인
|날짜|내용|
|:--:|--|
|2023.06.26.| - `ExpositionUniverselle`, `Item` 타입 생성 |
|2023.06.27.| - `ExpositionUniverselle`, `Item` 타입 리팩토링 |
|2023.06.28.| - `ExpositionUniverselleViewController UI` 구현 <br> - `JSON` 파일 디코딩 <br> - `touchUpExpositionItemListButton` 메서드 추가 | 
|2023.06.29.| - `ExpositionItemListViewController UI` 구현 <br> - `itemListTableView` 추가 <br> - `ItemUITableViewCellStyleSubtitle` 클래스 생성 <br> - `ExpositionItemViewController UI` 구현 <br> - 리팩토링 |
|2023.06.30.| - 리팩토링 <br> - README 작성 |
|2023.07.01.| - `lazy var`로 선언된 프로퍼티 수정 <br> - `ExpositionUniverselle` 타입 연산 프로퍼티 추가 <br> - `viewDidLoad` 수정 <br> - 매직리터럴 수정, `JSONFile` 타입 추가 |
|2023.07.03.| - `configureCell` 메서드를 사용하여 `cell`에 접근하도록 수정 <br> - `itemImage` 클로저 제거, `expositionItem` 상수로 변경 <br> - `addArrangedSubviews` 매개변수인 `subviews` 타입 수정 <br> - `JSONFile` 타입이 `CustomStringConvertible` 채택하도록 수정 |
|2023.07.04.| - `viewDidLoad` 추상화 수준 수정 <br> - `AlertController` 추가 |
|2023.07.05.| - `applicaion(_:supportedInterfaceOrientationsFor:)` 메서드 추가 <br> - `ExpositionUniverselleViewController` 오토레이아웃 제약 추가 <br> - `configureItemImageConstraint` 메서드 추가 <br> - 기존의 셀을 커스텀셀로 변경 <br> - 오토레이아웃 제약조건 수정|
|2023.07.06.| - `String+Extension` 추가 <br> - `dynamic type` 적용되도록 레이블 수정 <br> - `addTarget`을 `addAction`으로 수정 <br> |
|2023.07.07.| - `cell imageView constraints` 수정 <br> - `itemImage`의 `contentCompressionResistance priority` 변경 <br> - `changeOrientation` 네이밍 수정 및 가독성 개선 <br> - `configureCell` 메서드 수정 <br> - README 작성 |

</br>

## 👀 시각화된 프로젝트 구조

### ℹ️ File Tree

    ┌── Expo1900
    │   └── Expo1900
    │       ├── Model
    │       │   ├── ExpositionUniverselle
    │       │   ├── Item
    │       │   └── JSONFile
    │       ├── View
    │       │   ├── LaunchScreen
    │       │   └── ExpositionItemCell
    │       ├── Controller
    │       │   ├── AppDelegate
    │       │   ├── SceneDelegate
    │       │   ├── ExpositionUniverselleViewController
    │       │   ├── ExpositionItemListViewController
    │       │   ├── ExpositionItemViewController
    │       │   └── AlertController
    │       ├── Extension
    │       │   ├── JSONDecoder+Extension
    │       │   └── String+Extension
    │       ├── Error
    │       │   └── NSDataAssetError
    │       ├── Assets
    │       └── Info
    │
    ├── 팀회고.md
    └── README.md


### 📐 Class Diagram
<p align="center">
<img width="1000" src="https://hackmd.io/_uploads/r1CkYErY3.jpg">
</p>


</br>

## 💻 실행 화면 

|  실행 화면(세로)  | 디코딩 오류 | 폰트 크기 변경 |
|:-------------:|:--------:| :--------: |
|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/b9701fea-486c-405c-b15e-8531946e3777" width="250">|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/24187f3c-0085-42f6-be1c-9a6c750605c7" width="250">|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/3df7f44a-1bfb-41b1-b616-fad2d825ca7f" width="250">|

|  실행 화면(가로)  |
|:-------------:|
|<Img src="https://github.com/Hoon94/SwiftAlgorithm/assets/43189761/a7630713-0a66-4555-8bbc-cd8059b4c388" height="250">|

</br>

## 🧨 트러블 슈팅

1️⃣ **JSON 포멧의 데이터와 매칭할 모델 타입** <br>
-
🔒 **문제점** <br>
`exposition_universelle_1900.json`과 `items.json`에서 사용하는 `JSON` 포맷의 데이터는 네이밍을 snake case 형태로 사용하였습니다. Swift에서 사용하는 camel case 형태와 달랐고, 축약어를 사용하는 네이밍도 있었습니다. `JSONDecoder`를 사용하여 디코딩을 하면 JSON 파일에 저장된 네이밍을 저희가 생성한 타입의 프로퍼티의 네이밍과 일치시켜야 했지만, Swift에서 사용하는 [네이밍 가이드라인](https://www.swift.org/documentation/api-design-guidelines/#naming)에서 타입과 프로토콜은 **UpperCamelCase**로, 나머지는 **lowerCamelCase**로 작성해야 된다는 가이드라인과 다르다는 문제가 있었습니다.

🔑 **해결방법** <br>
`CodingKeys`를 사용하여 `JSON` 파일에 저장된 네이밍을 다음과 같이 각각의 프로퍼티의 케이스에 맞게 지정하였습니다.
```swift
struct Item: Decodable {
    let name: String
    let imageName: String
    let shortDescription: String
    let totalDescription: String
    
    enum CodingKeys: String, CodingKey {
        case name
        case imageName = "image_name"
        case shortDescription = "short_desc"
        case totalDescription = "desc"
    }
}
```

<br>

2️⃣ **코드베이스로 `UI` 구현하기** <br>
-
🔒 **문제점** <br>
지금까지의 프로젝트는 스토리보드를 이용해서 `UI`를 구현했습니다. 지금의 소규모 프로젝트는 괜찮지만, 만약 프로젝트가 복잡해진다면 협업을 할 때 스토리보드에서의 충돌이 있을 가능성이 있기 때문에 코드베이스로 `UI`를 구현하기로 했지만, `View`를 추가해도 시뮬레이터에 아무것도 표시되지 않는 문제가 있었습니다.

🔑 **해결방법** <br>
해당 문제는 각각의 뷰에 제약조건을 설정하지 않았던 문제로, 스토리보드 상에서 `UI`를 추가할 때 기본적으로 상수 값인 `constant`로 지정되어서 `UI`가 보이지만, 코드베이스로 `UI`를 구현할 때는 기본적으로 아무 값도 없기 때문에 `UI`가 표시되지 않았던 문제였습니다. `topAnchor`, `leadingAnchor`, `trailingAnchor`, `bottomAnchor`, `heightAnchor`, `widthAnchor`등의 제약조건을 사용해서 화면의 어느 위치에 뷰를 띄울지에 대한 오토레이아웃 제약조건을 설정했습니다.

<br>

3️⃣ **뷰 업데이트** <br>
-
🔒 **문제점** <br>
뷰 컨트롤러에서 연산 프로퍼티를 사용하여 뷰를 생성할 때 `UILabel`의 경우 입력되어야 하는 내용들을 함께 입력하여 생성하였습니다.

```swift
private var expositionUniverselle: ExpositionUniverselle?

private lazy var titleLabel: UILabel = {
    let label: UILabel = UILabel()
    label.text = = expositionUniverselle?.title
    label.textAlignment = .center
    
    return label
}()
```

위와 같이 사용하면 `expositionUniverselle`이 초기화 전에 값에 접근하여 `title` 값을 받아오려고 하기 때문에 `lazy var`를 사용해야 했습니다.

🔑 **해결방법** <br>
뷰를 생성하고 이후에 `updateLabel()` 메서드에서 `label.text`에 값을 할당하는 작업들을 모아서 진행하도록 하였습니다.

```swift
private var expositionUniverselle: ExpositionUniverselle?

private let titleLabel: UILabel = {
    let label: UILabel = UILabel()
    label.textAlignment = .center
    
    return label
}()

...

private func updateLabel() {
    navigationItem.title = "메인"
    titleLabel.text = expositionUniverselle?.titleForLabel
    visitorsLabel.text = expositionUniverselle?.visitorsForLabel
    locationLabel.text = expositionUniverselle?.locationForLabel
    durationLabel.text = expositionUniverselle?.durationForLabel
    totalDescriptionLabel.text = expositionUniverselle?.totalDescription
}
```

<br>

4️⃣ **매직리터널 최소화** <br>
-
🔒 **문제점** <br>
`exposition_universelle_1900`, `items`같은 `JSON` 파일에서 직접적으로 파일을 읽어올 때 아래와 같이 파일 이름을 직접 지정하여 읽어오도록 하였습니다.

```swift
private func decodeExpositionUniverselle() {
    do {
        expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: "exposition_universelle_1900")
    } catch {
        print(error)
    }
}
```

추후 파일의 이름이나 파일 자체가 바뀌는 경우 파일 이름을 계속 변경해야 되는데, 만약 뷰 컨트롤러가 커지거나 파일이 많아지는 경우 일일이 파일명을 입력하는 코드를 찾아 변경하기 어려울 것 같다고 생각했습니다.

🔑 **해결방법** <br>
이를 해결하기 위해 `JSONFile.swift` 파일을 만들고 파일 이름을 관리할 수 있도록 했습니다.

```swift
enum JSONFile: CustomStringConvertible {
    case exposition
    case items
    
    var description: String {
        switch self {
        case .exposition:
            return "exposition_universelle_1900"
        case .items:
            return "items"
        }
    }
}
```

이렇게 하나의 파일에서 `JSON` 파일을 관리하기 때문에 파일의 수정이 필요한 경우 파일명을 입력하는 코드를 찾아갈 필요 없이 `JSONFile.swift` 파일에서 모두 해결할 수 있었습니다. 위의 `JSONFile` 타입을 적용하여 아래와 같이 코드를 수정하였습니다.

```swift
private func decodeExpositionUniverselle() {
    do {
        expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: JSONFile.exposition.name)
    } catch {
        print(error)
    }
}
```

<br>

5️⃣ **다음 뷰 컨트롤러에 데이터 전달** <br> 
-
🔒 **문제점** <br>
두 번째 뷰 컨트롤인 `ExpositionItemViewController`에서 `JSON` 데이터를 사용할 때 `decodeItems()` 메서드를 통해 디코딩 된 데이터를 `itemList`에 할당합니다. 세 번째 뷰 컨트롤인 `ExpositionItemListViewController`에서도 위와 같이 디코딩을 하면 같은 결과가 나오지만, 동일한 파일을 두 번 디코딩 하는 중복된 동작을 수행합니다.

🔑 **해결방법** <br>
두 번째 뷰 컨트롤에서 다음 뷰 컨트롤러에 데이터를 넘겨주어 동일한 작업을 여러 번 반복하지 않도록 하였습니다. 특히 세 번째 화면은 각 아이템에 대한 상세 정보를 보여주는 화면입니다. 세 번째 화면에서 전체 아이템 목록을 디코딩 하여 사용하기보다는 개별 아이템에 대한 데이터만을 이전 화면에서 전달받아 사용하도록 아래와 같이 구현했습니다.

```swift
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
    tableView.deselectRow(at: indexPath, animated: true)
    let expositionItemViewController: ExpositionItemViewController = ExpositionItemViewController(item: itemList[indexPath.row])
    navigationController?.pushViewController(expositionItemViewController, animated: true)
}
```

<br>

6️⃣ **`JSONDecoder Extension`** <br>
-
🔒 **문제점** <br>
같은 `decode()` 메서드를 현재 두 번 사용하고 있습니다. `do - try - catch` 과정에서 `NSDataAsset`을 옵셔널 바인딩을 해서 사용하고 있었습니다. `decode()` 메서드를 사용하는 횟수가 늘어난다면, `NSDataAsset`에 대한 옵셔널 바인딩 코드가 중복되었습니다.

🔑 **해결방법** <br>
`Extension`으로 `decode`의 기능을 다음과 같이 확장하여 사용했습니다.
```swift
extension JSONDecoder {
    func decode<T: Decodable>(_ type: T.Type, from assetName: String) throws -> T {
        guard let json = NSDataAsset(name: assetName) else {
            throw NSDataAssetError.invalidDataAsset
        }

        return try JSONDecoder().decode(type, from: json.data)
    }
}
```

<br>

7️⃣ **사용자에게 에러 알리기** <br>
-
🔒 **문제점** <br>
기존 코드에서 에러가 발생하는 경우 `print(error)`와 같이 터미널에 알리도록 구현을 했습니다. 만약 앱에서 에러가 발생했을 때 사용자들은 에러 발생 상황을 인지하지 못하는 문제가 있었습니다.

```swift
 do {
    itemList = try JSONDecoder().decode([Item].self, from: JSONFile.items.description)
    } catch NSDataAssetError.invalidDataAsset {
        ...
    } catch {
        print(error)
 }
```

🔑 **해결방법** <br>
앱을 사용하는 사용자들에게 에러 발생과 같은 크리티컬한 상황을 알리고 싶을 때, [UIAlertController](https://developer.apple.com/documentation/uikit/uialertcontroller)를 사용해서 사용자에게 [Alerts](https://developer.apple.com/design/human-interface-guidelines/alerts)을 표시하는 방법이 에러 발생 상황을 알리는 가장 좋은 방법이라고 생각했고, 일반적으로도 이러한 방법을 사용하고 있기 때문에 얼럿으로 에러 발생을 알리도록 추가했습니다.

```swift
do {
    itemList = try JSONDecoder().decode([Item].self, from: JSONFile.items.description)
} catch NSDataAssetError.invalidDataAsset {
    let alert: UIAlertController = AlertController().configureAlert(errorName: NSDataAssetError.invalidDataAsset.localizedDescription)
    present(alert, animated: true)
} catch {
    ...
}
```

<br>

8️⃣ **`NavigationCotroller` 사용** <br>
-
🔒 **문제점** <br>
화면 간의 전환에서 `navigation` 방식을 사용하여 `push`를 하여 다음 화면으로 넘어가도록 구성하였습니다. 코드베이스로 `UI`를 작성하면서 `NavigationController`를 초기화를 코드로 직접 해주어야 했습니다.

🔑 **해결방법** <br>
`SceneDelegate.swift` 파일에서 아래의 코드를 입력하였습니다

```swift
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    
    //UIWindowScene은 scene을 관리하고, UIScene은 앱에서 사용자 인터페이스를 표시하는 역할
    //UIScene을 상속받은 UIWindowScene으로 Scene을 관리하기 위해 다운 캐스팅을 사용
    guard let windowScene = (scene as? UIWindowScene) else { return }

    // SceneDelegate의 프로퍼티에 할당
    // windowScene이 관리할 window 생성
    window = UIWindow(windowScene: windowScene)

    // 맨 처음 보여줄 ViewController 할당
    let mainViewController: ExpositionUniverselleViewController = ExpositionUniverselleViewController()

    // mainViewController를 사용하여 UINavigationController 초기화
    // 네비게이션의 첫 화면을 mainViewController로 지정
    let navigationController: UINavigationController = UINavigationController(rootViewController : mainViewController)

    // 화면의 첫 시작 지점을 할당
    window?.rootViewController = navigationController

    // window를 화면에 표시하고 key window로 사용
    window?.makeKeyAndVisible()
}
```

위에서 `UINavigationController`의 `init`을 통해 네비게이션의 첫 화면을 지정하였습니다.

<br>

9️⃣ **화면의 회전 고정** <br>
-
🔒 **문제점** <br>
첫 화면에서는 `portrait mode`만 지원하고 싶었습니다. 이 외의 화면에는 모든 회전에 대해 지원하고 싶었습니다. 전체 앱의 동작에 대한 화면은 프로젝트의 `Deployment Info`에서 설정할 수 있었습니다. 하지만 각각의 화면에 따라 회전에 대한 제한을 걸기는 힘들었습니다. 

🔑 **해결방법** <br>
첫 번째 화면은 세로로 고정되어야 했고, 알아보던 중 `appDelegate`를 이용한 방법을 사용하게 되었습니다. `appDelegate`는 앱의 라이프 사이클과 이벤트를 처리하는 객체로써 앱의 시작, 종료 및 상태변화 등과 관련된 작업을 수행합니다. `AppDelegate.swift`에 다음 코드를 작성하였습니다.

```swift
// AppDelegate.swift
var isOnlyPortrait: Bool = true

func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
    if isOnlyPortrait {
        return [.portrait]
    } else {
        return [.all]
    }
}

// ExpositionUniverselleViewController.swift
override func viewWillAppear(_ animated: Bool) {
    super.viewWillAppear(animated)
    delegate?.isOnlyPortrait = true
}

override func viewWillDisappear(_ animated: Bool) {
    super.viewWillDisappear(animated)
    delegate?.isOnlyPortrait = false
}
```

만약 `isOnlyPortrait`가 `true`일 때 인터페이스 방향을 나타내는`UIInterfaceOrientationMask`가 세로 방향만을 허용하도록 `[.portrait]`을 반환하고, false인 경우 모든 방향을 허용하도록 `[.all]`을 반환하도록 하였습니다. `isOnlyPortrait`를 화면 고정을 원하는 첫 번째 뷰에서 뷰가 나타날 때 viewWillAppear에서 true로 설정해서 세로로 고정되게 하고, 다른 뷰로 넘어갈 때 viewWillDisappear에서 false로 설정해서 화면이 모든 방향을 허용하도록 하였습니다.

<br>

🔟 **`Dynamic Type`** <br>
-
🔒 **문제점** <br>
1. `Dynamic Type`은 사용자가 원하는 텍스트 사이즈로 컨텐츠를 볼 수 있는 유연성을 제공하는 기능입니다. 이 기능을 사용하여 텍스트의 크기를 키우게 되면 `...`으로 표시되는 문제가 있었고, 이로 인해 사용자가 텍스트의 크기를 크게 했을 때 내용을 알 수 없는 문제가 있었습니다.

2. `Dynamic Type`을 사용하여 텍스트의 크기를 키울 때, 버튼의 텍스트는 커지지만 버튼 자체의 크기와 버튼이 담긴 스택뷰의 크기가 커지지 않는 문제가 있었습니다.

🔑 **해결방법** <br>
1. `UILabel`의 `numberOfLines`는 `UILabel`에서 텍스트를 표시할 줄의 최대 개수를 지정하는 데 사용이 되고, 아무것도 지정하지 않으면 기본값이 `1`로 지정되는 것을 알게 되었습니다. 이 값이 `1`일 때 텍스트가 한 줄에만 표시되기 때문에 `numberOfLines = 0`으로 설정을 해서 만약 넓이가 초과하는 경우 줄바꿈이 되도록 변경했습니다.

2. `이미지 - 버튼 - 이미지`의 형태로 하나의 스택뷰에 묶여있었고, 이미지의 제약조건이 버튼을 참조하도록 했습니다. 이런 경우 이미지의 우선순위가 높아져 버튼 크기가 변경되지 않은 문제였습니다. 이미지의 제약조건에 문제가 있었기 때문에 이를 제거하고, `setContentCompressionResistancePriority(_:for:)`메서드를 사용해서 이미지뷰의 `CompressionResistance Priority`를 조절했습니다.

    ```swift
    private let rightItemImage: UIImageView = {
        let imageName: String = "flag"
        let image: UIImage? = UIImage(named: imageName)
        let imageView: UIImageView = UIImageView(image: image)
        imageView.contentMode = .scaleAspectFit
        imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)

        return imageView
    }()

    private let leftItemImage: UIImageView = {
        let imageName: String = "flag"
        let image: UIImage? = UIImage(named: imageName)
        let imageView: UIImageView = UIImageView(image: image)
        imageView.contentMode = .scaleAspectFit
        imageView.setContentCompressionResistancePriority(.fittingSizeLevel, for: .vertical)

        return imageView
    }()
    ```
    
    `setContentCompressionResistancePriority(_:for:)` 메서드는 뷰가 적합한 크기를 유지하기 위해 우선순위를 설정하는 메서드로, 첫 번째 매개변수는 뷰가 적합한 크기를 유지하기 위한 우선순위를 나타내고 전달인자를 기본 값 보다 낮은 `.fittingSizeLevel`으로 설정해서 이미지뷰의 크기에 스택뷰의 우선순위가 낮아지지 않도록 했습니다.

    두 번째 매개변수는 수평 방향, 수직 방향 등 크기 조정에 대해 우선순위를 설정하는 것으로, 첫 번째 페이지는 수직으로만 표현되면 되기 때문에 전달인자를`.vertical`로 설정해서 뷰가 수직 방향으로 압축되는 경우 내용이 잘리지 않도록 했습니다.
<br>

</br>

## 📚 참고 링크
- [🍎Apple Docs: JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)
- [🍎Apple Docs: UITableView](https://developer.apple.com/documentation/uikit/uitableview)
- [🍎Apple Docs: Filling a table with data](https://developer.apple.com/documentation/uikit/views_and_controls/table_views/filling_a_table_with_data)
- [🍎Apple Docs: Encoding and Decoding Custom Types](https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types)
- [🍎Apple Docs: Typography - Dynamic Type](https://developer.apple.com/design/human-interface-guidelines/typography#iOS-iPadOS-Dynamic-Type-sizes)
- [🍎Apple Docs: loadView()](https://developer.apple.com/documentation/uikit/uiviewcontroller/1621454-loadview)
- [📘stackOverflow: NSAttributedString](https://stackoverflow.com/questions/43723345/nsattributedstring-change-the-font-overall-but-keep-all-other-attributes)
- [📘blog: NavigationController를 rootViewController로 지정하기](https://vanillacreamdonut.tistory.com/277)
- [📘blog: Hugging, Compression Resistance Priority](https://ios-development.tistory.com/673)
- [📘blog: 추상화 수준](https://velog.io/@mincho920/%ED%81%B4%EB%A6%B0%EC%BD%94%EB%93%9C-%ED%95%A8%EC%88%98%EC%9D%98-%EC%B6%94%EC%83%81%ED%99%94-%EC%88%98%EC%A4%80-%EA%B7%B8%EB%A6%AC%EA%B3%A0-Switch-%EB%AC%B8)

</br>

## 👥 팀 회고
- [팀 회고 링크](https://github.com/idinaloq/ios-exposition-universelle/wiki/%ED%8C%80-%ED%9A%8C%EA%B3%A0)



# 팀 회고
## 우리팀이 잘한 점
- 프로젝트 진행 기간안에 익숙하지 않은 코드베이스로 STEP3까지 구현한 점
- 발생하는 에러의 원인과 해결 방법을 빠르게 찾아낸 점

## 서로에게 좋았던 점 피드백
- to. idinaloq: 꼼꼼하게 이해하지 못한 부분에 대해 짚고 넘어가는 모습에서 배울 점이 무척 많았습니다. 특히 어떠한 기능을 추가하거나 사용할 때 이게 실제 적용을 했을 때 장단점이 무엇인지 비교하여 필요한 부분만을 적용하는 것이 많이 도움 되었습니다.

- to. hoon: 코드베이스로 작성하는 것이 처음이었는데, 촉박한 시간에도 불구하고 하루 정도 공부하는 시간을 갖은 것이 도움이 많이 되었습니다.
    

## 서로에게 하고싶은 말
- to. idinaloq: 팀으로 만나 정말 오랜 시간같이 프로젝트를 진행한 것 같습니다. 공부하면서 새로운 기능을 추가할 때 이걸 왜 사용해야지라고 생각하는 모습을 보며 꼭 배우고 싶은 부분이라고 생각했습니다. 다음에 또 다른 미션으로 같이 하면 좋겠습니다.😆
    
- to. hoon: 다음부턴 늦지 않겠습니다.🙇‍♂️
    





# PR: STEP 1

## 고민되었던 점

### Assets import 폴더 구성 
- 제공받은 expo_assets 폴더는 내부에 각각의 폴더를 가지고 있는 형태로 폴더가 구성되어 있었습니다. expo_assets 폴더 자체를 import 하면 후에 이미지를 불러올 때에 파일명으로 접근 시 폴더명과 함께 파일명을 입력해야 하는 부분이 있어 내부 폴더만을 개별적으로 import 하였습니다.

### Content.json
- `Asset`을 추가할 때 데이터 파일 외에 이미지 파일에 `Content.json`라는 `json`포멧의 파일이 있었습니다. 해당 파일도 추가를 해야 될지 고민을 하였는데, 여러 가지로 검색해 본 결과 `Contents.json` 파일은 `Xcode`에서 `Asset Catalog`에 포함된 리소스의 이름, 유형, 크기 및 기타 속성과 관련된 정보를 포함하는 파일인 것을 알게 되었습니다. 따라서 따로 추가하지는 않았습니다.


### Decodable
- `typealias Codable`에는 `Encodable`, `Decodable` 기능이 있지만, 이번 프로젝트에서는 인코딩을 할 필요가 없다고 생각해서 `Decodable`을 사용하였습니다.

### CodingKeys 사용
- Json 포멧 네이밍에 `_`가 있었지만, `camelCase` 사용을 위해 `enum CodingKeys: String, CodingKey{...}`를 추가해서 `camelCase`에 맞는 이름으로 변경하였습니다.

## 조언을 얻고 싶은 부분

### MVC 폴더 그룹화
- 현재 MVC 패턴에 맞추어 파일을 그룹화하였습니다. 올라프라면 어떻게 파일을 구분하여 나눌지 궁금합니다!

MVC 기준
- Model: 앱에서 사용하는 데이터와 비지니스 로직을 담당합니다.
- View: 데이터를 시각적으로 표현하는 역할을 담당합니다.
- Controller: Model과 View 사이의 이벤트 처리를 담당합니다.

Model: `ExpositionUniverselle.swift` 파일과 `Item.swift` 파일은 데이터와 비지니스 로직을 담당하는 부분이라고 생각했습니다.

View: `Main.storyboard`는 사용자에게 시각적으로 보이는 부분을 담당하고 있다고 생각했습니다.

Controller: `AppDelegate.swift`와 `SceneDelegate.swift`같은 경우는 직접적으로 `Model`과 `View` 사이의 중계자 역할을 하지는 않지만 `MVC`패턴으로 구분한다면 컨트롤러 역할에 좀 더 가깝다고 생각합니다.

그렇게 생각한 이유는 `AppDelegate`같은 경우 특정한 `scenes`, `views`, `view controllers`에 한정되지 않고 앱 자체를 타겟하는 이벤트에 대응하는 역할을 수행하며, `SceneDelegate`은 `UI` 상태에 따른 이벤트 처리를 담당하기 때문입니다.


`let`으로 수정하였습니다.
공식 문서에서 예시로 있는 샘플 코드는 `var`를 사용하였습니다. 하지만 저희가 찾아본 내용으로는 후에 값이 변경될 일이 없는 경우에는 `let`을 사용하여 안정성을 높일 수 있다고 생각하였습니다. 공식 문서의 예시 코드에는 이후에 변경에 대한 예시는 없지만 `var`를 사용해 변수로 사용하고 있는데 여기서 이렇게 사용하는 이유가 있을까요?

예시 코드: [JSONDecoder](https://developer.apple.com/documentation/foundation/jsondecoder)




안녕하세요 올라프(@1Consumption)😊 hoon, idinaloq입니다. 
만국박람회 STEP 2 PR 드립니다. 처음으로 코드 베이스로 UI를 구현하며 많은 어려움이 있었고 여전히 부족한 점이 많은 것 같습니다.
천천히 보시고 코멘트 남겨주시면 열심히 공부해서 다시 답변 남기도록 하겠습니다. 감사하겠습니다.🙇
좋은 주말 보내세요.🥳

# PR: STEP 2

## 고민되었던 점

### 오토레이아웃 사용
- 스토리보드를 사용하지 않고 코드 베이스로 뷰를 생성하여 `addSubview`를 통해 뷰를 추가하면 시뮬레이터에 나오지 않았습니다. 생성한 뷰를 화면에 어느 위치에 띄워주어야 하는지 판단하지 못하여 생긴 문제였습니다. 이를 해결해 주기 위해 오토레이아웃을 사용하여 화면의 어느 위치에 뷰를 띄울지에 대한 제약조건을 설정하였습니다.

    ```swift
    private func configureExpositionUniverselleScrollViewConstraint() {
        NSLayoutConstraint.activate([
            expositionUniverselleScrollView.topAnchor.constraint(equalTo: view.topAnchor),
            expositionUniverselleScrollView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
            expositionUniverselleScrollView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
            expositionUniverselleScrollView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
        ])
    }
    
    private func configureContentStackViewConstraint() {
        NSLayoutConstraint.activate([
            contentStackView.topAnchor.constraint(equalTo: expositionUniverselleScrollView.topAnchor),
            contentStackView.bottomAnchor.constraint(equalTo: expositionUniverselleScrollView.bottomAnchor),
            contentStackView.widthAnchor.constraint(equalTo: expositionUniverselleScrollView.widthAnchor)
        ])
    }
    ```

### 뷰 업데이트 시점
- 뷰 컨트롤러에서 연산 프로퍼티를 사용하여 뷰를 생성할 때 `UILabel`의 경우 입력되어야 하는 내용들을 함께 입력하여 생성하였습니다.

    ```swift
    private var expositionUniverselle: ExpositionUniverselle?

    private lazy var titleLabel: UILabel = {
        let label: UILabel = UILabel()
        label.text = = expositionUniverselle?.title
        label.textAlignment = .center
        
        return label
    }()
    ```
    
    위와 같이 사용하면 `expositionUniverselle`이 초기화 전에 값에 접근하여 `title` 값을 받아오려고 하기 때문에 `lazy var`를 사용해야 했습니다. 이러한 문제를 해결하기 위해 뷰를 생성하고 이후에 `updateLabel()` 메서드에서 `label.text`에 값을 할당하는 작업들을 모아서 진행하도록 하였습니다.
    
    ```swift
    private var expositionUniverselle: ExpositionUniverselle?

    private let titleLabel: UILabel = {
        let label: UILabel = UILabel()
        label.textAlignment = .center
        
        return label
    }()

    ...

    private func updateLabel() {
        navigationItem.title = "메인"
        titleLabel.text = expositionUniverselle?.titleForLabel
        visitorsLabel.text = expositionUniverselle?.visitorsForLabel
        locationLabel.text = expositionUniverselle?.locationForLabel
        durationLabel.text = expositionUniverselle?.durationForLabel
        totalDescriptionLabel.text = expositionUniverselle?.totalDescription
    }
    ```

### `addArrangedSubview()` 메서드 중복 제거
- `ExpositionUniverselleViewController`의 첫 화면에서는 다양한 뷰들을 사용하여 전체 화면을 구성하였습니다. 많은 뷰들을 `contentStackView`에 추가하여 관리하기 위해 아래와 같이 여러 번의 동일한 `addArrangedSubview()` 메서드를 작성해야 했습니다.

    ```swift
    override func viewDidLoad() {
        super.viewDidLoad()
        decodeExpositionUniverselle()
        horizontalStackView.addArrangedSubview(leftFlagImage)
        horizontalStackView.addArrangedSubview(expositionItemListButton)
        horizontalStackView.addArrangedSubview(rightFlagImage)
        verticalStackView.addArrangedSubview(titleLabel)
        verticalStackView.addArrangedSubview(posterImage)
        verticalStackView.addArrangedSubview(visitorsLabel)
        verticalStackView.addArrangedSubview(durationLabel)
        verticalStackView.addArrangedSubview(locationLabel)
        verticalStackView.addArrangedSubview(totalDescriptionLabel)
        verticalStackView.addArrangedSubview(horizontalStackView)
        expositionUniverselleScrollView.addSubview(verticalStackView)
        view.addSubview(expositionUniverselleScrollView)
    }
    ```
    
    `addArrangedSubview()` 메서드에 매개변수로 주어지는 뷰만 변경하면 중복된 코드를 줄일 수 있어 가독성을 높일 수 있었습니다.
    
    ```swift
    private func configureView() {
        addArrangedSubviews(to: buttonStackView, elements: [leftItemImage,
                                                            expositionItemListButton,
                                                            rightItemImage
                                                           ])
        addArrangedSubviews(to: contentStackView, elements: [titleLabel,
                                                             posterImage,
                                                             visitorsLabel,
                                                             locationLabel,
                                                             durationLabel,
                                                             totalDescriptionLabel,
                                                             buttonStackView])
        expositionUniverselleScrollView.addSubview(contentStackView)
        view.addSubview(expositionUniverselleScrollView)
    }
    
    private func addArrangedSubviews(to superview: UIStackView, elements subviews: [AnyObject]) {
        subviews.forEach {
            guard let subview = $0 as? UIView else { return }
            superview.addArrangedSubview(subview)
        }
    }
    ```
    
    위와 같이 `addArrangedSubviews()` 메서드에 여러 뷰들을 `Array`로 묶어 매개변수로 넘기면 순차적으로 `superview`에 `subview`들이 추가되도록 하였습니다.

### 매직리터널 최소화
- `exposition_universelle_1900`, `items`같은 `JSON` 파일에서 직접적으로 파일을 읽어올 때 아래와 같이 파일 이름을 직접 지정하여 읽어오도록 하였습니다.

    ```swift
    private func decodeExpositionUniverselle() {
        do {
            expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: "exposition_universelle_1900")
        } catch {
            print(error)
        }
    }
    ```
    
    추후 파일의 이름이나 파일 자체가 바뀌는 경우 파일 이름을 계속 변경해야 되는데, 만약 뷰 컨트롤러가 커지거나 파일이 많아지는 경우 일일이 파일명을 입력하는 코드를 찾아 변경하기 어려울 것 같다고 생각했습니다. 이를 해결하기 위해 `JSONFile.swift` 파일을 만들고 파일 이름을 관리할 수 있도록 했습니다.
    
    ```swift
    enum JSONFile {
        case exposition
        case items

        var name: String {
            switch self {
            case .exposition:
                return "exposition_universelle_1900"
            case .items:
                return "items"
            }
        }
    }
    ```
    
    이렇게 하나의 파일에서 JSON 파일을 관리하기 때문에 파일의 수정이 필요한 경우 파일 명을 입력하는 코드를 찾아갈 필요 없이 `JSONFile.swift` 파일에서 모두 해결할 수 있었습니다. 위의 `JSONFile` 타입을 적용하여 아래와 같이 코드를 수정하였습니다.
    
    ```swift
    private func decodeExpositionUniverselle() {
        do {
            expositionUniverselle = try JSONDecoder().decode(ExpositionUniverselle.self, from: JSONFile.exposition.name)
        } catch {
            print(error)
        }
    }
    ```

### 세 번째 뷰로 데이터 넘기기
- 두 번째 뷰 컨트롤인 `ExpositionItemViewController`에서 `JSON` 데이터를 사용할 때 `decodeItems()` 메서드를 통해 디코딩 된 데이터를 `itemList`에 할당합니다. 세 번째 뷰 컨트롤인 `ExpositionItemListViewController`에서도 위와 같이 디코딩을 하면 같은 결과가 나오지만, 동일한 파일을 두 번 디코딩 하는 것보다 두 번째 뷰 컨트롤에서 다음 뷰 컨트롤러에 데이터를 넘겨주는 편이 동일한 작업을 여러 번 반복하지 않아 좋다고 생각했기 때문에 아래와 같이 구현했습니다.

    ```swift
    func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
        tableView.deselectRow(at: indexPath, animated: true)
        let expositionItemViewController: ExpositionItemViewController = ExpositionItemViewController(item: itemList[indexPath.row])
        navigationController?.pushViewController(expositionItemViewController, animated: true)
    }
    ```

### `JSONDecoder Extension`
- 같은 `decode()` 메서드를 현재 두 번 사용하고 있는데, `do - try - catch` 과정에서 `NSDataAsset`을 옵셔널 바인딩을 해서 사용하고 있었습니다. `decode()` 메서드를 사용하는 횟수가 늘어난다면, `NSDataAsset`에 대한 옵셔널 바인딩 코드가 중복되는 것 같다고 생각하여 `Extension`으로 `decode`의 기능을 다음과 같이 확장하여 사용했습니다.

    ```swift
    extension JSONDecoder {
        func decode<T: Decodable>(_ type: T.Type, from assetName: String) throws -> T {
            guard let json = NSDataAsset(name: assetName) else {
                throw NSDataAssetError.invalidDataAsset
            }

            return try JSONDecoder().decode(type, from: json.data)
        }
    }
    ```

### `CustomCell` 사용
- 두 번째 뷰에서 각각의 셀이 `subtitle`을 필요로 했습니다.

    ```swift
    cell.detailTextLabel?.text = "Subtitle"
    ```
    
    위와 같이 선언해서 `subtitle`을 사용했는데, 뷰에는 적용되지 않는 문제가 있었고, 코드 베이스로 작성해서 이러한 문제가 있다고 생각했습니다. 커스텀 셀을 사용해서 `style`을 `subtitle`로 다음과 같이 설정했습니다.
    
    ```swift
    final class ItemUITableViewCellStyleSubtitle: UITableViewCell {
        override init(style: UITableViewCell.CellStyle, reuseIdentifier: String?) {
            super.init(style: .subtitle, reuseIdentifier: reuseIdentifier)
            accessoryType = .disclosureIndicator
        }
        
        ...
    }
    ```

## 해결이 되지 않은 점

### `massive viewController`
- 스토리보드로 `UI`를 구현할 때 아이콘을 스토리보드에 생성하는 순간 `UI`가 생성되고, 그것을 `ViewController`와 각각의 `UI`들을 연결해 주는 작업을 진행했었습니다. 코드 베이스로 변경된 지금의 코드를 보면, `ViewController`에서 `UI`를 생성하고 연결까지 하는 두 개의 동작을 하기 때문에 UI를 생성하는 부분은 `View`에 들어가야 된다고 생각합니다. 이러한 과정들이 어떻게 나눠지고, 실행되어야 하는지에 대해 잘 알지 못하여 `ViewController`에 모두 구현한 상태로 이 부분을 아직 해결하지 못하였습니다.

- 고민하신 점에 적어주신 아이콘을 스토리보드에 생성하는 순간 UI가 생성되고, 라는 부분이 어떤 것을 뜻하는지 잘 모르겠습니다. 이 부분을 조금 더 자세하게 설명해 주시겠어요?
    - 스토리보드로 UI를 구현할 때 아이콘을 스토리보드에 추가하면 XML에도 추가가 되어 UI에 대한 정보가 기록된다고 생각했습니다.
    - 

- 여기 있는 코드들의 추상화 수준이 안맞네요. 역할별로 메소드로 분리해보시겠어요?
    - clean code에서 추상화 레벨 공부해보기 

- print를 하면 앱을 사용하는 사용자들은 에러 발생 상황을 인지하지 못합니다. 어떤 방법이 있을까요?
    - 얼럿으로 표시하도록 수정하기

- 이 코드의 역할은 뭔가요? 왜 해주셨나요?
    - `translatesAutoresizingMaskIntoConstraints`의 [공식 문서](https://developer.apple.com/documentation/uikit/uiview/1622572-translatesautoresizingmaskintoco)를 보면 뷰의 `Autoresizing mask`를 `Auto Layout constraints`로 바꿀지를 결정하는 `Boolean` 값이라고 설명합니다. `translatesAutoresizingMaskIntoConstraints`의 값이 `true` 일 때 시스템은 뷰의 `Autoresizing mask`에 의해 지정된 제약 조건을 생성합니다. 이러한 경우에 새로운 제약 조건을 추가하면 이전 제약 조건과 충돌이 발생할 수 있습니다. 이를 해결하기 위해 `translatesAutoresizingMaskIntoConstraints`의 값을 `false`로 설정합니다.

- 클로저 생성을 통해 인스턴스를 만들어준 이유는 뭔가요? 이렇게하면 어떤 장점이 있던가요?
    - 클로저를 사용하여 인스턴스를 생성함으로써 관련된 코드를 블록화하여 가독성을 높일 수 있었습니다. 뷰를 생성하고 이에 필요한 프로퍼티들을 함께 설정함으로써 하나의 코드 블록 안에서 필요한 모든 동작을 수행해 가독성을 높였습니다.

- 하나하나 접근하지말고 메소드 하나로 접근을 제어해보는건 어떨까요?
    - `configureCell(with:item:)` 메서드를 사용하여 셀에 접근하도록 수정하였습니다.
    - 내부에서 셀에 각각의 데이터를 저장하도록 수정하였습니다.

- final을 붙여준 이유가 궁금합니다. 이 키워드는 어떤 역할을 하나요?
    - [final](https://docs.swift.org/swift-book/documentation/the-swift-programming-language/inheritance/#Preventing-Overrides) 키워드는 더 이상 상속이 필요 없음을 명시적으로 표현하며, 오버라이드 할 시 컴파일 에러를 발생시킬 수 있습니다.

```swift 
required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }
```
- 이 이니셜라이저는 언제 불리나요? fatalError를 해준 의도가 궁금합니다.
    - 상속받은 `init()` 메서드 대신 `ExpositionItemViewController`에서 지정 생성자를 직접 구현했습니다. 현재 코드베이스를 통해 초기화를 진행하고 있고, 스토리보드나 xib 파일을 사용하는 경우는 없기 때문에 `required init?` 메서드가 직접 호출될 일은 없다고 생각합니다. 만약 호출된다면 비정상적인 작동이기 때문에 `fatalError`를 통해 앱이 실행 종료되도록 했습니다.

- 이 코드의 역할이 궁금합니다. 한줄한줄 설명해주시겠어요?
    - 이 코드는 네이게이션 컨트롤러를 루트뷰 컨트롤러로 지정해주는 역할입니다.
    ```swift
    //UIWindowScene은 scene을 관리하고, UIScene은 앱에서 사용자 인터페이스를 표시하는 역할
    //UIScene을 상속받은 UIWindowScene으로 Scene을 관리하기위해 다운캐스팅을 사용
    guard let windowScene = (scene as? UIWindowScene) else { return }
        
    // SceneDelegate의 프로퍼티에 할당
    // windowScene이 관리할 window 생성
    window = UIWindow(windowScene: windowScene)
        
    // 맨 처음 보여줄 ViewController 할당
    let mainViewController = ExpositionUniverselleViewController()
        
    // mainViewController를 사용하여 UINavigationController 초기화
    // 네비게이션의 첫 화면을 mainViewController로 지정
    let navigationController = UINavigationController(rootViewController : mainViewController)
        
    // 화면의 첫 시작지점을 할당
    window?.rootViewController = navigationController
        
    // window를 화면에 표시하고 key window로 사용
    window?.makeKeyAndVisible()
    }
    ```


안녕하세요 올라프()😊 마지막 PR 드립니다.
2주간 정신없이 정말 빠르게 시간이 흘러간 것 같습니다.
코멘트 남겨주시면 마지막까지 열심히 마무리해 보도록 하겠습니다.🔥
짧은 시간이지만 올라프에게 정말 많이 배울 수 있었습니다.
다음에 다른 미션으로 또 만났으면 좋겠습니다. 감사합니다.🙇

# PR: STEP 3

## 고민되었던 점
### 이미지 비율 문제
- 처음에 이미지에 제약조건을 주었고, 그에 따라 이미지의 크기가 변하였습니다. 하지만 이미지의 크기가 강제로 잡아 늘린 것처럼 되었습니다. `contentMode`의 기능에 이미지 비율에 따라 늘려주는 기능인 `scaleAspectFit`을 사용해서 이미지의 크기를 조절할 때 비율에 맞춰 조정되도록 설정하였습니다.

### 커스텀 셀 사용
- `ItemUITableViewCellStyleSubtitle`을 사용하여 `subtitle` 스타일의 기본 셀을 사용하였습니다. 하지만 셀의 아이템들은 각각 다른 이미지 크기를 가지고 있기 때문에 셀의 `imageView`에 제약조건을 추가하여야 했습니다. 기본 셀의 경우 각각의 뷰에 대한 제약을 설정하기가 어려웠습니다. 각각의 뷰에 대해 제약을 추가하기 위해 커스텀 셀을 사용하여 contentView에 들어가는 뷰들의 제약조건을 추가하였습니다.
- `itemImageView.heightAnchor`의 높이를 기존에는 셀의 `itemImageView.widthAnchor`의 크기에 맞추어 제약조건을 추가하였습니다.

    ```swift
    private func configureItemImageViewConstraint() {
        NSLayoutConstraint.activate([
            itemImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
            itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            itemImageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.2),
            itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor),
            itemImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
            itemImageView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8)
        ])
    }
    ```
    
    이런 경우에 화면이 회전하면 `itemImageView`의 높이가 재설정 되며 `topAnchor`와 `bottomAnchor`의 제약을 통해 생긴 `imageItemView`의 높이와 충돌이 발생하였습니다. 이를 해결하기 위하여 제약조건에 우선순위를 선정하였습니다.
    
    ```swift
    private func configureItemImageViewConstraint() {
        NSLayoutConstraint.activate([
            itemImageView.leadingAnchor.constraint(equalTo: contentView.leadingAnchor, constant: 8),
            itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            itemImageView.widthAnchor.constraint(equalTo: contentView.widthAnchor, multiplier: 0.2),
            itemImageView.topAnchor.constraint(greaterThanOrEqualTo: contentView.topAnchor, constant: 8),
            itemImageView.bottomAnchor.constraint(lessThanOrEqualTo: contentView.bottomAnchor, constant: -8)
        ])
        
        let itemImageViewHeightConstraint: NSLayoutConstraint = itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor)
        itemImageViewHeightConstraint.priority = .defaultHigh
        itemImageViewHeightConstraint.isActive = true
    }
    ```

### 화면의 회전 고정
- 첫 번째 화면은 세로로 고정되어야 했고, 알아보던 중 `appDelegate`를 이용한 방법을 사용하게 되었습니다. `appDelegate`는 앱의 라이프 사이클과 이벤트를 처리하는 객체로써 앱의 시작, 종료 및 상태변화 등과 관련된 작업을 수행합니다.
- `appDelegate`에 다음 코드를 작성하였습니다.

    ```swift
    var changeOrientation: Bool = true

    func application(_ application: UIApplication, supportedInterfaceOrientationsFor window: UIWindow?) -> UIInterfaceOrientationMask {
            if !changeOrientation {
                return [.all]
            } else {
                return [.portrait]
            }
        }
    ```
    
    만약 `changeOrientation`가 `true`일 때 인터페이스 방향을 나타내는`UIInterfaceOrientationMask`가 모든 방향을 허용하도록 `[.all]`을 반환하고, `false`인 경우 세로 방향만을 허용하도록 `[.portrait]`를 반환하도록 했습니다.
    `changeOrientation`을 화면 고정을 원하는 첫 번째 뷰에서 뷰가 나타날 때 `viewWillAppear`에서 `true`로 설정해서 세로로 고정되게 하고, 다른 뷰로 넘어갈 때 `viewWillDisappear`에서 `false`로 설정해서 화면이 모든 방향을 허용하도록 했습니다.

### Dynamic type
- Dynamic Type은 사용자가 원하는 텍스트 사이즈로 컨텐츠를 볼 수 있는 유연성을 제공합니다. 
- 이 부분에 대해서 기존에 Dynamic Type으로 텍스트 크기를 키우게 되면 ...으로 표시되는 문제로 인해 사용자가 내용을 알 수 없는 문제가 있었습니다. `numberOfLines`의 값을 0으로 설정해서 모든 텍스트 레이블에 대해 텍스트의 크기가 커져도 내용을 알 수 있도록 `...`이 아닌 여러 줄로 표시하도록 설정했습니다.

## 해결이 되지 않은 점
### Layout 문제
- 두 번째 화면인 `ExpositionItemListViewController`에서 debug view hierarchy를 확인하면 다음과 같은 에러가 발생합니다.

    ```
    Height and vertical position are ambiguous for UIImageView.
    ```

    에러 메시지를 확인하면 `UIImageView`의 높이와 y축의 좌표가 불분명하다는 내용으로 이해했습니다. 하지만 제약 조건을 추가하는 부분을 보면 다음과 같습니다.
    
    ```swift
    private func configureItemImageViewConstraint() {
        NSLayoutConstraint.activate([
            ...
            
            itemImageView.centerYAnchor.constraint(equalTo: contentView.centerYAnchor),
            
            ...
        ])
        
        let itemImageViewHeightConstraint: NSLayoutConstraint = itemImageView.heightAnchor.constraint(equalTo: itemImageView.widthAnchor)
        itemImageViewHeightConstraint.priority = .defaultHigh
        itemImageViewHeightConstraint.isActive = true
    }
    ```

    이처럼 직접 `heightAnchor`를 통해 `itemImageView`의 높이를 `centerYAnchor`를 통해 y축에 대한 제약조건을 추가하고 있습니다. 위의 에러는 어떻게 해결해야 할까요? 

### ImageView에 제약조건 추가시 dynamicType 적용
- 개별적인 이미지 파일의 크기를 `ImageView`에서 통일된 크기로 보여주기 위하여 `ImageView`에 제약조건을 추가하였습니다. `ImageView`에 텍스트와 같이 다이나믹타입을 적용해서 늘어나거나 줄어들게 하고 싶었으나, `ImageView`의 제약조건 때문에 다이나믹타입 적용이 불가능한 문제가 있습니다. 이런 경우에는 어떻게 해결할 수 있을까요?


