-
Unsplash API 를 이용하여 사용자가 다양한 이미지를 보고 메모와 함께 이미지를 저장할수 있는 어플입니다.
-
두개의 화면으로 이루어진 어플이며 아래와 같은 역할을 합니다.
Home: API를 통해 사진 목록을 보여주는 화면입니다.Save: ⭐️ 를 표시한 ‘저장된’ 사진 목록을 보여주는 화면입니다.
| @Kai |
|---|
- Presentation, Domain, Data, 3가지 Layer 로 구성되었습니다.
- 해당 아키텍처는
Clean Architecture의 개념을 적용하였고,DDD(Domain Driven Design)방식으로 진행 되었습니다.
[View] (View & ViewController)
- 사용자와 어플리케이션의 원활한 상호작용 을 도와줍니다.
- 사용자에게 업데이트된 화면을 보여주고, 이벤트 핸들링 을 담당합니다.
[ViewModel]
- 필요한 데이터를 가공 및 받아와 View 에 전달합니다.
- View 에 보여지는 데이터의 생성 및 가공은 모두 ViewModel 에서 처리됩니다.
- 각 이벤트에 맞는 Usecase 를 이용하여 View 에서 요청된 작업을 진행합니다.
- Domain Layer 는 해당 어플리케이션의 핵심 로직을 포함하고 있습니다. Presentation, Data Layer 는 Domain Layer 를 의존하고 있어 Domain 에서 변경이 생길시 다른 레이어도 바뀐 로직에 따른 수정이 요구 됩니다. (반대로 Presentation, Data Layer 가 바뀌어도 Domain Layer 의 로직에는 수정이 필요하지 않습니다)
[Model]
- 어플리케이션에 사용되는 핵심 정보들을 그룹화시켜 모델로 정의합니다.
- 해당 어플리케이션은
Image라는 모델을 가지고 있고, Unsplash API 에서 에 받아오는 정보중 이미지를 보여줄때 필요한 정보들을 모델화 시켰습니다.
[Usecase]
- 시스템의 동작을 사용자의 입장에서 표현한 시나리오이며, 시스템에 관련한 요구사항을 알아내는 과정입니다.
- 사용자가 이루려고하는 행동 하나하나를 인터페이스화 시켰고, 이를 통해 Usecase 만 보아도 어떤 기능을 하는 어플리케이션인지 파악할수 있도록 구성하였습니다.
[Repository] (Interfaces)
- 각 Usecase 에서 필요한 데이터를 Network Service 혹은 Persistent Storage 에서 가져오는 역할을 합니다.
- Domain Layer 에서는 Reopository 의 인터페이스만 구성하고 구현체는 DataLayer 에서 구성됩니다. 따라서 Usecase 에서 Repository 는 dependency inversion 이 적용되어 data Layer 가 domain Layer 에 의존할수 있게 됩니다.
[Repository] (Implementation)
- 각 Usecase 에서 필요한 데이터를 Network Service 혹은 Persistent Storage 에서 가져오는 역할을 합니다.
- 서버에서 받아오는 데이터 DTO, Persistent Storage 에서 받아오는 Entity 등이 Domain Model 로 맵핑 되는 곳입니다.
[NetworkService]
- 네트워크 에 필요한 데이터를 요청하는 인터페이스와 로직을 가지고 있습니다.
- 따라서 실질적인 Network 구현체에 사용되는 기술스텍은 사용자가 선택할수 있습니다.
- 현재 프로젝트에선 URLSession 이 사용되었고 Network Layer 를 만들어 각 Usecase 마다 필요한 API 가 담긴 EndPoint 를 사용해 서버와 통신합니다.
[Persistent Storage]
- 이미지를 기기 내부에 저장하거나 삭제할때 사용됩니다.
- 현재 프로젝트에선 CoreData 가 사용되었습니다.
[Layout]
- 2개의 열을 가진 가변형 높이의 Cell을 가진 레이아웃으로 구성합니다.
- 두 열 중 길이가 짧은 열의 아래쪽으로 새로운 사진을 배치합니다
- 사진은 원본 이미지의 종횡비 를 계산하여 잘림, 왜곡 없이 표현 되었습니다.
[기능]
- 이미지를 길게 탭할시 메모를 남길수 있는 Alert 이 나타납니다.
- Alert 을 통해 사진을 저장할시,
Persistent Store에 이미지에 대한 정보를 저장합니다. - 저장이 완료되면 이미지의
별 모양 버튼이 채워진 채로 표현 됩니다.
[Layout]
- 저장된 사진이 없을시
Default화면이 보여집니다. - 1개의 열을 가진 가변형 높이의 Cell을 가진 레이아웃으로 구성합니다.
- 사진은 원본 이미지의 종횡비 를 계산하여 잘림, 왜곡 없이 표현 되었습니다.
- 이미지 좌상단
별 모양 버튼이 채워진 채로 표현 됩니다. - 사진 우상단에 저장된 메모를 보여줍니다.
[이미지 삭제]
- 사진을 길게 눌러 삭제할 수 있습니다.
- Alert을 통해 삭제 여부를 재확인합니다.
- 삭제 시 선택된 이미지에 대한
NSManaged Object를Persistent Store에서 삭제 합니다.
1.0 Network Layer 설계
- API 호출의 확장성/유지보수 를 고려하여 네트워크 레이어 설계
ex) 한 페이지에 나타낼수 있는 사진 개수를 변경/ 특정 사진만 검색하는 기능 이 추가 될경우 를 위하여
API 분석
사용 예:
https://api.unsplash.com/photos/?client_id=AK-
bF5BwmWGtMcRiSr3Kd74cD0f3QbYlTnhwwYoaxiI&per_page=50
Base:
Paths:
- 전체 사진 받기
→ photos/
QueryItems:
- Client_id ⇒ AccessKey
[
{
"id": "LBI7cgq3pbM",
"created_at": "2016-05-03T11:00:28-04:00",
"updated_at": "2016-07-10T11:00:01-05:00",
"width": 5245,
"height": 3497,
"color": "#60544D",
"blur_hash": "LoC%a7IoIVxZ_NM|M{s:%hRjWAo0",
"likes": 12,
"liked_by_user": false,
"description": "A man drinking a coffee.",
"user": {
"id": "pXhwzz1JtQU",
"username": "poorkane",
"name": "Gilbert Kane",
"portfolio_url": "https://theylooklikeeggsorsomething.com/",
"bio": "XO",
"location": "Way out there",
"total_likes": 5,
"total_photos": 74,
"total_collections": 52,
"instagram_username": "instantgrammer",
"twitter_username": "crew",
"profile_image": {
"small": "https://images.unsplash.com/face-springmorning.jpg?q=80&fm=jpg&crop=faces&fit=crop&h=32&w=32",
"medium": "https://images.unsplash.com/face-springmorning.jpg?q=80&fm=jpg&crop=faces&fit=crop&h=64&w=64",
"large": "https://images.unsplash.com/face-springmorning.jpg?q=80&fm=jpg&crop=faces&fit=crop&h=128&w=128"
},
"links": {
"self": "https://api.unsplash.com/users/poorkane",
"html": "https://unsplash.com/poorkane",
"photos": "https://api.unsplash.com/users/poorkane/photos",
"likes": "https://api.unsplash.com/users/poorkane/likes",
"portfolio": "https://api.unsplash.com/users/poorkane/portfolio"
}
},
"current_user_collections": [ // The *current user's* collections that this photo belongs to.
{
"id": 206,
"title": "Makers: Cat and Ben",
"published_at": "2016-01-12T18:16:09-05:00",
"last_collected_at": "2016-06-02T13:10:03-04:00",
"updated_at": "2016-07-10T11:00:01-05:00",
"cover_photo": null,
"user": null
},
// ... more collections
],
"urls": {
"raw": "https://images.unsplash.com/face-springmorning.jpg",
"full": "https://images.unsplash.com/face-springmorning.jpg?q=75&fm=jpg",
"regular": "https://images.unsplash.com/face-springmorning.jpg?q=75&fm=jpg&w=1080&fit=max",
"small": "https://images.unsplash.com/face-springmorning.jpg?q=75&fm=jpg&w=400&fit=max",
"thumb": "https://images.unsplash.com/face-springmorning.jpg?q=75&fm=jpg&w=200&fit=max"
},
"links": {
"self": "https://api.unsplash.com/photos/LBI7cgq3pbM",
"html": "https://unsplash.com/photos/LBI7cgq3pbM",
"download": "https://unsplash.com/photos/LBI7cgq3pbM/download",
"download_location": "https://api.unsplash.com/photos/LBI7cgq3pbM/download"
}
},
// ... more photos
]| Scheme | Host | Path | Query |
|---|---|---|---|
| https | api.unsplash.com | photos | client_id, page, per_page |
해결
- API 에대한 기본적인 정보들을 가지고 있습니다.
- api key: API 사용에 필요한 api key
- header: HTTP 해더
- baseURL: API 의 baseURL
- API 요청시 특정한 EndPoint 가르킬수있도록 하기위한 정보를 가지고 있습니다.
- method: HTTP 메소드
- path: host 주소의 특정한 리소스를 받을수 있는 path 명시
- queryItem: 특정한 path 에 받아올 리소스의 특정 제약사항을 명시
- EndPoint 와 APIConfigurator 를 사용하여 최종적인
URLRequest를 만듦니다.- body: HTTP 요청시 body 에 보낼 데이터
- endPoint: 특정 EndPoint
2.0 사용자 이벤트에 따른 Model 데이터의 상태 변화와 데이터의 흐름 설계
1.0 사용자의 상호작용에 따른 DTO, Model, ViewModel 흐름 설계
- 현 프로젝트에는 3가지의 데이터 스트럭처 (
DTO,Model,ViewModel) 들이 시스템 내에서 각기 다른 역할을 사용자의 요청을 완수하기 위해 사용되어집니다. - 각각 다른 이름으로 불리지만 비슷한 데이터를 가지고 있기때문에 어떤 데이터가 언제불려야하고, 어떤 책임과 역할을 하는지 정확한 흐름을 수립 해봐야 겠다는 고민을 했습니다.
2.0 사용자 이벤트에 따른 Model 데이터 변화
- 두개의 다른 화면에 사용되는 이미지 데이터는 사용자가 저장한 이미지를 표시할때 사용되는
Liked의 상태와 사용자의 메모Memo값의 여부만 다를뿐, 각 화면에 표시될 데이터는 같습니다. - 두화면의 이미지 데이터들은 완전히 공통적인 요소를 가지고 있기때문에 두 화면에서 사용되는 하나의 모델,
Image를 사용해서 구현 하려고 진행했습니다. - 하지만 요구사항을 구현하는과정에서 아래와 같은 문제 를 겪게 되었고 이미지가 저장되어질때 데이터의 변화 흐름을 수립해야 겠다는 고민을 했습니다.
Home화면에서 이미지를 메모를 남기고 저장을 할때
- 사용자가 입력한 메모는 HomeViewModel 에 있는 Image 데이터의
Like상태를 바꾸어주어도 되지만Memo에 사용자가 입력한 메모를 업데이트해주면 안됩니다. (Home 화면의 이미지 메모는N 번째 사진으로 그대로 남아 있어야합니다).
1.0 사용자의 상호작용에 따른 DTO, Model, ViewModel 흐름
1.0 서버에서 부터 raw 한 데이터들을 ImageDTO 를 사용하여 받는다.
2.0 실제 Cell 에 사용될 타입 과 디폴트 데이터들을 ImageDTO 에서 Image 로 맵핑한다.
3.0 ImageViewModel 에 실제로 화면에 보여지는 데이터 를 Image 를 사용해서 초기화/업데이트 한다.
4.0 Cell 에 ImageViewModel 을 맵핑 한다.
5.0 Cell 은 사용자에게 이미지에 대한 정보를 보여준다.
6.0 사용자가 Cell 를 터치하여 input 이벤트를 발생시킨다.
7.0 사용자의 input 에 따라 Cell index 에 해당하는 Image 데이터 를 가지고 CoreData 에 변경된 정보를 저장/업데이트 한다.
8.0 CoreData 에 성공적으로 변경이 적용됐다면 Image 의 정보를 업데이트 한다.
9.0 업데이트 된정보로 3.0, 4.0, 5.0 과정을 되풀이한다.
2.0 사용자 이벤트에 따른 Model 데이터의 상태 변화
아래 다이어그램은 사용자가 이미지를 저장 할때 데이터의 변화를 나타냈습니다.
- 1.0 선택한 index 에 있는 model 을 homeViewModel 로 부터 찾아줍니다.
- 2.0 순서에 맞는 model 의 copy 를 만들어 like, memo 정보를 바꾸어줍니다.
- 3.0 Image Model 은 ImageEntity 로 변환되어 persistent Store 에 저장됩니다.
- 4.0 Persistent Store 에 성공적으로 저장이 되었다면, 해당 image model 과 viewModel 의 like 상태를
true로 변경해 줍니다. - 5.0
Save화면으로 전환될때SaveViewModel은 persistent Store 에 저장되어 있는 NSManagedObject 를 Fetch 해오고, Image Model 로 변환된 데이터를 가지고 있습니다. - 6.0 Fetch 된 데이터를
Save화면에 나타내 줍니다.
아래 다이어그램은 사용자가 이미지를 삭제 할때 데이터의 변화를 나냈습니다.
- 1.0 삭제가 선택된 index path 에 맞는 image 모델을 찾아줍니다.
- 2.0 persistent Store 에 저장되어 있던 Entity 중 id 가 같는 오브젝트를 삭제 합니다.
- 3.0 성공적으로 삭제 되었다면 해당 Image 와 ImageViewModel 을 SaveViewModel 에서 삭제 시킵니다.
- 4.0 Home 화면으로 전환되었을때 현재 persistent store 에 저장되어있는 NSManagedObject 들을 fetch 해옵니다.
- 5.0 image Model 에서 liked 되어있는 model 들과 viewModel 중 persistent store 로부터 받아온데이터 에 없는 model 과 viewModel 의 like 상태를 false 로 업데이트시킵니다.
3.0 Custom CollectionView Layout 구현
- 해당 어플리케이션의
Home,Save화면은 CollectionView 로 구현이 되어있는데, 각각의 Cell 의 높이는 실제 이미지의 비율과 같은 비율로 화면에 나타나야합니다. - 여기서 Cell 의 높이는 현재 주어진 CollectionView width 와 행의 개수를 고려해서 각각 Cell 의 height 를 설정해주어야합니다.
- 이때
Home과Save화면 에 사용될 Layout 을 custom 하게 구현해야하는데, 1,2 개의 행 뿐만아니라 여러개의 행도 나타내어줄수있게 설계해 보려고 고민 했습니다.
해결
- 여러개의 행 을 만들기 위한 핵심적인 데이터를 가지고 있습니다.
- numberOfColumns: 원하는 행의 개수를 지정할수 있습니다.
- section: 하나의 화면에 여러개의 section 이 있을때 각 section 마다 다른 개수의 행을 가질수 있도록 section 값을 주었습니다.
- cellPadding: cell 들의 간격을 나타냅니다.
- cacheOptions: section 에 header, item, footer 등의 옵션을 줄수 있습니다.
- numberOfItemsPerPage: footer 가 있을 경우에 몇개의 item 마다 footer 의 position 을 업데이트 해주기 위해 선언했습니다.
- 각 Cell 에 사용될 사진의 비율을 viewcontroller 에서부터 받아올수 도록 delegate 함수를 가지고 있습니다.
- CollectionViewLayout 을 override 함으로써 prepare() 에 원하는 열개수만큼 보여지기 위해 로직을 구성하고 있습니다.
결과
4.0 Persistent Storage 구성하기
- 현 프로젝트의 기능은 ImageEntity 라는 NSManagedObject 1개로 요구사항을 충족시킬수 있다고 생각하여 1개의 Entity 만 선언하였고, 그 record 들을 관리하기위해 CoreData 를 사용했습니다.
- persistentContainer 를 사용하여 defualt viewContext 로 persistent store 를 관리해줍니다.
- persistent store 에 있는 데이터를 fetching, insert, delete 등 (Heavy lifting) 작업을 진행할땐 private queue 에서 진행되도록 performBackgroundTask() 메소드를 사용합니다.
- 각작업이 끝나면 background context 를 저장하여 main context 에 자동으로 업데이트 되도록 구현 해주었습니다.
5.0 DIContainer, FlowCoordinator 구성 하기
- 컴포넌트들의 단일 책임원칙을 어떻게 하면 더 명확히 나눌수 있을까? 라는 고민에서부터 시작되었습니다.
- 어플리케이션에 사용되는 모든 컴포넌트의 초기화 를 한곳에서 관리하면 유지보수시 초기화 관련 버그나, 기능을 추가해야할때 DIContainer 의 어떤부분을 봐야할지 명확해지므로 구현해봐야겠다는 생각을 했습니다.
- 화면내의 Navigation logic 을 한군데 모아 관리하는 flowCoordinator 또한 적용해 보면 좋겠다는 생각이 들었습니다.
-
2가지의 DIContainer,
AppDIContainer,SceneDIContainer가 적용되었습니다. -
AppDIContainer:- 핵심 네트워크 정보 (API key, baseURL, Header) 들을 사용해 Network Service 를 생성, SceneDIContainer 를 생성하는 역할을 합니다.
AppConfigurator라는 클래스를 가지고 있는데, 이는 프로젝트에 사용되는 핵심 API 정보들을 가지고 있습니다. 이를 통해서Repository에 사용되는NetworkService를 생성해줍니다.- 메소드
makeSceneDIContainer()는 위에서 초기화된 네트워크 서비스들을 SceneDIContainer 의 Dependencies 에 주입해 SceneDIContainer 를 생성하게됩니다.
-
SceneDIContainer:- 어플리케이션에 사용되는 ViewController, viewModel, Usecase, Repository, CollectionViewLayout 등을 생성하는 역할을합니다.
- 모든 컴포넌트를 생성할수 있는 팩토리인 SceneDIContainer 는 FlowCoordinatorDependencies 라는 프로토콜을 채택하고 있는데, 각 화면의 ViewController 를 생성시켜주는 메소드 를 포함하고 있습니다.
-
2가지의
AppFlowCoordinator,FlowCoordinator가 사용되었습니다. -
AppFlowCoordinator:- 앱이 시작될때 사용되는 Coordinator 로 AppDIContainer 를 사용하여 SceneDIContainer 를 생성, flowCoordinator 를 생성하여 어플리케이션의 시작을 알리는 역할을합니다.
- SceneDelegate 에서부터 사용되고, TabBarController, AppDIcontainer 를 생성자 parameter 로 할당받습니다.
-
FlowCoordinator:- 핵심 Embedded Controller 인 TabBarController 를 사용하여 탭을 구성하고
- 메소드
start()에 구성한 tabBarItems 를 tabBarController 의 viewController 로 할당합니다.
- 1.0 AppDIcontainer 는 핵심 네트워크 configuration 을 AppConfiguration 으로 부터 정의되어있는 AppKey, BaseURL, Header, 등을 사용하여 NetworkSerivce 들을 생성, 소유하고 있습니다.
- 2.0 SceneDelegate 에서 window 로 사용될 rootViewController 를 TabBarController 로 할당하고, 생성된 tabBarController 과 AppDIContainer 를 사용하여 AppFlowCoordinator 를 생성해줍니다.
- 3.0 AppFlowCoordinator 의 Start() 메소드 내에는 AppDIContainer 를 사용하여 SceneDIContainer 를 생성하게되는데, 이때 SceneDIContainer 의 Dependency 인 networkService 들은 APPDIContainer 에 정의되어있던 networkSerivce 가 사용됩니다.
- 4.0 SceneDIContainer 의 메소드
makeFlowCoordinator()를 통해서 FlowCoordinator 를 생성하게 됩니다. SceneDIContainer 는 TabBarController 를 가지고 있지 않기 때문에 FlowCoordinator 는 AppFlowCoordinator 가 가지고 있는 tabBarController 를 넘겨 받습니다. - 5.0 마지막으로 FlowCoordinator 내부에서 TabBarItems 를 생성시킨다음, Start() 메소드를 통해 tabBarController 의 viewController 를 할당해줍니다.












