Daehoon Lee
    • Create new note
    • Create a note from template
      • Sharing URL Link copied
      • /edit
      • View mode
        • Edit mode
        • View mode
        • Book mode
        • Slide mode
        Edit mode View mode Book mode Slide mode
      • Customize slides
      • Note Permission
      • Read
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Write
        • Only me
        • Signed-in users
        • Everyone
        Only me Signed-in users Everyone
      • Engagement control Commenting, Suggest edit, Emoji Reply
    • Invite by email
      Invitee

      This note has no invitees

    • Publish Note

      Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

      Your note will be visible on your profile and discoverable by anyone.
      Your note is now live.
      This note is visible on your profile and discoverable online.
      Everyone on the web can find and read all notes of this public team.
      See published notes
      Unpublish note
      Please check the box to agree to the Community Guidelines.
      View profile
    • Commenting
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
      • Everyone
    • Suggest edit
      Permission
      Disabled Forbidden Owners Signed-in users Everyone
    • Enable
    • Permission
      • Forbidden
      • Owners
      • Signed-in users
    • Emoji Reply
    • Enable
    • Versions and GitHub Sync
    • Note settings
    • Note Insights New
    • Engagement control
    • Make a copy
    • Transfer ownership
    • Delete this note
    • Save as template
    • Insert from template
    • Import from
      • Dropbox
      • Google Drive
      • Gist
      • Clipboard
    • Export to
      • Dropbox
      • Google Drive
      • Gist
    • Download
      • Markdown
      • HTML
      • Raw HTML
Menu Note settings Note Insights Versions and GitHub Sync Sharing URL Create Help
Create Create new note Create a note from template
Menu
Options
Engagement control Make a copy Transfer ownership Delete this note
Import from
Dropbox Google Drive Gist Clipboard
Export to
Dropbox Google Drive Gist
Download
Markdown HTML Raw HTML
Back
Sharing URL Link copied
/edit
View mode
  • Edit mode
  • View mode
  • Book mode
  • Slide mode
Edit mode View mode Book mode Slide mode
Customize slides
Note Permission
Read
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Write
Only me
  • Only me
  • Signed-in users
  • Everyone
Only me Signed-in users Everyone
Engagement control Commenting, Suggest edit, Emoji Reply
  • Invite by email
    Invitee

    This note has no invitees

  • Publish Note

    Share your work with the world Congratulations! 🎉 Your note is out in the world Publish Note

    Your note will be visible on your profile and discoverable by anyone.
    Your note is now live.
    This note is visible on your profile and discoverable online.
    Everyone on the web can find and read all notes of this public team.
    See published notes
    Unpublish note
    Please check the box to agree to the Community Guidelines.
    View profile
    Engagement control
    Commenting
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    • Everyone
    Suggest edit
    Permission
    Disabled Forbidden Owners Signed-in users Everyone
    Enable
    Permission
    • Forbidden
    • Owners
    • Signed-in users
    Emoji Reply
    Enable
    Import from Dropbox Google Drive Gist Clipboard
       Owned this note    Owned this note      
    Published Linked with GitHub
    • Any changes
      Be notified of any changes
    • Mention me
      Be notified of mention me
    • Unsubscribe
    ###### tags: `project` # 🍓🍌🍉🧃 쥬스 메이커 ![](https://hackmd.io/_uploads/Hyci6aHN2.png) ## Ground Rules ### 규칙 - TIL, 일일 회고 작성 시간(매일 22시부터 1시간 작성 진행) - 페어프로그래밍 시간 제한 최대 30분 ### 스크럼 - 오전 11시 디스코드에서 진행 - 금일 진행 사항 공유하기(오늘의 할일) ### 프로젝트 규칙 - 네이밍 준수하기(가이드 라인) - 커밋 메시지 규칙 진행 - 코드에 대한 기록 그때그때 하기 ## 일일 스크럼 ### 🙌 05/08 - 오늘의 컨디션 - redmango: 헤롱헤롱 숙취파티 - hoon: 개운해요😁 - 특이사항 - redmango: 점심시간 2시간 필요! - hoon: 오늘도 오래오래 공부할 예정! - 오늘 할 일 - [x] STEP 1 순서도 작성 - [x] STEP 1 시작 ### 🙌 05/09 - 오늘의 컨디션 - redmango: 상쾌하니 좋아요^^ - hoon: 상쾌하니 좋아요😆 - 특이사항 - redmango: 공부 할 예정 - hoon: 개인 공부하다가 낮잠 잘 예정 ㅋㅋㅋㅋ - 오늘 할 일 - [x] STEP 1 오류 처리 - [ ] STEP 1 리팩토링 ### 🙌 05/10 - 오늘의 컨디션 - redmango: 깨운해요(머리말고 몸이...) - hoon: 잠이 부족해요...😵‍💫 - 특이사항 - redmango: 없어요! - hoon: 머리를 자르고 싶어요... - 오늘 할 일 - [x] STEP 1 리팩토링 - [x] STEP 1 PR 보내기 ### 🙌 05/11 - 오늘의 컨디션 - redmango: 눈이 말똥말똥 - hoon: 꽃가루가 심해요...😭😭😭 - 특이사항 - redmango: 꿀잠 조으다 - hoon: 머리머리머리머리머리~💇💈✂️ - 오늘 할 일 - [x] STEP 1 PR 코멘트 답변 달기 ### 🙌 05/12 - 오늘의 컨디션 - redmango: 개운! - hoon: 피곤해요...😩😩😩 - 특이사항 - redmango: 없음 - hoon: 객사오를 읽어볼 거에요😵‍💫 with MINT😈 - 오늘 할 일 - [x] README 작성 ### 🙌 05/13 오프데이 - 오늘의 컨디션 - redmango: - hoon: - 특이사항 - redmango: 쉴겁니다 ㅎㅎ - hoon: 영화 볼 거에요🎬 - 오늘 할 일 - [x] 푹 쉬고 오기!!🥰 ### 🙌 05/14 - 오늘의 컨디션 - redmango: - hoon: 오랜만에 푹 자서 좋습니다. - 특이사항 - 오후 2시 시작!! - redmango: - hoon: 공부 반 휴식 반 민트와 수다 왕창 - 오늘 할 일 - [x] 데이터 전달 방식 - [x] alert 방식 알아보기 ### 🙌 05/15 - 오늘의 컨디션 - redmango: 살려주세요 - hoon: 월요일은 너무 힘들어요😵‍💫 - 특이사항 - redmango: 없습니다! - hoon: 아무 일정도 없어요🤣 민트랑 공부해요🥰 - 오늘 할 일 - [x] STEP 2 알림창 띄우기 - [ ] 화면 전환하기 ### 🙌 05/16 - 오늘의 컨디션 - redmango: 깨운합니다 - hoon: 너무 졸려요.... - 특이사항 - redmango: 딱히 없어요 - hoon: 낮잠이 필요합니다. - 오늘 할 일 - [x] 화면 전환하기 - [x] 쥬스를 만든 후 과일 개수 변경 표시 - [ ] 리팩토링 ### 🙌 05/17 - 오늘의 컨디션 - redmango: 깨운 - hoon: 나오기위해 잠을 푹 잤습니다.🥰 - 특이사항 - redmango: 오프오프! 메롱🤪 - hoon: 9기 오프라인 모각코 with MINT - 오늘 할 일 - [x] 리팩토링 - [x] STEP 2 PR 보내기 ### 🙌 05/18 - 오늘의 컨디션 - redmango: 나쁘지 않습니다 - hoon: 좋습니다 - 특이사항 - redmango: 역시 음주 다음날은 장이 미쳐날뛰는군요! - hoon: 민트가 보고싶어요🥹 - 오늘 할 일 - [x] STEP 2 PR 반영하여 코드 수정 - [ ] STEP 2 PR 답변 ### 🙌 05/19 - 오늘의 컨디션 - redmango: 졸려요 - hoon: 상쾌합니다 민트랑 함께하는 하루😈 - 특이사항 - redmango: 졸려요 - hoon: 오후 4시에 아주 짧게 20분 정도 토요 스터디 스크럼이 있어요 - 오늘 할 일 - [x] STEP 2 PR 답변 - [x] README 작성 ### 🙌 05/20 오프데이 - 오늘의 컨디션 - redmango: - hoon: 민트랑 놀아서 상쾌합니다. - 특이사항 - redmango: 쉴겁니다 ㅎㅎ - hoon: 잠으로 채울꺼에요 - 오늘 할 일 - [x] 푹 쉬고 오기!!🥰 ### 🙌 05/21 - 오늘의 컨디션 - redmango: 피곤합니다 ㄸㄹㄹ - hoon: 어제 푹 쉬었더니 행복합니다. - 특이사항 - redmango: 밤에 약속! - hoon: 밤에 잠시 자리를 비울 것 같습니다. - 오늘 할 일 - [ ] Alert 피드백 반영 - [x] 오토 레이아웃 공부 ### 🙌 05/22 - 오늘의 컨디션 - redmango: 피곤해요 ㄸㄹㄹ - hoon: 좋습니다.😆 - 특이사항 - redmango: 학습활동 끝난 직후 스터디 모임 잠시 갔다올게요! - hoon: 오프라인 모각코를 위해 달릴 예정입니다.💪 - 오늘 할 일 - [x] Alert 피드백 적용 - [x] 오토 레이아웃 사용 - [ ] 화면 간에 데이터 전달 ### 🙌 05/23 오프라인 모각코 with som☁️ - 오늘의 컨디션 - redmango: 좋습니다 ㅎㅎㅎ - hoon: 즐겁습니다. - 특이사항 - redmango: 솜이 저녁 사준데요! - hoon: 모각코 너무 좋아요😆 - 오늘 할 일 - [x] 싱글톤 적용 - [x] 데이터 전달 ### 🙌 05/24 - 오늘의 컨디션 - redmango: 좋습니다 - hoon: 상쾌합니다 - 특이사항 - redmango: 없는듯합니다 - hoon: 일찍 끝나면 밀린 개인 공부를 해야겠어요 ㅋㅋㅋㅋ🤣 - 오늘 할 일 - [x] STEP 3 마무리 - [x] STEP 3 PR 보내기 ### 🙌 05/25 - 오늘의 컨디션 - redmango: 내일 예비군 가기 시르다 - hoon: 살짝 피곤합니다🙃 - 특이사항 - redmango: 내일 예비군 가기 시르다 - hoon: 내일 쉬는 날~ - 오늘 할 일 - [ ] STEP 3 PR 코멘트 답변 달기 - [x] UML Class Diagram 작성 ### 🙌 05/26 - 오늘의 컨디션 - redmango: 집에 가고 싶어요 - hoon: MINT - 특이사항 - redmango: 예비군....ㄸㄹㄹㄹㄹ - hoon: 민트랑 놀기 - 오늘 할 일 - [x] README 작성 - [x] STEP 3 PR 코멘트 답변 달기 # 🍓🍌🧃 쥬스 메이커 ## 📖 목차 1. [소개](#-소개) 2. [팀원](#-팀원) 3. [타임라인](#-타임라인) 4. [시각화된 프로젝트 구조](#-시각화된-프로젝트-구조) 5. [실행 화면](#-실행-화면) 6. [트러블 슈팅](#-트러블-슈팅) 7. [참고 링크](#-참고-링크) 8. [팀 회고](#-팀-회고) </br> ## 🍀 소개 훈맹구(`hoon`, `redmango`) 팀이 만든 쥬스 메이커 프로젝트입니다. 첫 화면에서는 현재 가지고 있는 과일과 수량이 나타나며 원하는 쥬스를 선택하여 주문할 수 있습니다. 다음 화면에서는 과일의 재고 관리를 해주는 동작을 수행합니다. 쥬스를 주문하면 알림 창을 통해 쥬스가 나왔다는 것을 사용자에게 알립니다. 재고가 부족하여 만들지 못하는 경우 알림 창에서 바로 재고 관리 화면으로도 넘어갈 수 있습니다. * 주요 개념: `Initialization`, `Access Control`, `Nested Types`, `Type Casting`, `Error Handling` </br> ## 👨‍💻 팀원 | redmango | hoon | | :--------: | :--------: | | <Img src = "https://hackmd.io/_uploads/HJ2D-DoNn.png" width="200" height="200"> |<Img src="https://hackmd.io/_uploads/HylLMDsN2.jpg" width="200" height="200"> | |[Github Profile](https://github.com/redmango1447) |[Github Profile](https://github.com/Hoon94) | </br> ## ⏰ 타임라인 |날짜|내용| |:--:|--| |2023.05.08.| - `fruit` 인스턴스 생성을 위한 `Fruit` 클래스 추가 <br> - 쥬스 레시피를 위한 `Recipe` 열거형 추가 | |2023.05.09.| - 쥬스를 만드는 기능과 쥬스의 재료 부족 시 오류 처리 추가 | |2023.05.10.| - `Fruit`, `Recipe` 사용자 타입 리팩토링 <br> - `Ingredient` 구조체 추가 | |2023.05.11.| - `FruitStore` `struct` 타입으로 변경 <br> - 재료 차감시 발생하는 오류 수정 | |2023.05.12.| - `decereaseStock` 메서드 수정| |2023.05.15.| - `alert` 구현 및 `View`와 `ViewController` 연결| |2023.05.16.| - 에러처리 위치 수정 및 사용자 선택에 따른 데이터 반영을 위해 `UILabel` 및 주문버튼 판단 로직 수정| |2023.05.17.| - `UILabel`을 하나의 `IBOutlet Collection`으로 수정 및 `fruitStore` 프로퍼티 은닉화| |2023.05.18.| - 주스 주문 버튼과 `Enum`타입 `Juice`의 매칭을 위해 `CustomButton`클래스 생성 및 활용| |2023.05.19.| - 네이밍 수정| |2023.05.22.| - `재고 추가 View`에 `Auto Layout` 적용 <br> - `alert`관련 메서드 하나로 통일| |2023.05.23.| - 재고 추가 기능을 위한 `UIStepper` 사용 <br> - `FruitStore struct`를 `FruitStore singleton class`로 변경 <br> - `delegate` 패턴을 활용하여 화면 간의 정보 전달| |2023.05.24.| - `stockManagementViewController`에서 `FruitStore`의 `stockList`를 변경하는 기능 추가 <br> - `touchUpStockStepper` 메서드 내부의 `switch`문 분리| |2023.05.25.| - `Configurable protocol` 파일 분리 <br> - `Class Diagram` 작성| |2023.05.26.| - `FruitStore singleton class` 제거| </br> ## 👀 시각화된 프로젝트 구조 ### Diagram <p align="center"> <img width="800" src="https://hackmd.io/_uploads/HkLt6AhBh.jpg"> </p> </br> ## 💻 실행 화면 | 과일 쥬스 주문하기 | |:--------:| |<img src="https://hackmd.io/_uploads/By1dkl0B3.gif">| | 화면 전환하기 | |:--------:| |<img src="https://hackmd.io/_uploads/H1aDve0r3.gif">| | 과일 재고 추가하기 | |:--------:| |<img src="https://hackmd.io/_uploads/HyecDlAH2.gif">| </br> ## 🧨 트러블 슈팅 1️⃣ **`Fruit` 사용자 타입 생성** <br> - 🔒 **문제점** <br> - 다양한 종류의 과일 예제들이 미션에서 주어졌습니다. 주어진 과일에 대해 이름과 수량을 저장하고 있어야 했기 때문에 `class` 타입을 활용하여 `Fruit`을 생성하였습니다. ```swift class Fruit { var name: String var quantity: Int init(name: String, quantity: Int) { self.name = name self.quantity = quantity } } ``` 🔑 **해결방법** <br> - 처음 생성한 `Fruit`은 수량을 의미하는 `quantity` 프로퍼티를 가지고 있었습니다. 하지만 과일이라는 네이밍의 클래스 안에 과일의 이름, 색, 당도 등의 프로퍼티는 가능하지만 과일 객체 자체가 자신의 수량을 가지고 있다는 점이 객체지향적 관점에서는 어색하다는 말씀을 듣고 이번 과제를 하며 다시 객체에 대해 생각해 보는 시간이 되었습니다. ```swift enum Fruit: CaseIterable { case strawberry case banana case pineapple case kiwi case mango } ``` `Fruit`에서 어색한 `quantity`를 빼고 위와 같이 `enum`으로 재정의 하였습니다. <br> 2️⃣ **과일 종류에 따른 초기화 방법** <br> - 🔒 **문제점** <br> - `FruitStore`에 생성한 `stockList` 프로퍼티에 각각의 과일 초기 수량을 저장하기 위해 `dictionary` 타입을 사용하였습니다. 과일 초기 값이 10이라는 요구사항을 지키기 위해 프로퍼티에 기본 값을 할당하려 했지만 만약 초기 값이 변경 된다면 `dictionary`의 특성과 `Fruit`의 갯수에 따라 수정하기가 힘들수도 있다고 판단 되었습니다. 그에따라 처음엔 메서드를 만들어 사용하려 했습니다. 🔑 **해결방법** <br> - ```swift init(stockQuantity: Int = 10) { for fruit in Fruit.allCases { stockList[fruit] = stockQuantity } } ``` 하지만 메서드를 만들고 호출하여 `stockList`를 채우기보다는 `init` 함수를 사용하여 `FruitStore`의 인스턴스(객체)를 생성할 때 각각 과일에 대한 값들을 추가하도록 하였습니다. `init` 함수에서 `for`문을 활용하여 `Fruit`에 들어있는 과일들을 꺼내오기 위해 `CaseIterable` 프로토콜을 채택하였습니다. <br> 3️⃣ **`enum Juice` 활용 방법** <br> - 🔒 **문제점** <br> - 처음엔 `Raw Values`를 사용하려 했으나 사용 시 정확히 무엇을 뜻하는지 모른다는 조언을 듣고 `Associated Values`를 사용하기로 했습니다. 하지만 그 결과 `JuiceMaker`내부에 스위치 문을 사용하게 되었고 반복되는 코드의 양이 너무 많아 가독성을 떨어뜨리게 되었습니다. ```swift func makeJuice(with recipe: Recipe) { let fruitStore: FruitStore = FruitStore() switch recipe { case .strawberryJuice(strawberry: let quantity): fruitStore.changeStock(of: fruitStore.strawberry, by: quantity) case .bananaJuice(banana: let quantity): fruitStore.changeStock(of: fruitStore.banana, by: quantity) case .kiwiJuice(kiwi: let quantity): fruitStore.changeStock(of: fruitStore.kiwi, by: quantity) case ... } } ``` 🔑 **해결방법** <br> - 열거형 안에서 연산 프로퍼티 사용이 가능하다는 것을 알게 되었습니다. 초기엔 스위치문에 튜플,배열 등의 자료형을 사용하여 결과를 리턴 받아 사용했으나 `Ingredient`라는 타입을 만들어 보라는 조언에 따라 아래와 같이 코드를 생성 및 수정하여 활용했습니다. ```swift struct Ingredient { let name: Fruit let quantity: Int } ``` ```swift enum Juice { case... var recipe: [Ingredient] { switch self { case .strawberryJuice: return [Ingredient(name: .strawberry, quantity: 16)] case... } } } ``` ```swift mutating func makeOrder(juice: Juice) { do { for ingredient in juice.recipe { try fruitStore.checkStock(witch: ingredient.name, by: ingredient.quantity) } try juice.recipe.forEach { fruitStore.decreaseStock(witch: $0.name, by: $0.quantity) } } catch FruitStoreError.outOfStock(let fruit) { print("\(fruit)의 재고가 부족합니다.") } catch { print("알 수 없는 오류 발생.") } } ``` 또한 `Ingredient`타입을 `FruitStore`내 메서드에 매개변수로 받아옴으로써 `JuiceMaker`는 물론이고 `FruitStore`내부 메서드들의 코드가 간소화되고 가독성이 높아졌습니다. <br> 4️⃣ **`Ingredient` 타입 선언 위치** <br> - 🔒 **문제점** <br> - `Ingredient` 타입을 처음에는 `Juice` 타입 안에 `Nested Type`으로 생성하였습니다. 생성을 하고 사용을 하다 보니 저희의 코드에서는 `FruitStore`에 있는 메서드인 `decreaseIngredient`에서 다음과 같이 사용하게 되었습니다. ```swift func decreaseIngredient(with recipe: [Juice.Ingredient] ) throws { ... } ``` 여기서 `FruitStore`가 어떤 쥬스를 만드는지에 대해 알 필요가 있을까라는 의문이 들었습니다. 예를 들면 `FruitStore`에서는 **딸기 쥬스를 만들기** 위한 딸기 10개를 감소한다가 아닌 단순하게 **무엇을 만들지는 모르겠지만** 딸기 10개를 감소한다가 더 올바른 표현이지 않을까라는 생각을 가졌습니다. 이러한 생각 때문에 `Ingredient` 타입이 `Juice` 타입 내부에 있는 것이 아닌 외부에 존재해야 한다고 생각하였습니다. 이를 수정하여 다음과 같이 메서드를 선언하였습니다. ```swift func decreaseIngredient(with recipe: [Ingredient] ) throws { ... } ``` 🔑 **해결방법** <br> - `Ingredient` 타입은 `name`과 `quantity`를 프로퍼티로 가지고 있습니다. `FruitStore` 타입 안에 재고를 감소시키는 메서드에서 `Ingredient`라는 네이밍의 매개변수가 필요한지에 대해서 다시 한번 생각해 보게 되었습니다. 재료가 무엇인지를 알 필요 또한 없다고 생각이 들었고 처음 받아오는 파라미터에서 `name`과 `quantity`로 나누어서 전달 인자를 받는 방법으로 수정을 하였습니다. ```swift mutating func decreaseStock(witch fruit: Fruit, by quantity: Int) { ... } ``` 위와 같이 코드를 수정하고 나서 보면 `Ingredient`는 더 이상 사용하지 않는 타입이 되었습니다. 이를 통해 `Juice` 타입 안에 다시 `Nested Type`으로 `Ingredient`을 선언하였습니다. <br> 5️⃣ **여러 `UIButton`들을 하나의 `@IBAction`으로 묶기** <br> - 🔒 **문제점** <br> - 쥬스를 주문하는 버튼이 각각의 쥬스 종류만큼 개별적으로 존재했습니다. 모든 주문 버튼에 관련하여 각각 `@IBAction`을 생성하는 경우 아래와 같이 중복되는 코드의 양이 무척 많았습니다. ```swift @IBAction func touchUpStrawberryJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpBananaJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpPineappleJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpKiwiJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpMangoJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpStrawberryBananaJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpMangoKiwiJuiceOrderButton(_ sender: UIButton) { ... } ``` 🔑 **해결방법** <br> - 이러한 부분을 수정하기 위해 하나의 `@IBAction`에서 버튼을 구분 지어 각각의 버튼에 맞는 동작을 수행하도록 수정하였습니다. ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { ... } ``` 위와 같이 하나의 `@IBAction`에서 각각의 버튼을 구분하기 위해서는 이를 구분하기 위한 식별자가 필요했습니다. `UIButton`의 `tag`와 `title` 프로퍼티를 사용하여 식별자 역할을 할 수 있었습니다. 두 방법에 대해 다음과 같이 코드로 표현하였습니다. - `tag` 사용 ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { try juiceMaker.makeOrder(sender.tag) } ``` `tag`를 사용하여 해결을 한 경우 각각의 버튼에서 가지고 있는 프로퍼티의 `tag`값을 모두 개별적으로 지정해 주어야 한다는 점에서 코드의 수정이 필요했습니다. 또한 `tag`는 기본값으로 0을 가지고 있기 때문에 새로운 버튼을 추가하고 개발자가 `tag`를 수정하지 않을 시 같은 `tag` 값을 갖는 문제가 발생하였습니다. - `title` 사용 ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { guard let title = sender.titleLabel?.text, let juice = Juice(rawValue: title) else { return } try juiceMaker.makeOrder(juice) } ``` `title` 사용 시 버튼에 대한 기본 네이밍의 양식이 같다면 추가 수정 없이 Juice의 원시 값을 수정하여 매칭을 시킬 수 있었습니다. 이렇게 된다면 버튼이 추가될 때 `Juice`의 원시 값만 추가하여 해당 버튼의 `title`과 같은 값으로 맞춰주면 되었습니다. 위의 두 경우를 비교하여 생각하였을 때 `title`을 사용하는 방식이 `tag`를 활용하는 방법보다 새로운 버튼이 추가되었을 때 더 적은 수정을 필요로 하고 혹시 모를 에러(ex: 버튼 추가 후 `tag`미 수정 등)를 미연에 방지하기 때문에 최종적으로 `title`을 활용하기로 결정하여 문제를 해결하였습니다. <br> 6️⃣ **`Juice`와 쥬스 주문 버튼의 매칭** <br> - 🔒 **문제점** <br> - 처음엔`Juice`의 `rawValue`와 `UIButton`의 `titleLabel`을 가져와 매칭 시켰으나 아래와 같은 리뷰를 받고 고민해 본 결과 열거형의 원시값을 사용하는 것이 아닌 다른 방법으로 문제를 해결해야겠다고 생각하였습니다. > Juice의 rawVaule가 titleLabel가 무슨 연관성이 있는지 잘 모르겠습니다. by som ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { guard let title = sender.titleLabel?.text, let juice = Juice(rawValue: title) else { return } ... } ``` 🔑 **해결방법** <br> - `switch`문 활용 우선 `switch`문을 활용할 수 있게 적절한 값이 필요하다는 판단에 `Enum`타입의 `Juice`에 아래와 같이 연산 프로퍼티를 선언하였습니다. ```swift var name: String { switch self { case .strawberryJuice: return "strawberryJuice" case .bananaJuice: return "bananaJuice" case... } } ``` 이후 `UIButton`의 식별자로 활용할 만한 요소가 무엇이 있을까 고민하다가 `accessibilityIdentifier` 값을 사용하기로 결정하고 아래와 같이 `IBOutlet Collection`이용해 이전에 생성한 `Juice.name`값을 순차적으로 넣어줬습니다. ```swift private func fillJuiceButtonIdentifier() { for index in orderJuiceButtonCollection.indices { orderJuiceButtonCollection[index].accessibilityIdentifier = Juice.allCases[index].name } } ``` 다음으로는 저희가 의도한 대로 받아온 `UIButton`의 `accessibilityIdentifier`값을 `switch`문에 사용하여 `Juice`와 매칭하였고, 나온 반환값을 `juiceMaker.makeOrder()` 메서드의 매개변수로 넣어주었습니다. ```swift private func matchJuiceMenu(with buttonIdentifier: String) throws -> Juice { switch buttonIdentifier { case "strawberryJuice": return .strawberryJuice case "bananaJuice": return .bananaJuice case... default: throw JuiceError.outOfMenu } } @IBAction func touchUpOrderButton(_ sender: UIButton) { guard let buttonIdentifier = sender.accessibilityIdentifier else { return } let juice: Juice = matchJuiceMenu(with: buttonIdentifier) juiceMaker.makeOrder(juice) ... } ``` - `customIdentifier`를 만들어 사용 "`Button`에 `Juice`타입을 만들어 줄 수는 없을까?"라는 고민을 시작으로 별도의 타입을 만들어 상속시켜 사용해 보자는 결론을 내렸습니다. 그에 따라 아래와 같이 `UIButton`을 상속받은 `CustomButton`을 만들어 프로퍼티로 `Juice` 타입의 `customIdentifier`를 선언하였습니다. ```swift final class CustomButton: UIButton { var customIdentifier: Juice? } ``` 이후 모든 주스 주문 버튼에 `Juice`를 넣어줘야 하니 각각 넣어주는 건 추후 확장성 등을 고려해 비효율적이라는 판단에 `IBOutlet collection`와 `CaseIterable`을 활용하기로 결정했습니다. 물론 그전에 StoryBoard의 주문 버튼들의 `CustomClass` 설정을 일일이`CustomButton`으로 변경해 주는 과정이 필요했습니다. 이 과정에서 휴먼 에러가 발생하진 않을까 고민해 봤지만 이 과정을 실행하지 않을 경우 `CustomButton`타입인 `IBOutlet collection`와 이벤트 시 실행되는 메서드에 등록되지 않으니 체크가 반강제되고, 혹시 아예 과정 자체를 깜빡하는 경우를 생각해 봐도 발생할 수 있는 경우의 수는 `아무것도 실행되지 않음` or `Juice.allCases[index]`의 `indexOverError`이 2가지로 추측된다고 결론이 났습니다. 후자의 `indexOverError`의 경우 `for`문의 반복횟수를 `IBOutlet collection`를 기준으로 할 경우 방지할 수 있다고 판단하여 아래와 같이 코드를 작성 하였습니다.(`orderJuiceButtonCollection`과 `Juice`의 순서는 미리 맞춰뒀습니다.) ```swift private func fillJuiceButtonCustomIdentifier() { for index in orderJuiceButtonCollection.indices { orderJuiceButtonCollection[index].customIdentifier = Juice.allCases[index] } } ``` 마지막으로 버튼 이벤트 발생시 해당 버튼의 `customIdentifier`를 받아와 바로 메서드의 매개변수로 넣어주었습니다. ```swift @IBAction func touchUpOrderButton(_ sender: CustomButton) { guard let juice = sender.customIdentifier else { return } juiceMaker.makeOrder(juice) } ``` <br> 7️⃣ **`AlertAction handler` 화면 전환 방법** <br> - 🔒 **문제점** <br> - `재고 수정`을 누를 경우 `Segue`를 사용해`Present Modally` 방식으로 재고 수정 `viewController`와 연결해 줬습니다. `Alert`창에서 "예"를 누를 경우에도 `AlertAction handler`를 활용해 같은 재고 수정 `viewController`로 화면전환을 해줘야 하기에 처음엔 아래와 같이 `viewController`의 `identifierID`를 활용해 인스턴스화하여 `present` 해주는 방식으로 화면 전환을 하였습니다. ```swift guard let nextView = self.storyboard?.instantiateViewController(identifier: "identifierID") else { return } self.present(nextView, animated: true) ``` 🔑 **해결방법** <br> - 앞서 스토리보드에서 사용한 기존의 `Segue`를 어떻게 활용할 방법이 없을까 하고 고민이 들었고 결국 `performSegue`을 활용해 아래와 같이 해결했습니다. ```swift let confirmAction = UIAlertAction(title: "예", style: .default) { _ in self.performSegue(withIdentifier: "goToStockViewController", sender: nil) } ``` <br> 8️⃣ **`View Controller`는 `Fruit`의 종류를 알아야 하는가?** <br> - 🔒 **문제점** <br> - 뷰 컨트롤러에서 뷰에 있는 각각의 과일에 대한 수량을 나타내주는 UILabel을 초기화하기 위해 아래와 같은 메서드를 사용하였습니다. ```swift private func changeStockLabel() { for (fruitStockLabel, fruit) in zip(fruitStockLabels, Fruit.allCases) { fruitStockLabel.text = juiceMaker.showRemainStock(of: fruit) } } ``` `changeStockLabel` 메서드를 보면 `Fruit`의 종류를 `showRemainStock`의 인자로 전달하고 있습니다. 프로젝트를 진행하며 가장 많이 고민했던 점 중에 하나가 바로 객체의 관점에 대한 이해였고 뷰 컨트롤러 또한 쥬스의 종류까지만 알고 내부에서 사용하는 쥬스의 재료에 대한 `Fruit` 타입에 대해 알고 있지 않아도 되겠다는 생각을 가졌습니다. 🔑 **해결방법** <br> - 뷰 컨트롤러가 `Fruit`의 종류는 모르되 뷰의 `UILabel`에 입력할 수량에 대한 정보는 있어야함으로 `Array<Int>` 타입으로 수량에 대한 정보를 반환하였습니다. 배열 안에 있는 원소의 순서는 `Fruit`에 선언되어 있는 과일 종류의 순서에 맞추었습니다. 또한 기존에 사용하던 `changeStockLabel`명보다는 뷰 컨트롤러의 관점에서 `UILabel`을 채운다는 생각으로 `fillStockLabel`로 변경하였습니다. ```swift // class JuiceOrderViewController private func fillStockLabel() { let currentStockList: [String] = juiceMaker.showRemainStock() for index in fruitStockLabelCollection.indices { fruitStockLabelCollection[index].text = currentStockList[index] } } ``` ```swift // struct JuiceMaker func showRemainStock() -> [String] { return fruitStore.getRemainStock() } ``` ```swift // struct FruitStore func getRemainStock() -> [String] { var fruitStockList: [String] = [] for fruit in Fruit.allCases { let currentStock: Int = stockList[fruit] ?? 0 fruitStockList.append(String(currentStock)) } return fruitStockList } ``` 위와 같이 코드를 수정함으로써 뷰 컨트롤러는 `Fruit`을 알 필요가 없어졌고 또한 `showRemainStock`을 통해서 `getRemainStock` 메서드에 접근하기 때문에 `FruitStore`까지도 은닉화를 시킬 수 있었습니다. <br> 9️⃣ **`delegate` 패턴을 사용하여 이전 화면의 `UILabel` 업데이트** <br> - 🔒 **문제점** <br> - `StockManagementViewController`에서 재고에 추가된 내용을 `FruitStore`의 `stockList`에 저장하였습니다. `JuiceOrderViewController`의 화면에서 `StockManagementViewController`로 전환할 때 `present`를 사용하여 전환하였기 때문에 다시 이전 화면인 `JuiceOrderViewController`로 변경 시 `View State Method`의 `viewWillAppear` 메서드가 다시 호출되지 않는다는 문제점이 있었습니다. `modalPresentationStyle`을 `.fullScreen`로 설정해 주어야 `viewWillAppear` 메서드가 다시 호출되어 그 안에서 `UILable` 값을 변경해 줄 수 있었습니다. 🔑 **해결방법** <br> - 문제를 해결하고자 이전 화면으로 돌아가기 전 `StockManagementViewController` 화면의 `viewWillDisappear` 메서드에서 `delegate` 패턴을 활용하여 이전 화면의 `UILabel` 값을 다시 입력하도록 수정하였습니다. ```swift // JuiceOrderViewController protocol Configurable { func assignLabelText() } extension JuiceOrderViewController: Configurable { func assignLabelText() { configureStockLabel() } } final class JuiceOrderViewController: UIViewController { ... override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let stockManagementViewController = segue.destination as? StockManagementViewController else { return } stockManagementViewController.configurationDelegate = self } ... } ``` ```swift // StockManagementViewController final class StockManagementViewController: UIViewController { ... var configurationDelegate: Configurable? override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) changeStockList() configurationDelegate?.assignLabelText() } ... } ``` <br> 🔟 **`StockManagementViewController`에서 `FruitStore class instance` 사용** <br> - 🔒 **문제점** <br> - 재고 추가 화면인 `StockManagementViewController`에서도 `FruitStore`의 프로퍼티인 `stockList`에 대해서 접근하여 재고 추가에 대한 처리를 진행해야 했습니다. 현재 사용 중인 `JuiceOrderViewController`와 `StockManagementViewController` 모두 같은 값을 가지고 있는 `stockList`를 참조함으로써 화면 간에 데이터 전달이 아닌 공유를 하면 좋겠다는 판단에 싱글톤 클래스로 선언하였습니다. ```swift class FruitStore { static let shared: FruitStore = FruitStore() private var stockList: [Fruit: Int] = [:] private init(stockQuantity: Int = 10) { Fruit.allCases.forEach { stockList[$0] = stockQuantity } } ... } ``` 하지만 싱글톤 패턴을 사용하면 `thread unsafe` 문제와 싱글톤 객체의 변경이 일어나면 해당 객체를 활용하는 뷰의 수정이 연속적으로 일어나야 하는 문제가 있었습니다. 즉, `SOLID 원칙`의 개방-폐쇄 원칙을 지키지 않는다는 문제가 있었습니다. 🔑 **해결방법** <br> - 싱글톤 패턴을 사용하지 않고 첫 화면인 `JuiceOrderViewController`에서 사용하는 `FruitStore` 인스턴스를 다음 화면인 `StockManagementViewController`에 매개변수로 넘겨주었습니다. `class` 타입으로 선언한 `FruitStore`이기 때문에 같은 인스턴스를 참조할 거라고 생각했습니다. 결과적으로`StockManagementViewController`에서 변경한 값은 `JuiceOrderViewController`에도 같이 적용되어 변경된 값을 확인할 수 있었습니다. <br> ## 📚 참고 링크 - [🍎Apple Docs: Displaying and managing views with a view controller](https://developer.apple.com/documentation/uikit/view_controllers/displaying_and_managing_views_with_a_view_controller) - [🍎Apple Docs: UIViewController](https://developer.apple.com/documentation/uikit/uiviewcontroller) - [🍎Apple Docs: Modality](https://developer.apple.com/design/human-interface-guidelines/modality) - [🍎Apple Docs: Using Delegates to Customize Object Behavior](https://developer.apple.com/documentation/swift/using-delegates-to-customize-object-behavior) - [🍏Apple Archive: Managing the Lifetimes of Objects from Nib Files](https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/LoadingResources/CocoaNibs/CocoaNibs.html#//apple_ref/doc/uid/10000051i-CH4-SW6) - [🍏Apple Archive: Outlets](https://developer.apple.com/library/archive/documentation/General/Conceptual/CocoaEncyclopedia/Outlets/Outlets.html) - [🍏Apple Archive: Singleton](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Singleton.html) - [🍏Apple Archive: Delegation](https://developer.apple.com/library/archive/documentation/General/Conceptual/DevPedia-CocoaCore/Delegation.html) - [🍏Apple Archive: Auto Layout](https://developer.apple.com/library/archive/documentation/UserExperience/Conceptual/AutolayoutPG/index.html) - [📼Apple WWDC: Understanding Swift Performance](https://developer.apple.com/videos/play/wwdc2016/416/) - [📙stackOverflow: IBOutlet Collection](https://stackoverflow.com/questions/43398329/swift-iboutlet-collections-and-retain-cycle-safety) - [📘blog: Alert](https://peppo.tistory.com/29) - [📘blog: IBOutlet Collections](https://skytitan.tistory.com/579) - [📘blog: Tag](https://g1embed.tistory.com/8) - [📘blog: Accessibility Identifier](https://mildwhale.github.io/2019-12-26-uitesting-tip-and-tricks/) - [📘blog: IBOutlet Reference Counting](https://co-dong.tistory.com/60) - [📘blog: Auto Layout](https://velog.io/@eddy_song/ios-auto-layout-1) - [📘blog: View Controller 간 데이터 전달](https://velog.io/@nnnyeong/iOS-VC-%EA%B0%84-%EB%8D%B0%EC%9D%B4%ED%84%B0-%EC%A0%84%EB%8B%AC-%EB%B0%A9%EB%B2%95) - [📘blog: UML Class Diagram](https://www.nextree.co.kr/p6753/) </br> ## 👥 팀 회고 - [팀 회고 링크](https://github.com/redmango1447/ios-juice-maker/blob/main/%ED%8C%80%ED%9A%8C%EA%B3%A0.md) # 팀 회고 ## 우리팀이 잘한 점 - 객체 간의 관계에 대해 많은 토론을 했습니다. - 다양한 방법을 시도해보고 비교하여 커밋하였습니다. - 새롭게 시도하는 것에 대해 거부감이 없었습니다. - 민트를 새로운 팀원으로 받아들였습니다. ## 서로에게 좋았던 점 피드백 - redmango to hoon - hoon은 제가 잘 생각해 보지 못한 객체 간의 관계 및 확장성이나 유지 보수 등에 관한 생각이 깊어 저도 그에 관해 많이 생각해 보게끔 만드는 좋은 계기가 되어주었습니다. - 배려심이 아쥬아쥬아쥬 많습니다! 이거시 으른...?! - hoon to redmango - 아이디어가 많아서 좋았습니다. 특히 어떠한 방법을 시도해 보는 것에 대해 두려움이 없고 현재 프로젝트에 적합하지 않다고 생각하는 경우 공부를 해봤다는 점에서 만족하고 롤백 하는 모습이 좋았습니다. ## 서로에게 하고싶은 말 - redmango to hoon - 매번 늦어도 기다려주시고 예비군 때문에 리드미도 대부분 훈이 써주셔서 너무 감사하고 죄송해요 ㅠㅠ 언젠가 민트의 마수에서 구해드리겠습니다. - hoon to redmango - 일산까지 먼 길도 오시고 함께 2번이나 오프라인 모각코를 해서 너무 좋았습니다. 제가 하나에 너무 오랜 시간 고민을 하고 있어도 차분히 기다려 주셔서 너무 감사했습니다.🙇 # STEP 1 ## 고민되었던 점 ### `Fruit` 사용자 타입 생성 - 다양한 종류의 과일 예제들이 미션에서 주어졌습니다. 주어진 과일에 대해 이름과 수량을 저장하고 있어야 했기 때문에 `class` 타입을 활용하여 `Fruit`을 생성하였습니다. ```swift class Fruit { var name: String var quantity: Int init(name: String, quantity: Int) { self.name = name self.quantity = quantity } } ``` 처음 생성한 `Fruit`은 수량을 의미하는 `quantity` 프로퍼티를 가지고 있었습니다. 하지만 과일이라는 네이밍의 클래스 안에 과일의 이름, 색, 당도 등의 프로퍼티는 가능하지만 과일 객체 자체가 자신의 수량을 가지고 있다는 점이 객체지향적 관점에서는 어색하다는 말씀을 듣고 이번 과제를 하며 다시 객체에 대해 생각해 보는 시간이 되었습니다. ```swift enum Fruit: CaseIterable { case strawberry case banana case pineapple case kiwi case mango } ``` `Fruit`에서 어색한 `quantity`를 빼고 위와 같이 `enum`으로 재정의 하였습니다. ### 과일 종류에 따른 초기화 방법 - `FruitStore`에 생성한 `stockList` 프로퍼티에 각각의 과일 초기 수량을 저장하기 위해 `dictionary` 타입을 사용하였습니다. 이 부분에서 다른 함수를 사용하여 `stockList`를 채우기보다는 `init` 함수를 사용하여 `FruitStore`의 인스턴스(객체)를 생성할 때 각각 과일에 대한 값들을 추가하도록 하였습니다. `init` 함수에서 `for`문을 활용하여 `Fruit`에 들어있는 과일들을 꺼내오기 위해 `CaseIterable` 프로토콜을 채택하였습니다. ```swift init(stockQuantity: Int = 10) { for fruit in Fruit.allCases { stockList[fruit] = stockQuantity } } ``` ### `enum Juice` 활용 방법 - 처음엔 `Raw Values`를 사용하려 했으나 사용 시 정확히 무엇을 뜻하는지 모른다는 조언을 듣고 `Associated Values`를 사용하기로 했습니다. 하지만 그 결과 `JuiceMaker`내부에 스위치 문을 사용하게 되었고 반복되는 코드의 양이 너무 많아 가독성을 떨어뜨리게 되었습니다. 저희는 이 부분을 해결하기 위해 공식 문서를 참고했고, 열거형 안에서 연산 프로퍼티 사용이 가능하다는 것을 알게 되었습니다. 초기엔 스위치문에 튜플,배열 등의 자료형을 사용하여 결과를 리턴 받아 사용했으나 `재료`라는 타입을 만들어 보라는 조언에 따라 아래와 같이 코드를 생성 및 수정하여 활용했습니다. ```swift struct Ingredient { let name: Fruit let quantity: Int } ``` ```swift enum Juice { case... var recipe: [Ingredient] { switch self { case .strawberryJuice: return [Ingredient(name: .strawberry, quantity: 16)] case... } } } ``` 또한 `Ingredient`타입을 `FruitStore`내 메서드에 매개변수로 받아옴으로써 `JuiceMaker`는 물론이고 `FruitStore`내부 메서드들의 코드가 간소화되고 가독성이 높아졌습니다. ## 조언을 얻고 싶은 부분 ### 과일 종류에 따른 초기화 방법 - `FruitStore`의 `init` 메서드를 사용할 때 현재는 내부에서 `for`문을 활용합니다. 여기서 `for`문을 사용하는 메서드를 따로 만들고 그 메서드를 `init` 메서드에서 호출하는 것이 더 좋을까요? 현재 코드에서는 `init` 메서드에서 하는 동작이 그것 하나뿐이라 만약 메서드를 호출하는 동작만을 수행한다면 `init` 메서드에서 다시 다른 메서드를 호출하는 이중적인 역할만을 수행한다고 생각하여 따로 메서드를 생성하진 않았습니다. ### 사용자 타입 파일 분리 - 하나의 `classA`에서만 사용하는 사용하는 `classB`를 생성한다면 이 경우에도 파일을 나누어 관리를 할까요? 이런 경우에 파일 분리를 줄이고자 `Nested Types`이라는 방법이 있다고 생각이 되어 어떤 경우를 기준으로 파일을 나눌 수 있을지 궁금합니다. ### `Ingredient` 타입 생성 위치 - `Ingredient` 타입을 처음에는 `Juice` 타입 안에 `Nested Type`으로 생성하였습니다. 생성을 하고 사용을 하다 보니 저희의 코드에서는 `FruitStore`에 있는 메서드인 `decreaseIngredient`에서 다음과 같이 사용하게 되었습니다. ```swift func decreaseIngredient(with recipe: [Juice.Ingredient] ) throws ``` 여기서 든 의문점이 `FruitStore`에서 어떤 쥬스를 만드는지에 대해 알 필요가 있을까라는 의문이었습니다. 예를 들면 `FruitStore`에서는 **딸기 쥬스를 만들기** 위한 딸기 10개를 감소한다가 아닌 단순하게 **무엇을 만들지는 모르겠지만** 딸기 10개를 감소한다가 더 올바른 표현이지 않을까라는 생각을 가졌습니다. 이러한 생각 때문에 `Ingredient` 타입이 `Juice` 타입 내부에 있는 것이 아닌 외부에 존재해야 한다고 생각하였습니다. 이를 수정하여 다음과 같이 메서드를 선언하였습니다. ```swift func decreaseIngredient(with recipe: [Ingredient] ) throws ``` 이러한 생각들을 하다 보니 아직 객체지향적 관점에서에 대한 해석이 스스로 불분명하다고 생각되어 조언을 얻고 싶습니다. ### 필요한 재료의 수량에 대한 입력 방법 - 아래의 코드를 보면 현재 각각의 쥬스를 만들 때 들어가는 과일의 수량이 리터럴 값으로 명시되어 있습니다. 이렇게 하드코딩으로 직접적인 값을 입력해 주기보다는 변수를 사용하여 입력하는 방식으로 진행해 보면 어떨까라는 생각을 하였습니다. ```swift var recipe: [Ingredient] { switch self { case .strawberryJuice: return [Ingredient(name: .strawberry, quantity: 16)] case .bananaJuice: return [Ingredient(name: .banana, quantity: 2)] case .kiwiJuice: return [Ingredient(name: .kiwi, quantity: 3)] case .pineappleJuice: return [Ingredient(name: .pineapple, quantity: 2)] case .strawberryBananaJuice: return [Ingredient(name: .strawberry, quantity: 10), Ingredient(name: .banana, quantity: 1)] case .mangoJuice: return [Ingredient(name: .mango, quantity: 3)] case .mangoKiwiJuice: return [Ingredient(name: .mango, quantity: 2), Ingredient(name: .kiwi, quantity: 1)] } } ``` 여기서 생긴 질문이 코드상에서는 그렇다면 각각의 과일쥬스의 종류가 늘어날 때마다 이를 위한 변수들이 매번 필요하겠다는 문제점이었습니다. 코드뿐만 아니라 만약 사용자의 입력을 통해 필요한 과일의 수를 정하는 동작을 실행한다면 이때도 각각에 대한 변수가 필요할까요? 이런 부분에 대해서는 이렇게 명시적으로 나타내는 것 말고는 다른 방법이 어떤 것이 있을지 궁금합니다. # 고민한 점 - change quantity +/- 연산자 설정 - makeJuice 내부 switch code 네이밍 ### 참고 링크 - https://stackoverflow.com/questions/72668943/default-value-for-associated-type-enums-in-swift ### 생각할 점 - 접근 제어 - switch문 간결하게 - changeStock 함수 for문 처리 - 객체 관점에서 생각해보기 ## PR 1 코멘트 답변할 내용 - class 사용 이유 지금보니 현재로서는 굳이 class를 사용할 이유가 없어 보여 struct로 수정했습니다. 물론 final키워드는 한번 살펴보겠습니다! - decreaseIngredient 함수 매개변수 타입 - struct 사용 이유 - `JuiceMaker`를 선언하여 사용할 때 현재 내부의 프로퍼티 값을 변경하는 부분이 없었습니다. 또한 생성한 인스턴스를 아직 다른 곳에서 사용하는 부분도 없어 STEP 1에서는 `struct`를 사용하였습니다. - 지역변수 `decreaseStock`의 매개변수를 변경하여 배열의 뎁스가 깊어지는 것을 수정하였습니다. ```swift mutating func decreaseStock(witch fruit: Fruit, by quantity: Int) throws { ... } ``` `JuiceMaker`에서 `decreaseStock`을 호출하는 부분에서 `for`문을 사용하여 여러 재료들 중에서 하나씩 재료를 받아와 `decreaseStock` 안에서 있던 `for`문을 생략하였습니다. # PR: STEP 2 ## 고민되었던 점 ### 여러 `UIButton`들을 하나의 `@IBAction`으로 묶기 - 쥬스를 주문하는 버튼이 각각의 쥬스 종류만큼 개별적으로 존재했습니다. 모든 주문 버튼에 관련하여 각각 `@IBAction`을 생성하는 경우 아래와 같이 중복되는 코드의 양이 무척 많았습니다. ```swift @IBAction func touchUpStrawberryJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpBananaJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpPineappleJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpKiwiJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpMangoJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpStrawberryBananaJuiceOrderButton(_ sender: UIButton) { ... } @IBAction func touchUpMangoKiwiJuiceOrderButton(_ sender: UIButton) { ... } ``` 이러한 부분을 수정하기 위해 하나의 `@IBAction`에서 버튼을 구분 지어 각각의 버튼에 맞는 동작을 수행하도록 수정하였습니다. ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { ... } ``` 위와 같이 하나의 `@IBAction`에서 각각의 버튼을 구분하기 위해서는 이를 구분하기 위한 식별자가 필요했습니다. `UIButton`의 `tag`와 `title` 프로퍼티를 사용하여 식별자 역할을 할 수 있었습니다. 두 방법에 대해 다음과 같이 코드로 표현하였습니다. - `tag` 사용 ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { try juiceMaker.makeOrder(sender.tag) } ``` `tag`를 사용하여 해결을 한 경우 각각의 버튼에서 가지고 있는 프로퍼티의 `tag`값을 모두 개별적으로 지정해 주어야 한다는 점에서 코드의 수정이 필요했습니다. 또한 `tag`는 기본값으로 0을 가지고 있기 때문에 새로운 버튼을 추가하고 개발자가 `tag`를 수정하지 않을 시 같은 `tag` 값을 갖는 문제가 발생하였습니다. - `title` 사용 ```swift @IBAction func touchUpOrderButton(_ sender: UIButton) { guard let title = sender.titleLabel?.text, let juice = Juice(rawValue: title) else { return } try juiceMaker.makeOrder(juice) } ``` `title` 사용 시 버튼에 대한 기본 네이밍의 양식이 같다면 추가 수정 없이 Juice의 원시 값을 수정하여 매칭을 시킬 수 있었습니다. 이렇게 된다면 버튼이 추가될 때 `Juice`의 원시 값만 추가하여 해당 버튼의 `title`과 같은 값으로 맞춰주면 되었습니다. 위의 두 경우를 비교하여 생각하였을 때 `title`을 사용하는 방식이 `tag`를 활용하는 방법보다 새로운 버튼이 추가되었을 때 더 적은 수정을 필요로 하고 혹시 모를 에러(ex: 버튼 추가 후 `tag`미 수정 등)를 미연에 방지하기 때문에 최종적으로 `title`을 활용하기로 결정하여 문제를 해결하였습니다. ### `UIOutlet Collection` 활용 - `UILable`에 재고 값을 하나하나 넣는 것보단 반복문 등을 통해 한 번에 넣는 게 더 좋다고 판단했습니다. 아래와 같이 `UIOutlet Collection`을 활용해 `UILable`을 하나로 묶어 배열처럼 사용하였습니다. ```swift private func changeStockLabel() { for (fruitStockLabel, fruit) in zip(fruitStockLabels, Fruit.allCases) { fruitStockLabel.text = juiceMaker.showRemainStock(of: fruit) } } ``` ### `AlertAction handler` 화면 전환 방법 - `재고 수정`을 누를 경우 `Segue`를 사용해`Present Modally` 방식으로 재고 수정 `viewController`와 연결해 줬습니다. `Alert`창에서 "예"를 누를 경우에도 `AlertAction handler`를 활용해 같은 재고 수정 `viewController`로 화면전환을 해줘야 하기에 처음엔 아래와 같이 `viewController`의 `identifierID`를 활용해 인스턴스화하여 `present` 해주는 방식으로 화면 전환을 하였습니다. ```swift guard let nextView = self.storyboard?.instantiateViewController(identifier: "identifierID") else { return } self.present(nextView, animated: true) ``` 하지만 기존의 `Segue`를 어떻게 활용할 방법이 없을까 하고 고민이 들었고 결국 아래와 같이 `performSegue`을 활용해 아래와 같이 해결했습니다. ```swift let confirmAction = UIAlertAction(title: "예", style: .default) { _ in self.performSegue(withIdentifier: "goToStockViewController", sender: nil) } ``` ## 조언을 얻고 싶은 부분 ### 특정 `label`에 값을 받아오는 방법과 `stockList`의 딕셔너리 사용시 관찰의 어려움 - 현재 작성한 코드에서는 쥬스를 만들고 쥬스를 만들기 위해 사용한 과일의 수만큼을 해당 재고에서 감소시킨 후 그 전체 값을 다시 받아와서 `UILabel`에 새로 입력을 해줍니다. ```swift private func changeStockLabel() { for (fruitStockLabel, fruit) in zip(fruitStockLabelCollection, Fruit.allCases) { fruitStockLabel.text = juiceMaker.showRemainStock(of: fruit) } } ``` 위처럼 실제로 사용한 과일뿐만 아니라 전체 과일에 대해 순환을 하면서 변경되지 않은 과일의 재고도 다시 `UILabel`에 새로 할당합니다. 이런 방식이 아닌 실제 사용된 과일을 특정하여 그 재고 값만을 다시 `UILabel`에 할당하는 방식을 위해서 생각한 내용이 `Observer` 방식을 활용하는 것이었습니다. 여기서 생긴 문제점은 기존에 선언한 `stockList`가 `dictionary`타입이라 하나의 프로퍼티가 모든 과일에 대한 값을 담고 있다는 것이었습니다. 이렇게 되면 관찰할 대상은 개별 과일과 수량이 아닌 전체 과일이 되기 때문에 여기서 특정 과일을 지칭하기가 어려웠습니다. 이러한 경우에 각각의 과일을 구조체와 같은 타입으로 변경하여 각각 관찰할 수 있는 인스턴스로 만드는 것이 괜찮은 방법일까요? ### `enum` 타입에서 `rawValue`의 사용과 `switch`문을 사용한 해결 - `enum`의 `case`에 따라 각각 다른 동작을 수행합니다. `enum`의 `case`에 따라 구분하기 위해서 `switch`문을 사용하여 각각의 `case`에 대한 처리를 진행하였습니다. ```swift switch juice { case .strawberryJuice: <#code#> case .bananaJuice: <#code#> case .pineappleJuice: <#code#> case .kiwiJuice: <#code#> case .mangoJuice: <#code#> case .strawberryBananaJuice: <#code#> case .mangoKiwiJuice: <#code#> } ``` `switch`문을 사용하는 경우 `enum`의 `case`가 추가될 시 `switch`문에서도 수정을 해야 한다는 부분에서 확장성이 떨어져 보인다고 생각하였습니다. 실례로 개방-폐쇄 원칙(OCP)을 지키기 위한 하나의 방법이 무분별한 `switch`문의 사용을 지양하는 것이라는 내용을 보았습니다. 이러한 생각을 가지고 개별 동작을 구분하기 위한 방법으로 `rawValue`를 활용하는 방안을 생각해 보았습니다. ```swift Juice(rawValue: title) ``` 위와 같이 `Juice`의 어떤 특정 `case`에 대해 `rawValue`를 사용하여 값을 초기화하는 방법을 사용하였습니다. 여기서 든 궁금점은 `enum`에서 `rawValue`의 사용은 사용자 입장에서 어떠한 타입이 사용되는지에 대한 예측이 불가능하단 점에서 지양하는 방식이라고 들었습니다. 여기서는 어떤 값에 접근하기 위해 `enumCase.rawValue`처럼 사용하는 것이 아니어서 괜찮다고 생각하였습니다. 두 경우 모두 각각의 장단점들이 있었고 어떤 방법이 여기서 더 어울릴지와 이 외에 다른 생각지 못한 방법이 있을지 궁금합니다. ## STEP 2 답변 - `IBOutlet Collection`은 `Array` 타입으로 구조체인 값 타입의 변수입니다. `weak` 키워드는 클래스 타입인 경우에 참조할 때 `reference count`를 증가시키지 않기 위해 사용합니다. `weak` 키워드를 `IBOutlet Collection`에서 사용하려고 하면 다음과 같은 에러 메시지를 출력합니다. ``` 'weak' may only be applied to class and class-bound protocol types, not '[UITextField]' ``` - `@IBOutlet`의 경우 `Implicitly Unwrapped Optionals(IUO)` 방법을 통해 옵셔널을 사용하며 이를 사용하면 변수에 값을 `lazy`하게 초기화 시킬 수 있습니다. View Controller에서 View를 사용하기 위해 연결하는 `IBOutlet` 변수는 View가 메모리에 할당된 이후에 이를 가지고 초기화를 진행하며 `IUO`가 아닌 `Optionals(?)`를 사용할 수도 있지만 기본적으로 지원하는 `IUO` 방식을 사용하였습니다. 확실한 안정성을 위해서라면 `Optionals`를 사용하는 것이 좋겠지만 `@IBOutlet attribute keyword` 자체에서 Interface Builder에 있는 View와 연관하여 사용하는 변수라는 것을 컴파일러에게 사전에 알려줬기에 만약 사용하지 않을 View 또는 변수였다면 처음부터 선언할 필요가 없었다는 점에서 `viewDidLoad` 메서드가 호출된 이후라면 `IUO` 방식을 통해 간편하게 사용할 수 있다고 생각하였습니다. # PR: STEP 3 ## 고민되었던 점 ### `FruitStore Singleton class` 사용 - 기존 `FruitStore`는 `class`가 아닌 `structure`로 사용하고 있었습니다. ```swift struct FruitStore { private var stockList: [Fruit: Int] = [:] init(stockQuantity: Int = 10) { Fruit.allCases.forEach { stockList[$0] = stockQuantity } } ... } ``` 하지만 재고 추가 화면인 `StockManagementViewController`에서도 `FruitStore`의 프로퍼티인 `stockList`에 대해서 접근하여 재고 추가에 대한 처리를 진행해야 했습니다. 현재 사용 중인 `JuiceOrderViewController`와 `StockManagementViewController` 모두 같은 값을 가지고 있는 `stockList`를 참조함으로써 화면 간에 데이터 전달이 아닌 공유를 하면 좋겠다는 판단에 싱글톤 클래스로 선언하였습니다. ```swift class FruitStore { static let shared: FruitStore = FruitStore() private var stockList: [Fruit: Int] = [:] private init(stockQuantity: Int = 10) { Fruit.allCases.forEach { stockList[$0] = stockQuantity } } ... } ``` ### segue를 사용하여 화면 이동시 데이터 전달 방법 - 화면 간의 전환 방법을 `storyboard`의 기능을 활용하기 위해 `segue`를 사용하였습니다. 코드에서도 다음과 같이 작성하였습니다. ```swift self.performSegue(withIdentifier: "goToStockViewController", sender: nil) ``` 이미 만들어진 `segue`가 있으니 데이터를 전달할 때도 `segue`를 활용해 보는 건 어떨까 하는 고민이 들었습니다. `segue`를 통해 화면을 전환할 때 `prepare` 메서드가 호출된다는 걸 알게 되었고 이를 오버라이딩하여 데이터를 전달하는 데 사용해 보았습니다. ```swift override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let stockManagementViewController = segue.destination as? StockManagementViewController else { return } stockManagementViewController.configurationDelegate = self } ``` ### `delegate`을 사용하여 이전 화면의 `UILabel` 업데이트 - `StockManagementViewController`에서 재고에 추가된 내용을 `FruitStore`의 `stockList`에 저장하였습니다. `JuiceOrderViewController`의 화면에서 `StockManagementViewController`로 전환할 때 `present`를 사용하여 전환하였기 때문에 다시 이전 화면인 `JuiceOrderViewController`로 변경 시 `View State Method`의 `viewWillAppear` 메서드가 다시 호출되지 않는다는 문제점이 있었습니다. `modalPresentationStyle`을 `.fullScreen`로 설정해 주어야 `viewWillAppear` 메서드가 다시 호출되어 그 안에서 `UILable` 값을 변경해 줄 수 있었습니다. 이러한 문제를 해결하고자 이전 화면으로 돌아가기 전 `StockManagementViewController` 화면의 `viewWillDisappear` 메서드에서 `delegate`을 활용하여 이전 화면의 `UILabel` 값을 다시 입력하도록 수정하였습니다. ```swift // JuiceOrderViewController protocol Configurable { func assignLabelText() } extension JuiceOrderViewController: Configurable { func assignLabelText() { configureStockLabel() } } final class JuiceOrderViewController: UIViewController { ... override func prepare(for segue: UIStoryboardSegue, sender: Any?) { guard let stockManagementViewController = segue.destination as? StockManagementViewController else { return } stockManagementViewController.configurationDelegate = self } ... } ``` ```swift // StockManagementViewController final class StockManagementViewController: UIViewController { ... var configurationDelegate: Configurable? override func viewWillDisappear(_ animated: Bool) { super.viewWillDisappear(animated) changeStockList() configurationDelegate?.assignLabelText() } ... } ``` ### `Auto layout stack view` 사용 - 재고 추가 화면에서 각각의 과일에 따른 이미지와 수량을 나타내는 `UILabel`과 `UIStepper`가 있습니다. 각각의 객체마다 제약 조건을 맞추는 것보다는 3개의 객체를 하나로 묶어서 맞추어 일관성을 주는 게 좋다고 생각했습니다. ## 해결이 되지 않은 점 ### `StockManagementViewController` 초기화 - `StockManagementViewController` 내부의 몇몇 메서드 내에서 `stockList`,`fruitStockLabelCollection`등 같은 변수를 선언해 사용하고 있는데 중복되는 변수를 줄이고자 class의 프로퍼티로 선언을 하려고 하였습니다. 프로퍼티로 선언을 하면 다음과 같은 컴파일 에러가 발생하였습니다. > Cannot use instance member 'fruitStore' within property initializer; property initializers run before 'self' is available 이처럼 초기화되기 전에 사용되는 인스턴스의 문제를 해결하기 위해 `laze var`와 `init`를 사용해 보려 했습니다. - `lazy var` ```swift private lazy var stockList: [String] = fruitStore.getRemainStock() private lazy let fruitStockLabelCollection: [UILabel] = [strawberryStockLabel, bananaStockLabel, pineappleStockLabel, kiwiStockLabel, mangoStockLabel] ``` 위와 같이 `laze var`를 사용한 경우는 문제가 없었습니다. 하지만 `UIViewController`와 그 상위 클래스들에 대한 이해가 부족하여 `init`를 사용하는 방법은 더 이상 진행할 수 없었습니다. ## 조언을 얻고 싶은 부분 ### `protocol`, `extension` 파일 분리 - protocol과 extension을 분리해 파일로 만들어야 하는지 궁금합니다. 또한 파일을 분리하는 경우 어떤 폴더에 속해야 할지 궁금합니다. 다른 오픈된 프로젝트 폴더 구조를 보면 `Utils`라는 폴더 밑에 `Extension`과 `Protocol`이라는 폴더를 만들어 사용하는 것을 보았습니다. 이렇게 분리하는 게 좋을까요? ## STEP 3 PR 코멘트 답변 ### 싱글톤 - 하나의 인스턴스를 공용으로 사용한다 - 변경 시점이 명확하지 않다 - 메모리 사용량을 줄일 수 있다 - - ## 리팩토링 - button의 title/identifier등 문자열 사용과 tag의 Int를 활용한 caseiterable 사용 비교 - 의견 1. 이 버튼이 무엇을 넘겨주는지 확실히 하고자 문자열을 넘겨주는게 어떠한가(스위치 사용이 강제된다는 점이 있다..?) - 의견 2. 확장성을 고려하면 Int를 넘겨주어 (Enum의 CaseIterable을 활용) index값으로 활용하는게 어떠한가 - 결론: `UIbutton`의 `title`을 가져와 `Enum`의 `rewValue`와 매칭시켜 해결하였습니다. - 오류처리 위치가 `ViewController` 또는 `JuiceMaker` 둘중 어디가 적합할까 - 조언을 받아 고민해봤지만 현재 프로젝트의 특성상 MVC관점의 구분은 힘들듯해 그것보단 중복체크 방지를 고려해 기존 `JuiceMaker`에서 `ViewController`로 위치를 변경했습니다. - tag 관련 수정 - stocklist 딕셔너리 구조일 때 관찰하는 방법? - 초기화 - Viewcontroller에서 Fruit을 사용하지 않으려면 stockList를 배열화 시켜서 가져와야할듯하다.(물론 전체 재고 불러온다는 전제하에) - Juice.rawValue 타입이 String인게 맞을까요? ![](https://hackmd.io/_uploads/HkLt6AhBh.jpg) 꺄항

    Import from clipboard

    Paste your markdown or webpage here...

    Advanced permission required

    Your current role can only read. Ask the system administrator to acquire write and comment permission.

    This team is disabled

    Sorry, this team is disabled. You can't edit this note.

    This note is locked

    Sorry, only owner can edit this note.

    Reach the limit

    Sorry, you've reached the max length this note can be.
    Please reduce the content or divide it to more notes, thank you!

    Import from Gist

    Import from Snippet

    or

    Export to Snippet

    Are you sure?

    Do you really want to delete this note?
    All users will lose their connection.

    Create a note from template

    Create a note from template

    Oops...
    This template has been removed or transferred.
    Upgrade
    All
    • All
    • Team
    No template.

    Create a template

    Upgrade

    Delete template

    Do you really want to delete this template?
    Turn this template into a regular note and keep its content, versions, and comments.

    This page need refresh

    You have an incompatible client version.
    Refresh to update.
    New version available!
    See releases notes here
    Refresh to enjoy new features.
    Your user state has changed.
    Refresh to load new user state.

    Sign in

    Forgot password

    or

    By clicking below, you agree to our terms of service.

    Sign in via Facebook Sign in via Twitter Sign in via GitHub Sign in via Dropbox Sign in with Wallet
    Wallet ( )
    Connect another wallet

    New to HackMD? Sign up

    Help

    • English
    • 中文
    • Français
    • Deutsch
    • 日本語
    • Español
    • Català
    • Ελληνικά
    • Português
    • italiano
    • Türkçe
    • Русский
    • Nederlands
    • hrvatski jezik
    • język polski
    • Українська
    • हिन्दी
    • svenska
    • Esperanto
    • dansk

    Documents

    Help & Tutorial

    How to use Book mode

    Slide Example

    API Docs

    Edit in VSCode

    Install browser extension

    Contacts

    Feedback

    Discord

    Send us email

    Resources

    Releases

    Pricing

    Blog

    Policy

    Terms

    Privacy

    Cheatsheet

    Syntax Example Reference
    # Header Header 基本排版
    - Unordered List
    • Unordered List
    1. Ordered List
    1. Ordered List
    - [ ] Todo List
    • Todo List
    > Blockquote
    Blockquote
    **Bold font** Bold font
    *Italics font* Italics font
    ~~Strikethrough~~ Strikethrough
    19^th^ 19th
    H~2~O H2O
    ++Inserted text++ Inserted text
    ==Marked text== Marked text
    [link text](https:// "title") Link
    ![image alt](https:// "title") Image
    `Code` Code 在筆記中貼入程式碼
    ```javascript
    var i = 0;
    ```
    var i = 0;
    :smile: :smile: Emoji list
    {%youtube youtube_id %} Externals
    $L^aT_eX$ LaTeX
    :::info
    This is a alert area.
    :::

    This is a alert area.

    Versions and GitHub Sync
    Get Full History Access

    • Edit version name
    • Delete

    revision author avatar     named on  

    More Less

    Note content is identical to the latest version.
    Compare
      Choose a version
      No search result
      Version not found
    Sign in to link this note to GitHub
    Learn more
    This note is not linked with GitHub
     

    Feedback

    Submission failed, please try again

    Thanks for your support.

    On a scale of 0-10, how likely is it that you would recommend HackMD to your friends, family or business associates?

    Please give us some advice and help us improve HackMD.

     

    Thanks for your feedback

    Remove version name

    Do you want to remove this version name and description?

    Transfer ownership

    Transfer to
      Warning: is a public team. If you transfer note to this team, everyone on the web can find and read this note.

        Link with GitHub

        Please authorize HackMD on GitHub
        • Please sign in to GitHub and install the HackMD app on your GitHub repo.
        • HackMD links with GitHub through a GitHub App. You can choose which repo to install our App.
        Learn more  Sign in to GitHub

        Push the note to GitHub Push to GitHub Pull a file from GitHub

          Authorize again
         

        Choose which file to push to

        Select repo
        Refresh Authorize more repos
        Select branch
        Select file
        Select branch
        Choose version(s) to push
        • Save a new version and push
        • Choose from existing versions
        Include title and tags
        Available push count

        Pull from GitHub

         
        File from GitHub
        File from HackMD

        GitHub Link Settings

        File linked

        Linked by
        File path
        Last synced branch
        Available push count

        Danger Zone

        Unlink
        You will no longer receive notification when GitHub file changes after unlink.

        Syncing

        Push failed

        Push successfully