이런적 있나요,,,?😭
1. 리스트를 보여줘야하는 화면에서 아이템이 없을 경우, 하나만 있는 경우, 여러개 있는 경우 모두 다른 뷰를 보여줘야하는 뷰 코딩할 때, 뷰 확인을 위해 서버 개발자분에게 아이템 개수를 계속 바꿔달라고 하기 미안할 때!
2. 서버와 앱이 개발을 같이 시작해서 API가 나오지 않았는데, API를 연동해서 결과를 보고싶을 때!
3. API데이터에 대한 예외케이스를 처리하고싶을 때
저는 프로젝트를 진행하면서 이러한 경우를 너무 자주 마주했던 것 같았는데,,,
뉴비인 나녀석은 Mocking이란 것을 알기 전까지는 그저 API가 나오기 전까지 대기를 타거나,,,
API를 그냥 받았다고 가정하고(실제로는 API 통신을 하지 않고)코딩하기,,,
이렇게 멍청하게 진행을 했었는데요. Mocking이란 것을 알고난 뒤부터는 위의 3가지 케이스를 Mocking으로 모두 컨트롤하게 되었어요!
Mocking?🤔
Mock object은 위키에서 모의 객체라고 적혀져있습니다.
더 단순하게 말하면 가짜로 만든 데이터라 생각하면 될 것 같습니다.
가짜 데이터를 만들어서 앱에다가 주입시켜 실제 데이터 처럼 보여지게 하는 행위입니다.
즉 위에서 발생하는 케이스들을 전부 Mocking을 사용해 해결할 수 있다는 의미!
어떻게 만들면 되나!!👻
Modal 과 API Service 만들기
Mock 데이터를 만들기 위해 일단 호출할 API를 먼저 골라야 하는데요!
지금은 간단하게 호출할 수 있는 오버워치 API를 호출하겠습니다.
먼저 API 응답을 보고 모델링을 해야겠네요.
응답 데이터는 (overwatcharcade.today/api/overwatch/today) 여기를 클릭해서 들어가면 바로 확인할 수 있습니다.
응답 데이터를 확인한 뒤, 아래와 같이 모델을 생성했습니다.
(단순하게 만들기 위해서, nil처리는 따로 하지 않았습니다.)
// Arcade.swift
struct Arcade: Codable {
var created_at: String
var is_today: Bool
var modes: Modes
}
// Modes.swift
struct Modes: Codable {
var tile1: Mode
var tile2: Mode
var tile3: Mode
var tile4: Mode
var tile5: Mode
var tile6: Mode
var tile7: Mode
}
// Mode.swift
struct Mode: Codable {
var id: Int
var image: String
var name: String
var players: String
}
모델을 만들었으니 이제 API를 호출하는 서비스를 만들어봅시다.
서비스 클래스를 만들때, RxSwift + Alamofire를 사용했습니다.
// OverwatchService.swift
protocol OverwatchServiceProtocol {
func fetchArcade() -> Observable<Arcade>
}
struct OverwatchService: OverwatchServiceProtocol {
func fetchArcade() -> Observable<Arcade> {
return Observable.create { observer -> Disposable in
let urlString = "https://overwatcharcade.today/api/overwatch/today"
AF.request(urlString, method: .get).responseJSON { response in
if let value = response.value {
let arcade: Arcade = JsonUtils.toJson(object: value)! // 받은 데이터를 Arcade로 파싱해서 만들어주는 유틸 함수
observer.onNext(arcade)
observer.onCompleted()
}
else {
let error = CommonError(desc: "value is nil")
observer.onError(error)
}
}
return Disposables.create()
}
}
}
Mocking을 하기 위해 인터페이스로 프로토콜을 만들어두고 실제 API통신을 하는 구조체는 프로토콜을 채택해 구현했습니다.
API를 호출하면 Observable 형태로 반환이 되고 호출한 곳에서 subscribe만 하면 되는 형태로 구현했습니다.
API 호출해보기👋
API를 호출할 수 있는 Service를 만들었으니 ViewController에서 호출해봅시다.
// ViewController.swift
override func viewDidLoad() {
OverwatchService().fetchArcade().subscribe(
onNext: { arcade in
print(arcade)
},
onError: { error in
print(error)
}
)
.disposed(by: disposeBag)
}
ViewController에서 API를 호출하면 정상적으로 데이터를 받을 수 있습니다.
하지만 우리가 원하는 것은.. Mock 데이터를 만들어서 그 데이터를 받는게 이번 목표죠!
그러니까 호출해서 성공하면 로그 찍는 것만 만들어두고 이제 Mock데이터를 만들러 가봅시다.
Mock데이터 만들기
Mock데이터를 만드는 방법은 응답으로 오는 데이터를 Json파일로 미리 만들어 두고 API를 호출하는 대신에 Json 파일을 로드해서 응답으로 대신 보내주는 방식으로 진행을 합니다.
그러면 Mock데이터를 먼저 만들어봅시다. 일단 위에 API를 호출했을 때, 돌아오는 응답 Json형식의 데이터를 그대로 Json파일로 만들어 줍시다. 텍스트 에디터 혹은 vi로 ArcadeMockData.json 파일을 생성합니다.
// ArcadeMockData.json
{
"notice": "WARNING - This API will be deprecated and change breaking soon. I'm rewriting everything. Keep an eye out for changes on https:\/\/github.com\/OverwatchArcade.",
"created_at": "2021-02-10T00:00:44+00:00",
"is_today": true,
"user": {
"battletag": "Redhawk#11341",
"avatar": "https:\/\/overwatcharcade.today\/img\/avatars\/brigitte.jpg"
},
"modes": {
"tile_1": {
"id": 90,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/2655A94516FBA3CAD3FB39502E4ABB87.jpg",
"name": "Bounty Hunter",
"players": "6 Player FFA",
"label": null
},
"tile_2": {
"id": 91,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/6E3AEB865FBBC1B6DD31CBB020D987D8.jpg",
"name": "CTF Modes",
"players": "-",
"label": "Daily"
},
"tile_3": {
"id": 19,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/4509429F96BEC63894C718A5E5C21EC4.jpg",
"name": "Elimination",
"players": "3v3",
"label": "Daily"
},
"tile_4": {
"id": 61,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/E20EBDB332EBD018DA0704D1AB88AC4C.jpg",
"name": "Quick Play Classic",
"players": "6v6",
"label": null
},
"tile_5": {
"id": 51,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/CDB1B21B980488A5CB2AECE2711EFF8A.jpg",
"name": "Mystery Deathmatch",
"players": "8 Player FFA",
"label": "Daily"
},
"tile_6": {
"id": 17,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/9E44C7933CFB5317F0649E95E84EA243.jpg",
"name": "Limited Duel",
"players": "1v1",
"label": "Daily"
},
"tile_7": {
"id": 9,
"image": "https:\/\/overwatcharcade.today\/img\/gamemodes\/79EBA70D1695E915A810F1A41D0BB756.jpg",
"name": "Mystery Heroes",
"players": "6v6",
"label": null
}
}
}
데이터를 만들었으니 프로젝트에 import시켜줍니다.
import 시킬 때, 타겟에 프로젝트를 반드시 체크해 주세요.
데이터 import가 되면 이렇게 보여집니다.(저는 resource라는 디렉토리에 넣었어요!)
MockService 만들기
MockData를 만들었으니 MockData를 사용하는 MockService를 만들어줍시다.
MockService는 원래 API를 호출하던 OverwatchService와 똑같은 OverwatchServiceProtocol 프로토콜을 채택해서 구현해줍니다.
결과값으로는 바로 위에서 생성한 ArcadeMockData.json파일을 불러와서 Arcade 모델 형태로 만들어서 반환해주면 됩니다.
// OverwatchMockService.swift
import RxSwift
class OverwatchMockService: OverwatchServiceProtocol {
func fetchArcade() -> Observable<Arcade> {
return Observable.create { observer -> Disposable in
let filename = "ArcadeMockData.json"
var data: Data
guard let file = Bundle.main.url(forResource: filename, withExtension: nil)
else {
fatalError("Couldn't find \(filename) in main bundle.")
}
do {
data = try Data(contentsOf: file)
} catch {
fatalError("Couldn't load \(filename) from main bundle:\n\(error)")
}
do {
let decoder = JSONDecoder()
observer.onNext(try decoder.decode(Arcade.self, from: data))
observer.onCompleted()
} catch {
fatalError("Couldn't parse \(filename) as \(Arcade.self):\n\(error)")
}
return Disposables.create()
}
}
}
MockService 호출하기🙌
MockService를 만들었으니 이제 서비스 클래스만 Mock으로 변경해서 호출하면 우리가 원하는 MockData를 결과값으로 얻을 수 있습니다.
호출하는 Service의 이름만 OverwatchService() 에서 OverwatchMockService()로만 바꿔주면 바로 호출이 가능합니다.
override func viewDidLoad() {
OverwatchMockService().fetchArcade().subscribe(
onNext: { arcade in
print(arcade)
},
onError: { error in
print(error)
}
)
.disposed(by: disposeBag)
}
이제 Mock json 데이터를 자기가 원하는 값 또는 형태로 바꿔서 넣어주면 그 형태 그대로 데이터를 받을 수 있습니다!
이런 방법으로 데이터에 따라 분기처리를 해야하는 경우가 많을 때, Mock 데이터를 변경해서 원하는 형태로 바꿔서 손쉽게 처리를 할 수 있답니다~👋
'iOS' 카테고리의 다른 글
나의 iOS 아키텍처 패턴 일대기: MVC (0) | 2021.10.04 |
---|---|
[Swift] AutoLayout 코드로 그리기🖌 (Code base UI 그리기) (0) | 2021.03.10 |
📱Swift Struct vs Class : Class 안에 Struct가 있으면 어떻게 복사될까 (0) | 2020.08.28 |
📱Google sheet를 사용해서 iOS 문자열을 관리해보자! (2) | 2020.08.26 |
📱RxSwift + MVVM 패턴 iOS에 적용해보기 (간단한 로그인 화면) (0) | 2020.08.02 |