-
iOS 오픈마켓 앱을 구현합니다.
-
SwiftLint를 통해 코딩 컨벤션을 규정합니다.
-
URLSession 및 HTTP 프로토콜을 이해하고 네트워크 계층을 추상화 합니다.
-
유지보수 및 사용성이 쉬운 네트워크 API 타입을 설계합니다.
-
사용자 경험을 향상시키기 위한 접근성 지원 및 키보드 제어를 구현합니다.
-
Test-Driven-Development
및 목업 객체를 통해 네트워크 연결이 없는 상황에서의UnitTest
를 구현합니다. -
프로토콜 기본구현, 클래스 상속을 활용하여 공통기능을 모듈화합니다.
오픈마켓 동작화면 |
---|
리뷰어 | 개발자 | 의존모둠원 |
---|---|---|
오픈마켓 ⓵: 찰리 @kcharliek 오픈마켓 ⓶: 코리 @corykim0829 |
@singularis7 신나 | @jsim27 나무 |
의존 모둠원 칭찬하기 |
- 메인 프로젝트 기간: 2022.01.03 ~ 2022.01.28
- 추가 유지보수 기간: 2022.02.01 ~ 2022.02.04
- 평일 오전 10시 30 ~ 저녁 8시 (필요 시 30분 정도 조정 가능)
- 밥먹는 시간 : 1시간 ~ 1시간 30분
- 공식적인 휴일 : 편할때 쉽시다~
- 모르거나 새로운 개념을 적용하고 싶을 때 바로 찾아볼 시간을 주세요!
- 매번 스탭이 끝날 때마다 코드 설명해주기!
- 커뮤니케이션을 진행할 때 편하고 적극적으로 표현해주세요!
- PR 보내기 전에 고민했던점 공유하기!
- 단위 : 함수를 구현, 기능 구현, SwiftLint 규칙을 따릅니다.
- 컨벤션 : Karma Style
- URLSession, URLDataTask를 활용하여 비동기 네트워크 통신을 이해합니다.
- HTTP 프로토콜에 맞추어 자료 및 첨부파일을 송수신 할 수 있도록 설계합니다.
- JSON 인코딩, 디코딩을 위한 Model Type을 설계합니다.
- 네트워크 모듈을 손쉽게 사용하고 유지보수 할 수 있도록 추상화시킵니다.
- Pull Request : Reviewed By 🧑🏻💻 @kcharliek Step 1 Pull Request Link
- Modern CollectionView를 통해 상품 리스트 화면을 구현합니다.
- Compositional Layout 을 통해 LIST, GRID 레이아웃을 구현합니다.
- DiffableDataSource을 통해 data-driven UI update를 구현합니다.
- RefreshControl 및 페이지네이션을 통한 새로운 데이터를 로드합니다.
- Pull Request : Reviewed By 🧑🏻💻 @kcharliek Step 2 Pull Request Link
- 상품 등록 화면을 구현합니다.
- 상품 수정 화면에서 공통으로 사용될 기능을 모듈화 시킵니다.
- 키보드가 TextEditor를 가리지 않도록 제어합니다.
- Dynamic Type, 접근성 레이블 지정을 통한 접근성 지원을 구현합니다.
- Pull Request : Reviewed By 🧑🏻💻 @corykim0829 Step 3 Pull Request Link
- 상품 세부정보 화면을 구현합니다.
- 상품 정보를 수정하고 삭제하는 기능을 구현합니다.
- 이전 스텝에서 구현한 공통 모듈을 활용해 상품 수정 화면을 구현합니다.
- NSCache를 도입하여 이미지 캐싱 기능을 구현합니다.
- ViewModel을 도입하여 ViewController의 present 역할을 분리해주었습니다.
- Pull Request : Reviewed By 🧑🏻💻 @corykim0829 Step 4 Pull Request Link
-
서로 다른 객체간에 메시지를 주고 받는 방식
-
ViewController의 역할을 분리하면서 서로 다른 객체간의 메시지 통신 방법으로 다음을 적용해보았습니다.
-
Closure Capture를 활용해 인스턴스의 메서드를 전달하는 방법
-
Delegation 패턴을 활용하여 대리자를 통해 메서드를 호출하는 방법
-
관찰자 패턴을 통해 구독 모델을 활용하여 메시지를 전달하는 방법
-
-
Delegation 패턴은 다음의 목적으로 사용하였습니다.
- 두 객체간에 의존성을 줄여주고 싶을때
- 실질적인 행위를 외부의 객체가 구현하도록 하고 싶을 때
- MVC 패턴에서 View가 외부의 객체를 통해 데이터를 받아오도록 구현하고 싶을때
-
관찰자 패턴(Notification Center, KVO, Property Observer)은 다음의 목적으로 사용하였습니다.
- Model 데이터가 수정된 이벤트를 감지하여 View에 데이터 바인딩 시켜주고 싶을때
- ViewController는 UI에 관한 정보를 알고있기 때문에 Model과 UI간의 독립 관계를 유지하도록 구현하고 싶을때
-
클로저 캡처를 활용한 방식은 다음의 목적으로 사용하게 되었습니다.
- 앱에서 수행되는 네트워크 통신은 비동기적으로 동작합니다.
- 네트워크 통신이 완료 혹은 실패했을 때 이벤트를 통해 특정 기능을 수행해야할 수 있습니다.
- 예를 들어 Model의 값을 변경하거나 UI를 갱신하는 행위가 해당될 수 있습니다.
-
-
객체간 통신 방법으로 Closure Capture 및 delegation 패턴을 선택한 사유
-
NotificationCenter의 문제점
- 관찰자 객체와 전달되는 메시지가 많아지면 성능에 손실이 있을 수 있습니다.
- 코드의 흐름 파악이 어려워 디버깅 과정이 어려워질 수 있습니다.
- 살아있는 모든 객체가 이벤트를 받을 수 있다는 점에서 데이터 흐름을 제어하기 어렵습니다.
- 코드 운영의 측면에서 메시지를 주고 받는 목적으로 도입하기에 다량의 코드를 작성해야 합니다.
-
KVO의 문제점
- Objective-C의 런타임에 의존하기에 레거시 코드를 사용하게 됩니다.
- 런타임시에 실행할 코드의 주소를 추적한다는 측면(다이나믹 디스패치)에서 성능상의 손실이 발생합니다.
- 코드의 운용 측면에서는 Notification Center 에서와 비슷한 문제점을 공유하고 있습니다.
- 다만, NotificationCenter 처럼 메시지가 앱의 전역적으로 송출되는 문제점을 줄일 수 있다는 차별점이 존재합니다.
-
클로저 캡쳐 및 델리게이션 패턴의 장점
- 레거시 코드를 사용하지 않습니다.
- 메시지 전달을 위해 작성해야 하는 코드의 양이 적습니다.
- 메시지가 전역적으로 방송되지 않기에 실행 흐름을 추적하기에 유리합니다.
-
-
View를 구현하는 방식에 관한 고민 XIB(NIB) vs Storyboard vs Code
-
상품 등록 및 수정 화면은 공통 모양의 입력 양식을 사용합니다.
-
스토리보드의 Scene에 View를 구현하면 target-action 패턴에 의해 View가 ViewController에 종속되는 문제점이 있습니다.
-
공통의 View를 재사용할 수 있도록 별도의 모듈(템플릿)로 구현하는 방법을 다음과 같이 생각해보았습니다.
- StoryBoard를 활용한 View 구현 방법
- Code를 활용한 View 구현 방법
- XIB(NIB)를 활용한 View를 구현 방법
-
스토리보드 방법의 역활 및 장단점에 관하여 다음과 같이 생각해보았습니다.
- 서로 다른 화면간의 전환 맥락과 한 화면 의미를 시각적으로 파악할 수 있습니다.
- 실제 앱을 구현하기 전에 간단한 동작을 목업의 형태로 구현해보기에 적합합니다.
- ViewController와 View간에 의존성이 높아져서 재사용성이 떨어집니다.
- 협업시에 코드나 XIB 보다 병합 충돌 문제가 자주 발생할 수 있습니다.
-
Code 방법의 역할 및 장단점에 관하여 다음과 같이 생각해보았습니다.
- 협업시에 스토리보드 혹은 XIB(XML) 에서 발생할 수 있는 병합 충돌 문제를 해결하기에 유리합니다.
- 인터페이스 빌더를 활용해 구현하기 어려운 화면을 코드를 통해 세밀하게 구현할 수 있습니다.
- 코드만 보고 화면의 모습이나 전체적인 구성을 파악하기에 어려움이 있습니다.
- 복잡한 오토 레이아웃을 구현할 때 늘어나는 코드를 관리하기 어렵습니다.
-
XIB 방법의 역할 및 장단점에 관하여 다음과 같이 생각해보았습니다.
- 작은 단위로 View 를 구현하기에 스토리보드 보다는 병합 충돌 발생가능성이 낮으나 완전히 해결된 것은 아닙니다.
- 인터페이스 빌더를 통해 시각적으로 화면의 구성을 파악하고 구현할 수 있습니다.
- 코드로 구현하는 것 보다는 복잡한 화면을 구성함에 있어 어려울 수 있다. (자유도가 떨어진다.)
-
각 3가지 방법의 장점을 추려서 다음의 구조를 프로젝트에 접목해보았습니다.
-
재사용 가능성이 높은 View는 XIB를 우선적으로 활용하여 모듈화 구현했습니다.
-
앱 구조에 관하여 다른 개발자와 시각적으로 커뮤니케이션할 수 있도록 화면 전환 및 각 Scene을 스토리보드에 구현했습니다.
-
모듈화된 View를 스토리보드의 Scene 위에 얹을 때에는 코드를 활용하여 스토리보드에서 발생할 수 있는 병합 충돌 문제를 줄일 수 있도록 구현했습니다.
-
-
-
Modern CollectionView가 기존의 delegation 방식과 갖는 구조적 차이점
-
Modern Collection View를 접했을 때, delegation 패턴으로 구현하는 Data Source와 구조적 차이가 있어서 코드 사용법 이해에 어려움을 겪었습니다.
-
WWDC20의 Advanced in diffable data sources 영상에서 diffableDatasource라는 별도의 객체를 통해 CollectionView의 UI 상태와 SnapShot의 데이터 차이를 기반으로 UI를 갱신해주고 있다는 점을 확인하였습니다.
-
전통적인 컬렉션 뷰 구현방식은 View가 이벤트를 받으면 datasource delegate 를 통해 데이터를 갱신하는 구조를 갖습니다.
-
이번에 새롭게 등장한 diffable Data Source 는 apply 해줄 때 입력 받은 snapshot 데이터의 변동사항을 기준으로 View를 갱신해주고 있어서 데이터의 흐름 방향이 기존과 반대의 구조를 갖는다고 생각하였습니다.
-
이런 구조적 차이가 초기 Modern CollectionView에 데이터를 공급하는 코드 패턴을 이해하기 어렵게 만들었다고 생각합니다.
-
-
Compositional Layout 의 확장성 있는 레이아웃 설계가 불러온 어려움
- CollectionView 는 cell, delegate, datasource 등등 여러 객체가 협력하여 우리의 눈에 보이는 구조를 갖는다고 이해하고 있습니다.
- 초심자가 사용하기에 파악해야하는 클래스가 많아서 시작점을 파악하기 어려운 단점을 지녔다고 생각합니다.
- 사용자가 compositional layout 을 통해 item, group, section 에 관하여도 너무나도 자유롭게 설정할 수 있습니다.
- 별도로 제공되는 옵션인 flow 혹은 List layout 을 활용하는 방법도 존재합니다.
- 애플의 ModernCollectionView 코드 프로젝트를 가이드라인으로 참조하였습니다.
- URLSession을 활용한 서버와의 통신
- JSON 데이터와 맵핑할 모델 설계
- CodingKeys 프로토콜을 활용한 JSON Serialization
- 네트워크 상황과 무관한 TDD & UnitTest 설계
-
OpenMarketService의 역할
- 서버 API 문서를 기준으로 7개의 기능을 사용성을 고려하려 추상화합니다.
- API가 요구하는 형식에 적합한 URLRequest를 만들어 줍니다.
- 열거형의 연관값으로 필수 인자를 받아서 request를 생성해줍니다.
- 개발자는 열거형 타입을 통해 호출 가능한 목록을 손쉽게 얻을 수 있습니다.
enum OpenMarketService { case checkHealth case createProduct(sellerID: String, params: Data, images: [Data]) case updateProduct(sellerID: String, productID: Int, body: Data) case showProductSecret(sellerID: String, sellerPW: String, productID: Int) case deleteProduct(sellerID: String, productID: Int, productSecret: String) case showProductDetail(productID: Int) case showProductPage(pageNumber: Int, itemsPerPage: Int) }
-
URLSessionProvider 타입을 만들게된 배경
- HTTP에 기반한 통신 과정에서 서버에 요청하고 응답을 받는 과정을 추상화합니다.
- 서버에서 응답을 받을 때 completionHandler로 통신결과에 따른 세부 동작을 구현합니다.
- 네트워크가 없는 상황에서 테스트할 수 있도록 Mock과 Server에 대한 구현체를 주입합니다.
- 의존성 주입을 위해 필요한 동작을 URLSessionProtocol을 통해 추상화 시켰습니다.
sutURLSesssionProvider.request(.showProductPage(pageNumber: 1, itemsPerPage: 10)) { result in switch result { case .success(let data): print("통신에 성공했을때 동작을 여기에 구현합니다!") case .failure(let error): print("통신에 실패했을때 동작을 여기에 구현합니다!") } }
-
HTTP 메서드에 따른 범용적 사용을 위한 URLRequest 추상화
- 첫 설계에서는 HTTP메서드 기준으로
중간 단계 추상 타입
을 구현하였습니다. - Body Content Type에서의 중복 구현이 발생하는 문제점이 발생하였습니다.
- 따라서 Content Type을 기준으로 분류하여 추상화 하는 방식을 선택하였습니다.
- 실제 서비스에서는 HOST API 주소가 다양할 수 있다는 점을 고려하여 별도의 서비스 프로토콜로 분리 구현하였습니다.
- 네트워크 계층을 설계하면서 프로토콜 지향 프로그래밍을 도입하여 프로토콜 기본 구현을 적극 사용하였습니다.
protocol OpenMarketInfoOwner { var baseURL: String { get } } protocol OpenMarketAPIRequest: APIRequest, OpenMarketInfoOwner { } protocol OpenMarketJSONRequest: JSONRequest, OpenMarketInfoOwner { } protocol OpenMarketMultiPartRequest: MultiPartRequest, OpenMarketInfoOwner { }
- 프로토콜을 채택한 타입을 API 명세처럼 활용하는 사용성을 고려하여 설계하였습니다.
struct ShowProductDetailRequest: OpenMarketAPIRequest { var method: HTTPMethod var header: [String : String]? var path: String init(productID: String) { self.method = .GET self.header = nil self.path = "/api/products/\(productID)" } }
- 첫 설계에서는 HTTP메서드 기준으로
-
앱 모델과 API 서버 모델을 분리하여 구현하였습니다.
- 앱 모델은 서버 API의 Request 혹은 Response 타입에서 공통적인 요소를 추상화하여 Entity 타입으로 구현하였습니다.
- 예를 들어 Model 폴더의 Page, Product, ProductImage, Vendor, Currency가 존재합니다.
- 서버모델(API 요청, 응답 타입)은 Network 폴더의 API 모델 폴더에 구현하였습니다.
- 서버 모델이 앱 모델과 동일하다면 앱 모델과 typealias 를 통해 연동해주었습니다.
- 서버 모델이 앱모델과 다르다면 요청, 응답 타입을 정의해주었습니다.
- 이를 통해 서버의 API 명세가 달라져도 API 타입 설계만 수정하여 사용할 수 있도록 유지보수성을 개선하였습니다.
-
네트워크 비동기 메서드 테스트를 위한 전용 메서드를 도입해보았습니다.
- DispatchSemaphore를 사용하여 비동기 메서드의 결과를 받을때까지 스레드를 멈춰두는 방식을 사용했습니다.
- 테스트 프레임워크에서 제공하는 전용 인스턴스인 XCTestExpextation를 활용해보았습니다.
class URLSessionProviderDecodingTests: XCTestCase { var sutURLSesssionProvider: URLSessionProvider! var sutTestExpectation: XCTestExpectation! override func setUpWithError() throws { self.sutURLSesssionProvider = URLSessionProvider(session: URLSession.shared) self.sutTestExpectation = XCTestExpectation() } override func tearDownWithError() throws { self.sutURLSesssionProvider = nil self.sutTestExpectation = nil } func test_showPage가_200번때_상태코드를_반환해야한다() { sutURLSesssionProvider.request(.showProductPage(pageNumber: "1", itemsPerPage: "10")) { (result: Result<ShowProductPageResponse, URLSessionProviderError>) in switch result { case .success(let data): print(data) XCTAssertTrue(true) self.sutTestExpectation.fulfill() case .failure(let error): XCTFail("\(error)") self.sutTestExpectation.fulfill() } } wait(for: [sutTestExpectation], timeout: 10.0) } }
- Safe Area을 고려한 오토 레이아웃 구현
- Collection View의 활용
- Mordern Collection View 활용
- Modern Collection View 통해 데이터 중심의 UI 데이터 바인딩 방식으로 구현했습니다.
- UICollectionViewDiffableDataSource
- 재사용 셀을 반환시켜주는 역할을 담당합니다.
- UICollectionViewCellRegistration
- cell에 담길 데이터를 설정해주는 역할을 담당합니다.
- snapshot
- 컬렉션 뷰에서 보여줄 데이터의 상태를 스냅샷으로 정의할 수 있습니다.
- 데이터가 갱신될 때 새로운 스냅샷을 생성하여 데이터 소스에 apply 해주면 컬렉션 뷰가 차이점이 있는 데이터만 변경해줍니다.
- 자동으로 예쁜 애니메이션 효과가 적용됩니다.
- UICollectionViewDiffableDataSource
- Segmented Control을 활용하여 LIST와 GRID 사이의 레이아웃 전환 구현 방법을 고민하였습니다.
- Segmented Control 은 현재 화면에서 보여줘야 할 컬렉션뷰 레이아웃을 표현합니다.
- ProductPageViewController은 LIST와 GRID Collection View를 보여줄 수 있습니다.
- 각 레이아웃을 갖는 두개의 CollectionView를 교차하여 보여주는 방식을 채택하였습니다.
- 메모리를 더 사용하여 처리로 인하여 발생되는 main 스레드 block 현상을 줄이는 전략을 사용하였습니다.
- Segmented Control 상태에 따라서 이전 CollectionView를 제거하고 새로운 CollectionView를 보여주는 방식으로 동작합니다.
- 하단 방향의 스크롤 동작시에 페이지네이션 기능이 동작하도록 구현했습니다.
- scrollview의 delegate 메서드중 드래그가 끝났을 때 호출 되는 콜백 메서드를 활용하였습니다.
- 컨텐츠 bound의 하단 좌표에 도달하면 모델 객체에게 새로운 데이터를 요청하도록 명령합니다.
- 모델이 데이터 갱신 후 viewcontroller에게 알려주면 viewController는 데이터 소스에 수정된 스냅샷을 apply 해서 UI를 갱신합니다.
- UIRefreshController를 통해 ScrollView 상단 방향 스크롤시에 새로운 데이터를 불러오도록 구현하였습니다.
- 각 컬렉션 뷰는 refreshController 인스턴스를 갖고 있습니다.
- subView로 refreshController 를 갖고 있기에 아래로 당겼을 때 로딩중임을 나타내는 애니메이션 효과를 확인할 수 있습니다.
- target-action 패턴을 통해 refreshController 가 데이터를 갱신하는 메서드를 호출하도록 액션을 등록해주었습니다.
- UIActivityIndicatorView를 통해 초기 로딩 시 사용자에게 로딩중임을 알림
- 애니메이션 효과가 멈추면 자동으로 숨겨지는 기능을 사용하였습니다.
- 모델에 전달된 네트워크 요청이 끝나면 stopanmation 메서드가 호출되어 애니메이션이 중단됩니다.
- 이후 activityIndicator가 자동으로 숨겨집니다.
- 사용자 친화적인 UI/UX 구현 (적절한 입력 컴포넌트 사용, 알맞은 키보드 타입 지정)
- 상속을 통해 (수정/등록 과정의) 공통기능 구현
- URLSession을 활용한 multipart-form 요청 전송
- ViewController가 더 적은 일을 하도록 역할을 나눠주고자 노력했습니다.
- Delegate 객체, 모델 객체, 네트워크 객체 등 좀더 세밀하게 역할을 나누었습니다.
- 공통적으로 사용되는 기능의 경우 extension을 통해 공통 모듈로 분리해주었습니다.
- 상품 목록, 수정, 세부 정보, 제거 기능의 데이터 및 서비스 로직을 담당하는 ProductModelManager 모델을 정의하였습니다.
- 상품수정, 상품 등록과 같이 공통의 View 를 사용하는 경우 별도의 View nib 를 통해 템플릿을 생성하여 재사용성을 높히고자 노력했습니다.
- 스토리보드는 전체 화면의 전개에 관한 Overview를 보여줄 수 있는 수단으로써 장점이 있습니다.
- nib를 통한 템플릿 뷰를 선언해주는 것은 재사용이 용이한 View를 작성하는 수단으로써 장점이 있습니다.
- 코드를 통한 View 작성 방식은 위 방법으로 구현하기 어려운 화면 구성을 구현할 수 있다는 장점이 있지만 코드가 나타내는 화면 구성을 이해하기 어렵다는 단점이 있습니다.
- 위 고민 끝에 nib로 만든 재사용 성이 높은 View를 스토리보드 상의 Scene에 프로그래밍 방식을 통해 SubView를 추가시켜주는 방식을 사용하게 되었습니다.
- 다음 스텝에서 새로운 화면을 구성할 때 기존에 nib로 정의한 View를 손쉽게 생성할 수 있게되었습니다.
- 상품 수정에 관한 ViewController를 구현할 떄에도 공통기능을 정의한 ProductUpdateViewController를 상속하여 정의하면 공통 모듈을 최대한 재사용할 수 있는 구조가 됩니다!
- 동일한 모델 데이터에 대하여 각 화면에 알맞은 모습으로 변환시켜주는 모델 객체로써 각 화면에 관한 ViewModel로 사용하였습니다.
- 각 화면에서 자료를 보여주는 방법을 지정해주고, 사용자의 의도가 담긴 이벤트가 발생했을 때, 의도를 해석하여 서비스 로직으로 처리해줍니다.
- 상품수정과 상품 등록 뷰컨트롤러에 대한 키보드 처리 등의 공통 기능을 클래스 상속을 활용하여 구현해보았습니다.
- ProductUpdateViewController에서 상품 갱신에 필요한 키보드 처리 등의 공통 기능을 구현하였습니다.
- 이를 상속받아 구현한 상품 수정 뷰컨과 상품 등록 뷰컨은 이미지 등록 가능 여부로 구현이 갈리게 되었습니다.
- 상품에 관한 전반적인 네트워크 통신을 관할하는 ProductNetworkManager 객체에 NSCache를 활용하여 이미지 캐싱을 구현하였습니다.
- 이미지 정보의 캐싱 정보가 중복되지 않도록 해당 객체를 싱글턴 인스턴스로 사용하고 있습니다.
- 즉, 각 모델이 네트워크 매니저의 싱글턴 인스턴스를 참조하여 사용하며, 그에 따라 앱 내부에서 공통으로 이미지 캐싱 정보를 사용하게 되었습니다.
- 사용자 친화적인 UI/UX 구현
- 상속을 통해 (수정/등록 과정의) 공통기능 구현
- 얼럿 컨트롤러 액션의 컴플리션 핸들러 활용
- 얼럿 컨트롤러의 텍스트 필드 활용
- URLSession을 활용한 multipart-form 요청 전송
- 상품 등록 과정에서 문제가 발생할 경우 사용자가 어떤 행위를 취해야 하는지 알림창을 통해 알려줄 수 있도록 구성하였습니다.