---
# System prepended metadata

title: 개요

---

### Yagom iOS 커리어 스타터 캠프

## 프로젝트 6 - 만국박람회

# 개요

1. 프로젝트 기간: '21.12.06 ~ '21.12.17
2. 캠퍼 : 나무, 제이티, 숲재
3. 리뷰어 : 흰

# 학습 키워드
1. Step1
- `JSON` 
- `Codable`
- `NSDataAsset` 
2. Step2

# STEP1 PR & ReadME

## 고민한 점
1. Codable? Decodable? 

`Codable` 프로토콜은 `typealias Codable = Decodable & Encodable` 이렇게 두 가지 프로토콜을 결합한 형태입니다. 저희 프로젝트에서는 미리 준비된 데이터를 파싱하여 사용하기만 하지, 가공하거나 생성하여 통신하는 등의 Encoding 작업은 하지 않을것으로 예상했습니다. 

그래서 `Codable` 을 채택하면 코드를 읽는데 있어 불필요한 오해의 소지가 있을 것 같아, 대신 `Decodable`을 채택해 주었습니다.


2. 연산 프로퍼티 사용을 통한 넘버 포맷팅
```swift=
extension ExpoInfo {
    var formattedVisitors: String {
        let numberFormatter = NumberFormatter()
        
        numberFormatter.numberStyle = .decimal
        guard let formatted = numberFormatter.string(for: self.visitors) else {
            return ""
        }
        
        return formatted + " 명"
    }
```

스텝2 요구사항을 참고하여 첫번째 화면의 `방문객`에 표시되어야 할 정보를 형식 변환하는 연산프로퍼티를 추가했습니다. VC에서는 View를 표시하는 것 외의 역할을 하지 않도록 데이터를 원하는 형태로 미리 가공을 해두는 방식을 택했습니다. 

## 궁금한 점
1. Parsing 테스트 방법
파싱이 원활히 작동하는지 테스트 해보기 위해서 `XCTAssertNoThrow` 메소드를 통하여 `decode` 메소드가 에러를 던지는지의 여부를 확인했습니다. 추가로 파싱된 인스턴스의 프로퍼티값을 검증하기 위해 `print`문을 사용하여 확인했는데, 이러한 확인 과정이 필요한건지? 만약 필요하다면 `print`문을 사용해 출력해보는 것 외에 좋은 방법이 없는지 궁금합니다. `XCTAssertEqual`메소드를 고민해 보았는데, `description` 프로퍼티와 같은 너무 긴 문자열이 테스트 코드에 포함되게 되어 진행하지 않았습니다. 

---
전반적인 의견
코드에 줄바꿈이 너무 많아요.(테스트 코드 포함) 과연 의미있는 줄바꿈인지 생각해보면 좋을 것 같습니다.

질문에 대한 답변
에러 던지는지 확인하는 부분이 필요한데 이 부분 적절히 테스트 되는 걸로 보이네요. XCTAssertEqual 을 사용해서 동일한지 확인하는게 테스트에서 필요하다고 판단하면 넣으시면 됩니다. 검증하기 위해 print 문을 사용하는건 의미가 없지 않나요? 단순히 출력만 하고 동일하거나 다른지 구별할 수는 없잖아요. 길더라도 검증에 필요하다면 적절한 테스트 메소드 사용하는 게 좋습니다.


> 의미있는 줄바꿈이 사용되었는지 생각해보면 좋을 것 같아요

처음에 GroundRule로 정했던 스타일이 선언부 - 호출부 - 반환부 기준으로 개행을 하자고 정했었는데, 흰 말씀대로 같은 맥락의 작업에서도 의미 없는 개행이 발생한 것 같아요. 그래서 해당 부분 개행은 삭제해 줬습니다. 다만, 마지막 `return` 에 대해서는 구분해 주는 것이 가독성에 도움이 되는 것 같아 개행을 유지했습니다. 

테스트 코드에서는 XCT...메서드 호출 부분이 이 함수의 목적에 해당하는 부분이라 개행으로 구분하고자 했었는데, 이것도 의미 없는 개행이 되는걸까요? 어떤 기준으로 줄바꿈을 적용하면 좋을지 흰의 의견을 구하고 싶습니다!

> NumberFormatter 의 performance 에 대해서 알아보길 추천드려요

말씀해주신대로 `NumberFormatter`의 퍼포먼스에 대해 알아보았습니다. `let numberFormatter = NumberFormatter()` 와 같이 인스턴스를 생성하는 비용이 큰 것을 알게 되었습니다. 그래서 클래스의 `extension`에 싱글턴 프로퍼티를 생성하여 필요한 옵션을 가진 인스턴스를 미리 생성해 두고, 필요할 때 가져다 쓰는 방식으로 변경하였습니다. 

--------------------------------------------------------------------------

# STEP2 PR& ReadME
## 고민한 점
**1. TextView? Label?**

첫번째뷰와 세번째뷰에는 긴 스트링을 나타내는 UI요소가 필요한데, `Label`과 `TextView` 중 어느것을 선택할지 고민했습니다. 
Apple Human Interface Guidelines를 찾아보니 `TextView`는 여러줄의 텍스트 내용을 출력하는데 적합하고 `Label`은 짧은 메시지를 전달하는데 적합하다고 판단하여 `TextView`를 선택했습니다.
> A text view displays multiline, styled text content. (HIG TextView)

> A label describes an onscreen interface element or provides a short message. (HIG Label)

프로젝트 요구사항에서는 텍스트 뷰 안에 들어갈 내용이 단어 단위로 lineBreak되어 있는데, `TextView`가 기본적으로 가지고 있는 lineBreakMode 프로퍼티로는 한글로 이루어진 텍스트에 적용이 되지 않는 것을 확인했습니다. 그래서 `TextView`의 `attributedText`프로퍼티를 활용하여 `lineBreakStrategy`를 적용하여 해결했습니다. `label`을 사용했다면 기본적으로 `lineBreakStrategy` 프로퍼티를 가지고 있기 때문에 쉽게 해결되었을텐데, 저희가 `TextView`를 선택한 것이 좋은 선택이었는지 모르겠습니다 😅

## 궁금한 점

**1. 매직 스트링을 어떻게 관리해야 할까요?**

`ExpoInfoViewController`의 title, `AlertAction`의 title, `TableViewCell`과 `ItemsViewController`의 identifier 등을 코드 내부에 직접 적어주었는데, 유지보수 시 문제가 될 것 같다는 생각이 들었습니다. 필요한 스트링들을 모아서 관리하는 타입을 만드는 방법이 대안으로 생각나는데요, 혹시 다른 좋은 방법이 있을까요? 

**2. 어디까지를 storyboard로 하고 어디까지를 코드로 해야할까요?**

현재 저희 프로젝트에서는 일관성을 위하여 AutoLayout **Constraint**는 **모두 인터페이스 빌더로 설정**해 주었고, **그 외 설정값들을 각 뷰컨트롤러 코드에서 셋팅**합니다.
오토레이아웃 제약 이외의 부분들이 여기저기에서 관리된다면 추후 유지보수시 혼란이 있을 것 같다는 판단에서였습니다.
하지만 그로 인해, 현재 첫 번째 뷰의 Storyboard에서는 빨간색 에러가 발생하고 있습니다. (TextView의 Scroll이 Enabled되어 높이 값을 특정할 수 없으므로)
스토리보드와 코드를 병용하는 현 상황에서 뷰에 대한 기능들을 어떤 기준으로 코드/인터페이스빌더에서 설정해 줄지가 궁금합니다!

**3. guard let + 후행클로저??**

스토리보드를 통해 세 번째 VC에 접근하여 `ItemDetailViewController`로 인스턴스화 한 후에, 옵셔널 값인 스토리보드를 바인딩해 주려 하였으나 빨간색 에러 메세지가 나오며 빌드되지 않는 상황이 있었습니다.
```swift
guard let detailViewController =
        storyboard?.instantiateViewController(identifier: "itemDetail") { coder in
    return ItemDetailViewController(coder: coder, data: self.items[indexPath.row])
} else {
    return
}
// 에러 발생

```
후행 클로저 이후에 else를 써 주면 문법적으로 틀렸다고 인식하는 것으로 보입니다.
그래서 다음과 같이 수정해 주었는데요.
```swift
guard let detailViewController =
        storyboard?.instantiateViewController(identifier: "itemDetail", creator: { coder in
            return ItemDetailViewController(coder: coder, data: self.items[indexPath.row])
        }) else {
    return
}
// 빌드 성공
```
후행클로저를 사용하지 않고 creator 매개변수에 클로저를 직접 넘겨주는 방식입니다.

혹시 후행클로저를 사용하면서도 guard let을 사용할 수 있는 방법이 있을까요? 이것이 왜 문법에서 어긋난 것인지 궁금합니다.

## 이슈 & 해결방법
### 1. 스크롤뷰의 ContentsLayoutGuide가 의도치 않은 위치에서 시작되는 문제
첫 번째 화면의 스크롤뷰 내부 Constraint를 구현하는 도중, 내부 콘텐츠의 세로 길이가 일정 길이 이상인 경우와 그렇지 않은 경우에서 ContentsLayoutGuide의 시작지점에 차이가 생기는 문제가 발생했습니다.
ScrollView와 그 하위 ContentView의 배경색을 다르게 하여 눈으로 구분할 수 있게 테스트 해 보았습니다.

<img src="https://i.imgur.com/xMucjA6.png" width="40%" height="40%">

내부 콘텐츠의 세로 길이가 일정 길이 이하인 경우
<img src="https://i.imgur.com/aLpaLx8.png" width="40%" height="40%">

내부 콘텐츠의 세로 길이가 일정 길이 이상인 경우


- 콘텐츠의 세로 길이가 일정 길이 이상인 경우에는 Safety Area 아래에서부터 콘텐츠가 시작됨을 볼 수 있습니다.

### 결론
스크롤뷰는 **"스크롤될만큼 컨텐츠뷰 크기가 큰 경우"** 에는 **Safety Area 아래**에서부터 컨텐츠가 표시되도록 Contents Layout Guide를 조절하는 듯 합니다.

그렇지 않은 경우에는, Safety Area와 겹치더라도 y = 0 지점부터 컨텐츠를 표시하는 경우가 발생합니다.
이에 유의하여 스크롤뷰를 다루는 것이 좋을 것 같습니다.

---
### 2. 네비게이션 스택의 첫 번째 뷰에 NavigationBar가 표시되지 않도록 하려면?

프로젝트 요구사항에서는 다음과 같이 첫번째 뷰에서는 네비게이션 바가 표시되지 않고, **두 번째 화면으로 전환할 때 네비게이션 바가 함께 push/pop**되도록 요구하고 있습니다.
<img src="https://i.imgur.com/i57U9wV.png" width="40%" height="40%">
<img src="https://i.imgur.com/X9leBBt.png" width="40%" height="40%">

두 번째 화면부터 네비게이션 컨트롤러에 embed해야 할지 등등 여러 방법들을 고민했지만, push 방법으로 화면전환을 하기 위해서는 첫 번째 뷰도 네비게이션 컨트롤러에 embed되어있어야 했습니다. 고민 끝에 찾아낸 해결 방법은 다음과 같습니다.

### 결론
`viewWillAppear`와 `viewWillDisappear`에서 `setNavigationBarHidden(bool:animated:)` 메서드를 사용하여 해당 뷰를 벗어날 때는 내비게이션 바가 드러나고 다시 해당 뷰가 표시될 때는 내비게이션 바를 숨기도록 구현하였습니다. 그리고 해당 메서드의 `animated` 파라미터를 true로 설정했을 때 요구사항의 동작과 완벽히 일치하는 것을 확인할 수 있었습니다(두번째 뷰가 pop될 때 내비게이션 바가 서서히 사라지도록).


---
## Step2 리코멘트
1. 
> 모아서 관리하는 것도 좋은 방법입니다. 혹은 관련 있는 곳에 nested type 으로 관리하는 방식도 있어요. 절대적인 정답은 없으니 일단 시도해보고 더 좋은 방식이 있으면 알려주세요

- 폰트 상수들은 UIFont의 extension으로, String Literal들은 별도의 파일에 enum으로, identifier들은 각 타입의 타입 프로퍼티로 관리하도록 개선해 보았습니다!

2. 
> delegate 나 datasource 설정도 storyboard 에서 할 수 있으니 찾아보시길 바래요. 지금 constraint 가 안잡히는 이유는 scrollview 에서 content 의 높이를 알 수 없어서 그런 건데 요것도 잘 찾아보면 해결방법이 있어요. 지금 기준대로 제약 조건까지 storyboard 에서 지정하는 방식을 저도 회사에서 사용하고 있습니다. 지금 기준대로 하는게 제일 베스트인 것 같아요.

- datasource와 delegate는 저희가 정한 기준에 맞게 코드로 관리하는 것이 조금 더 가시적으로 보여서 이 방법을 쓰고 있었는데, 스토리보드에서 연결하는 방법도 있었는지 몰랐네요. 감사합니다!
- constraint 안잡히는 문제는 `Scrolling Enabled` 체크박스를 해제하거나, Size Inspector의 `Intrinsic Size`의 PlaceHolder 옵션을 주는 방법이 있는 것 같아요. 빨간색 에러가 떠 있는 상태 보다는 이렇게 constraint 잡아주는 것이 좋겠네요! 혹시 저희가 생각한 2가지 방법 외에 다른 좋은 방법도 있을까요?


3. 
> 요 링크 확인해보시고 "guard with trailing closure" 로 구글링 하면 관련 스택오버플로우 글도 있으니 참고 부탁드립니다
- Guard문에서는 저희가 사용한 문법을 Swift 언어 차원에서 지원하지 않고, 대신 If문에서는 else를 사용하지 않기 때문에 Swift 언어 차원에서 지원하는 것 같은데 저희가 이해한게 맞을까요?
- 해당 문법이 Guard문에서 설령 가능하다해도 다른 사람이 읽기 난해한 코드가 될 수 있어 해당 방식을 사용하지 않는 것이 좋을 것 같다고 결론 내렸습니다.

- 후행클로저 + If문
```swift
if let detailViewController =
    storyboard?.instantiateViewController(identifier: "itemDetail"){ coder in
        return ItemDetailViewController(coder: coder, data: self.items[indexPath.row])
    } {
    self.navigationController?.pushViewController(detailViewController, animated: true)
}
// Trailing closure in this context is confusable with the body of the statement; pass as a parenthesized argument to silence this warning
```

- 후행클로저 + Guard문
```swift
guard let detailViewController =
        storyboard?.instantiateViewController(identifier: "itemDetail") { coder in
    return ItemDetailViewController(coder: coder, data: self.items[indexPath.row])
} else {
    return
}
// 빌드되지 않음
```

## 궁금한 점
UITextView의 설정을 하는데에 중복되는 코드가 있어 이것을 타입의 extension에서 메소드를 구현해 보았습니다. 

``` swift
extension UITextView {
    func setTextView(with text: String) {
        let paragraphStyle = NSMutableParagraphStyle()
        paragraphStyle.lineBreakStrategy = .hangulWordPriority
        let attribute: [NSAttributedString.Key: Any] = [
            .font: UIFont.systemFont(ofSize: 17),
            .paragraphStyle: paragraphStyle]
        self.attributedText = NSAttributedString(
            string: text,
            attributes: attribute)
        self.allowsEditingTextAttributes = false
        self.isScrollEnabled = false
    }
}
```
다만, 위 방법은 프로젝트 내부에서 쓰이는 모든 UITextView에서 메소드에 접근할 수 있기 때문에 아래와 같이 상속을 활용한 Custom View를 만드는 방법도 생각을 해보았습니다. 어떤 방법이 일반적으로 선호되는지 혹은 다른 좋은 방법이 있는지 궁금합니다!
``` swift
class DescriptionTextView: UITextView {
    private let paragraphStyle: NSMutableParagraphStyle = {
        let style = NSMutableParagraphStyle()
        style.lineBreakStrategy = .hangulWordPriority
        return style
    }()
    
    private lazy var attribute: [NSAttributedString.Key: Any] = [
        .font: UIFont.systemFont(ofSize: 17),
        .paragraphStyle: paragraphStyle]
    
    override init(frame: CGRect, textContainer: NSTextContainer?) {
        super.init(frame: frame, textContainer: textContainer)
        commomInit()
    }
    
    required init?(coder: NSCoder) {
        super.init(coder: coder)
        commomInit()
    }
    
    func commomInit() {
        self.allowsEditingTextAttributes = false
        self.isScrollEnabled = false
    }
    
    func setAttribute(withText text: String) {
        self.attributedText = NSAttributedString(
            string: text,
            attributes: attribute)
    }
}
```


# Step3

## 학습 키워드
- `AutoLayout`
- `Accessibility`
- `Dynamic Type`
- `Generics`
- `Orientation`


## 고민한 점
1. Parser의 중복되는 코드 제거
`Parser` 타입의 `parsedExpoInfo` 메소드와 `parsedItemsInfo` 메소드의 중복되는 코드를 수정하기 용이하도록 하기 위해 가급적 한 곳에서 관리하고 싶었습니다. 해당 기능을 구현하기 위해 제너릭을 활용하여 코드 중복을 제거해주었습니다.
    
2. 상수 관리
`Parser` 타입 내부의 `Information` 타입이나 `UIFont` Extension, `ExpoStringLiteral` 타입 등을 활용하여 코드 상에서 관리할 필요가 있다고 느낀 상수를 한 곳에서 더 읽기 편하게 관리할 수 있도록 구현하였습니다. 그 외에 identifier와 같은 String 상수를 사용하는 코드도 하드코딩하지 않고 가독성을 높일 수 있도록 코드를 작성해주었습니다.
    
3. 화면 회전
`UINavigationController`를 상속한 새로운 클래스 `ExpoNavigationController`를 만들어준 뒤 사용하여 기존에 스토리보드에서 사용하던 Navigation Controller의 클래스를 대체하였습니다. 새로운 클래스 `ExpoNavigationController`는 내부에서 현재 보여지는 childViewController의 Orientation 설정을 따라가도록 구현되어 있습니다.

4. Storyboard
코드에서 작성한 View의 설정이 Storyboard 미리보기에서는 적용되지 않는 현상으로 인해 시뮬레이터 실행 시 발생하지 않는 오류를 XCode에서 띄워주는 문제가 있었습니다. 해당 문제를 해결하기 위해 `intrinsicSizeContent`의 속성에 Placeholder를 지정해주었습니다.


## 알게 된 내용
1. Dynamic Type
- `UIButton` 내부의 `UILabel`에 Dynamic Type 프로퍼티 값을 변경해주자 심각한 문제가 발생하였습니다. `UILabel`의 높이가 커져도 `UIButton`의 높이가 변경되지 않는 문제가 발생하였습니다.
- `UIButton` 내부에 `UILabel`이 존재하기 때문에 자연스럽게 `UILabel`에 따라서 `UIButton`의 높이 값이 변경될 것을 기대하였으나 해당 기능은 자체적으로 구현되어있지는 않았습니다. 코드에서 `UIButton`과 `UILabel`의 높이가 동일해지도록 `NSLayoutConstraint`를 걸어주어 해결하였습니다.
- 호환성을 고려하지 않는다면 `NSLayoutConstraint`를 걸어주는 대신에 iOS 15 이상에서 추가된 `UIButton.Configuration`를 활용하는 방법도 있습니다.

2. 커스텀뷰의 요소의 설정값 프로퍼티들이 초기화 되는 과정
- 트러블슈팅과정에서, 뷰 객체를 생성하면 어떤 과정을 거쳐 설정이 반영되는지를 알아보고자 `override var adjustsFontForContentSizeCategory: Bool` 에서 프로퍼티 옵저버를 통해 설정값이 바뀌는 과정을 관찰해 보았습니다.
- IB에서의 체크 여부와 관계없이, 정해진 특정한 기본값을 가진 상태로 객체는 초기화 됩니다. -> IB에서의 체크된 설정에 따라 설정값이 바뀜 -> super.init -> self.init

3. 제네릭 메서드의 문법
```swift
protocol someProtocol {
    static func converted(from text: String) -> Self
}
extension Double: someProtocol {
    static func converted(from text: String) -> Self {
        Double(text) ?? 0.0
    }
}
extension Int: someProtocol {
    static func converted(from text: String) -> Self {
        Int(Double(text) ?? 0) ?? 0
    }
}

func qwe<T: someProtocol>(text: String) -> T? {
    return T.converted(from: text)
}

print(qwe(text: "24.9") as Int?)
print(qwe(text: "24.9") as Double?)

let result: Int? = qwe(text: "24.9")
print(result)
```
- 제네릭 메서드는 제네릭 타입과는 달리 호출부에서 `<>` 내부에 타입을 명시하는 식으로 타입을 추론시키는 것이 아닙니다.
- 메서드의 매개변수나 반환값을 통해 타입을 추론시켜야 합니다. 이 중 반환값을 통해 추론시키는 경우에는 위 코드처럼 `타입 명시` 방법이나, `as`를 통해 타입을 알려줄 수 있습니다.
- [관련 Stackoverflow 링크](https://stackoverflow.com/questions/27965439/cannot-explicitly-specialize-a-generic-function/43422710)


## 궁금한 점
1. Orientation 관련 UINavigationController 상속
첫번째 화면에서 Orientation을 `.portrait`으로 고정하기 위해 아래와 같이 구현하였습니다. 더 좋은 방법이 없는지 궁금합니다!
```swift
class ExpoInfoViewController: UIViewController {
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        return [.portrait]
    }
    ...
}
```
```swift
class ExpoNavigationController: UINavigationController {    
    override var supportedInterfaceOrientations: UIInterfaceOrientationMask {
        guard let topViewController = topViewController else {
            return .all
        }
        return topViewController.supportedInterfaceOrientations
    }
}
```
2. AutoLayout with Dynamic Type (해결하지 못함)
    - 첫번째 화면에서 방문객, 개최지, 개최 기간등의 요소를 표현할때 타이틀 레이블과 컨텐츠  레이블을 분리하여 3개의 스택뷰로 구현하였습니다. 컨텐츠 레이블의 `numberOfLines`속성은 0으로, 타이틀 레이블은 1로 설정하고 타이틀 레이블의 크기가 너무 커지지 않도록 `ContentHugging` Priority를 1000으로 설정하였습니다.
    - iOS15.0 이상의 버전에서는 제대로 AutoLayout이 작동하나, iOS14.4에서는 아래와 같이 깨지는 현상이 발생하였습니다. 어떤 이유에서 발생하는지 파악하지 못했습니다. iOS 버전에 따라 View를 다른 모습으로 렌더링해주는 것으로 추측했습니다. 특정 버전에서만 발생하는 버그를 현업에서는 어떻게 처리하는지 궁금합니다. (무시하는지, 아니면 임시방편으로나마 해결하는지)

<div style="display: flex; flex-direction: row; justify-content: space-between; align-items: flex-end; width: 80%; margin: 0 auto;">
    <span style="display: inline-block; width:50%; position: relative;">
        <img src="https://i.imgur.com/zRkaAhM.png" style="display:inline-block">
        <span style="text-align: center; position: absolute; right: 16px; bottom: 16px; color: #fff; background-color: rgba(0,0,0,0.7); border-radius:4px; padding: 4px 8px;">
            iOS 15.0 이미지
        </span>
    </span>
    <span style="display: inline-block; width:30%; position: relative;">
        <img src="https://i.imgur.com/Pvaeetg.png" style="display:inline-block">
        <span style="text-align: center; position: absolute; right: 16px; bottom: 16px; color: #fff; background-color: rgba(0,0,0,0.7); border-radius:4px; padding: 4px 8px;">
            iOS 14.4 이미지
        </span>
    </span>
</div>

&nbsp;
3. Accessibility Trait (Voice Over)
2번째 화면의 TableViewCell의 Accessibility Trait 이 `Button`으로 자동으로 설정되고 변경할 수 없는것으로 파악했습니다.(`accessibilitTrait` 프로퍼티를 활용하여 변경해 주어도 기존 Trait에 추가만 되는 것으로 확인) Trait이 음성을 읽어줄때 포함되는 필수 요소이기 때문에 개발자가 커스텀하게 변경할 수 있는 방법이 있을거라고 생각하는데 찾지 못했습니다. 다른 방법이 있는지 궁금합니다.  
    


## 트러블슈팅


1. 시뮬레이터의 Dynamic Type 폰트 크기를 최대로 한 후 앱 실행 시 글자가 잘리며 ...으로 나오는 현상
- 추측 원인: 앱을 실행한 이후에 Dynamic Type 폰트 크기를 변경하면 문제가 발생하지 않았습니다. 추정컨데, `방문객`, `개최지`, `개최 기간` label의 `numberOfLines`를 1로 설정해 두었기 때문에 stackView의 높이가 늘어나지 않아 발생하는 일시적인 현상인 것 같았습니다.
    <div style="display: flex; flex-direction: row; justify-content: space-between; align-items: flex-end; width: 80%; margin: 0 auto;">
    <span style="display: inline-block; width:30%; position: relative;">
        <img src="https://i.imgur.com/o32dhJ9.png">
        <span style="text-align: center; position: absolute; right: 16px; bottom: 16px; color: #fff; background-color: rgba(0,0,0,0.7); border-radius:4px; padding: 4px 8px;">
            시뮬레이터 처음 켜졌을 때 이미지
        </span>
    </span>
    <span style="display: inline-block; width:60%; position: relative;">
        <img src="https://i.imgur.com/AaPaS7V.gif">
        <span style="text-align: center; position: absolute; right: 16px; bottom: 16px; color: #fff; background-color: rgba(0,0,0,0.7); border-radius:4px; padding: 4px 8px;">
            Dynamic Type 변경 시 gif 이미지
        </span>
    </span>
</div>

- 해결 방법: Storyboard에서 각 Label의 Content Compression Resistance Priority를 적절히 설정하여 해당 문제를 해결했습니다. 왼쪽 라벨들의 Content Compression Resistance를 오른쪽 라벨들보다 1 높게 설정하면 자동 줄임이 되지 않고 의도대로 표시되었습니다.