Github
: https://github.com/YohanBlessYou/yagom-open-marketContributor
: yohan, July협업방식
: 페어 프로그래밍 (commit 단위로 돌아가며 작성)기간
: 21.12.20 ~ 21.12.31 (페어2주 + 솔로2주)메인화면 | 상품등록 | 상품수정 |
---|---|---|
![]() | ![]() | ![]() |
아래와 같은 기준으로 폴더를 분리하고 View와 ViewController에 대한 코드구조를 create
/organize
/configure
로 통일하는 시도. 프로퍼티 초기화없이 나머지를 하면 바로 exception나므로 create를 가장 먼저 해줘야 하고, addSubview없이 오토레이아웃걸면 마찬가지로 exception나므로 organize를 그 다음에 바로 해줘야 한다
상품등록 화면과 상품수정 화면은 UI가 매우 유사하므로 코드를 재사용하는 것을 고려해볼 수 있습니다. 이를 위해, 두 화면의 view를 통째로 모델링한 ProductEditingView
를 만들어 서로 다른 부분은 변경이 가능하도록 인터페이스 메서드를 구현해주었습니다. 이는 요구사항이 변경되지 않는다면 괜찮은 구현일 수 있으나, 만에 하나 두 화면의 다른 부분이 늘어난다면 (특히 어느 한 화면에만 있는 UI가 발생한다면) 변경이 매우 어려운 구조라는 문제가 있었습니다
이를 해결하기 위해 한 덩어리로 된 view를 세분화해주는게 필요했습니다. 세분화 level은 세세할수록 변경에는 용이하나 재사용성은 떨어지는 부분이 있어 미래에 변경될 가능성이 적은 "적당한 수준"으로 세분화하는 것이 타당해보였습니다. NavigationBar와 ImageScrollView는 코드규모가 어느정도 있으면서 요구사항이 변경될 가능성이 낮다 판단되어 별도의 타입으로 구현해주었고 그 외 TextField/TextView는 마찬가지로 개별 UI 단위로는 변경 가능성이 낮으나 코드규모가 작아 재사용성에 따른 이득이 크지 않으므로 별도의 Custom 타입으로 구현하지 않고 UITextField
/UITextView
그대로 사용하였습니다
Custom type을 정의하는 경우 이를 다시 상속해서 또 다른 Custom type을 만들어 쓰는 등 상속으로 인해 복잡도가 올라가 가독성이 낮아집니다. 규모가 큰 코드에서 이런 상속 구조를 파악하기 위해선 일일히 검색해야 하므로 자식 타입이 없는 경우 final 처리를 통해 수고를 덜 수 있습니다
Grid
/ListCollectionViewController
는 상위 ViewController의 presentation 수행을 위해 viewPresentationDelegate
라는 프로퍼티를 가집니다. 이는 서로를 참조하는 상황이므로 반드시 한 쪽은 약한참조 처리를 해주어야 참조순환 문제가 발생하지 않으므로 주의가 필요합니다
네트워크 API 요청을 사용하면 JSON 포맷의 요청/응답을 parsing하기 위해 Codable
을 채택하는 타입을 설계하게 됩니다. 이 때 API 간 필요한 Parsing 타입이 겹치더라도 개별적으로 두는 것이 좋습니다. 이 부분은 이름이라던지 required 여부라던지 하는 세세한 부분이 빈번히 변경되기 때문입니다
제곧내
제곧내
지연초기화를 위해 사용하는 lazy var 프로퍼티가 서로를 필요로 하면 무한루프에 빠지므로 각별한 주의가 필요하다
버튼에 addTarget
을 해줄 때 objc의 selector를 등록하게 된다. selector 메서드를 호출할 때 argument를 전달할 순 없을까하는 의문이 들었다. 예로, selector가 호출된 시점에 self의 어떤 파라미터를 읽어 프린트한다던지 등의 수요가 있을 것 같다
하지만, "확실히 안된다"는 답변은 찾기 어려웠고.. 참고포스팅에 나오는 objc의 selector 사용법에 따르면 addTarget 내부에서 selector를 호출하는 구문에 argument를 넣어줘야 하므로 외부에서 코드를 짜는 우리는 조절이 불가할 것 같다는 추정이다
상품 등록 시 각 이미지 용량을 300KB로 제한하는 요구사항이 있었다. 이런 용량 관련 요구사항은 현업에서는 너무나 당연한 것으로 생각된다. 하지만, 이번 프로젝트를 하며 이 부분에 많은 시간을 쏟았으나 온전한 정답을 찾진 못했다. 특정 용량으로의 최적화 방법은 찾지 못했고 용량을 줄이기 위해 이미지 크기와 화질을 조절하는 방법을 학습하였다
Resizing
이미지 크기와 용량이 정비례할 것이라 생각했으나 실제론 그렇지 않았다. 수소문 끝에 알아낸 것은 이미지가 렌더링될 때 사용되는 메모리는 widht height bytesPerPixel이 맞으나, 이미지가 이미 특정 포맷으로 압축된 상태이므로 resizing하더라도 정비례하게 줄지 않는다는 것이다
Compression
jpegData()
URL을 기반으로 서버로부터 이미지를 받아 Cell을 채워주는 경우 재사용에 관한 이슈를 항상 염두에 두어야 합니다. 아래와 같이 이미지 로딩의 completion handler에 해당 Cell에 로딩된 이미지를 넣는 방식을 사용하게 됩니다. 만약 로딩이 완료되지 않은 상태에서 스크롤이 발생하여 다른 indexPath에 대해 해당 Cell이 재사용될 경우 원래 의도했던 indexPath가 아닌 다른 indexPath의 Cell에 해당 이미지가 업데이트 되는 문제가 발생합니다
이를 막기 위해 보통 로딩된 이미지를 UI 업데이트하기 전에 조건문을 넣어 원래 의도한 indexPath의 Cell이 아니라면 업데이트하지 않도록 하는 방식을 사용하게 됩니다. 이 조건문에는 다양한 기준이 사용될 수 있는데 indexPath가 사용될 수도 있고 아래와 같이 이미지 URL을 사용할 수도 있습니다
guard self.currentThumbnailURL == product.thumbnail else { return }
DispatchQueue.main.async {
self.imageView.image = UIImage(data: data)
}
현재 코드에서 상품상세화면에서 상품수정으로 넘어갈 경우 URL을 기반으로 해당 상품에 대해 서버가 가지고 있는 모든 이미지를 불러와 StackView에 넣어주는 구조입니다. 이처럼 네트워크 로딩으로 여러 개의 이미지를 받아와 업데이트해주는 경우에서 요청순서와 응답순서가 다를 수 있으므로 필요하다면 순서를 지켜주기 위해 추가 처리가 필요합니다
현재 코드에선 이를 위해 이미지가 들어갈 UIImageView
를 StackView에 먼저 넣어놓고 인덱스를 사용하여 로딩이 완료된 이미지를 절절한 위치의 ImageView에 넣는 방식을 사용하고 있습니다
처음에 이미지 데이터 로딩을 위해 Data(contentsOf: URL)
를 사용했는데 이는 동기로 동작하기 때문에 네트워크 기반 URL에는 적합하지 않습니다. 네트워크 로딩에는 URLSession의 dataTask
를 사용해야 합니다. Data(contentsOf: URL) 공식문서 참고
dynamic type을 정상적으로 지원하려면 텍스트 자체와 텍스트를 담은 레이블 (버튼이라면 버튼까지) 모두가 dynamic type에 따라 늘어날 수 있도록 설정해줘야 합니다
UIFontMetrics
를 사용해줘야 합니다label.adjustsFontForContentSizeCategory = true
label.translatesAutoresizingMaskIntoConstraints = false
button.translatesAutoresizingMaskIntoConstraints = false
PNG는 무손실 압축인 반면 JPEG은 손실 압축이다. 따라서 JPEG이 화질 손실은 발생하나 압축률은 더 높다
모든 인스턴스의 값이 같다면 타입 프로퍼티로 사용하는 것을 고려할 수 있다. 이는 메모리 관점에서 효율적이기 때문이다. 하지만, 만약 생성하는 인스턴스 수가 많지 않다면 메모리 life time 관점에서는 인스턴스와 함께 해제되는 인스턴스 프로퍼티를 사용하는게 유리할 수 있다
StubURLSession
처럼 호출되면 정해진 값(HTTP status code 등)을 반환하는 테스트 더블을 만들어야 하는 경우 Stub이 사용된다고 파악했습니다App 메인 화면에서 상품목록을 보여주는 방식으로 List와 Grid 모두를 지원하며 segmented control로 전환하도록 구현되어 있습니다. 스위칭마다 List/Grid를 새로이 빌드하여 보여주는 것은 매우 비효율적이므로 일단 둘 다 그려놓고 하나씩 hide하는 방식을 사용하거나 혹은 (현재 코드처럼)scroll view로 왔다갔다하는 방법이 있습니다
Table/Collection View의 기본 style cell에 대해서는 defaultConfiguration
혹은 contentConfiguration
를 사용하여 "WWDC: Modern cell configuration"에서 소개하는 방식으로 효율적으로 cell 설정이 가능합니다. 하지만 Custom Cell에 대해선 해당 WWDC 세션에서 소개되지 않아 조사가 필요합니다
참고포스팅1
참고포스팅2
이번 프로젝트를 하며 CollectionView를 새로이 학습하고 구현해보았습니다. 그 과정에서 다양한 시행착오가 있었지만 가장 기억에 남는 것은 DiffableDataSource
을 사용한 것입니다. 전통적인 UICollectionViewDataSource
과는 달리 snapshot
이라는 개념이 도입되었고 이전 snapshot과 이후 snapshot을 비교하여 변경된 부분에 대해서만 갱신을 하기에 성능면에서 우수하다는 점이 매력적이었습니다
하지만 이는 일부분이므로 WWDC: Advances in UI Data Sources 시청 필요
단순히 사용법을 익히고자 FlowLayout 대신 사용하였는데 각 방식의 장.단점 비교 필요
현재 코드는 첫 page의 20개 item만을 받아오도록 되어 있는데 pagenation을 구현하여 스크롤 다운에 반응하여 다음 페이지를 불러오도록 구현 필요