YATA는 macOS용 자작 앱으로 Yet Another Telegra.ph App을 의미하는 약어이다. 약 2달 반 정도 개발하면서 생각나는 것들을 정리해본다.

Telegra.ph는 telegram에서 만든 웹에 글을 쉽게 게시할 수 있는 서비스이다.
Telegraph – a publishing tool that lets you create richly formatted posts with photos and all sorts of embedded stuff. Telegraph posts get beautiful Instant View pages on Telegram.
이 서비스에 대한 Telegra.ph API도 공개하고 있는데, 단순한 서비스라 복잡하지 않았다. API를 살펴보니 현재 웹에서 제공하지 않는 기능을 제공할 수 있어 보였다. 첫 macOS 앱으로 이 서비스를 선택한 이유이기도 하다.
가장 먼저 시작한 작업은 Telegra.ph API에 대한 작업이었다.
Model 구현#
API 문서의 Types에 나오는 것들을 하나씩 Model로 구현했다. ObjectMapper를 이용하여 JSON에 대응하였다.
다른 건 어려운 건 없었고, Node Type 하나가 좀 어려웠다. 문서를 보면 다음과 같이 나온다.
This abstract object represents a DOM Node. It can be a String which represents a DOM text node or a NodeElement object.
단순 값인 “문자열” 또는 NodeElement라는 “객체"가 될 수 있다고 나온다. ObjectMapper에서 이런 경우 자동으로 해주는 것이 없어, “Custom Transforms"로 다음처럼 직접 구현을 해줬다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| class Page: Mappable {
// 중간 생략
var content: [Node]?
// 중간 생략
func mapping(map: Map) {
// 중간 생략
content <- (map["content"], NodeArrayTransform())
// 중간 생략
}
}
|
JSON에서 읽을 때와 JSON으로 쓸 때에 대한 것을 구현했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
| class NodeArrayTransform: TransformType {
typealias Object = [Node]
typealias JSON = [Any]
func transformFromJSON(_ value: Any?) -> [Node]? {
guard let list = value as? [Any], list.count > 0 else {
return nil
}
var node: [Node] = []
for item in list {
if let string = item as? String {
node.append(Node(string: string))
} else if let dict = item as? [String: Any] {
if let nodeElement = NodeElement(JSON: dict) {
node.append(Node(element: nodeElement))
}
}
}
return node
}
func transformToJSON(_ value: [Node]?) -> [Any]? {
guard let list = value, list.count > 0 else { return nil }
var json: [Any] = []
for node in list {
switch node.type {
case .string:
json.append(node.value)
case .nodeElement:
json.append(node.element.toJSON())
}
}
return json
}
}
|
통신 구현#
통신 관련 구현은 Moya를 사용했다. Moya는 Alamofire 기반의 통신 라이브러리이다. 더불어 RxSwift도 지원하고 있다.
Moya를 이용하여 Telegra.ph API의 메쏘드들을 Swift enum타입의 case로 추상화시켜 구현했다.
예를 들어 createAccount의 추상화는 다음과 같다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
| struct CreateAccountParameter {
let shortName: String
let authorName: String?
let authorUrl: String?
func toDictionary() -> [String: Any] {
// 생략
}
}
enum TelegraphApi {
case createAccount(parameter: CreateAccountParameter)
// 생략
}
|
이를 사용하는 코드는 대략 다음과 같다. Moya의 RxSwift 확장을 이용했다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
| class Telegraph {
// 생략
private let provider = RxMoyaProvider<TelegraphApi>()
// 생략
func createAccount(shortName: String, authorName: String?, authorUrl: String?) -> Observable<Account> {
let observable = Observable<Account>.create { observer in
let param = CreateAccountParameter(shortName: shortName, authorName: authorName, authorUrl: authorUrl)
let disposable = self.provider.request(.createAccount(parameter: param))
.filterSuccessfulStatusCodes()
.mapJSON()
.map { data -> [String: Any]? in
return data as? [String: Any]
}
.subscribe(onNext: { value in
if let error = self.checkError(value: value) {
observer.onError(error)
return
}
let result = value?["result"] as! [String: Any]
guard let account = Account(JSON: result) else {
observer.onError(TelegraphError.WrongResultFormat)
return
}
observer.onNext(account)
observer.onCompleted()
}, onError: { error in
observer.onError(error)
})
return Disposables.create {
disposable.dispose()
}
}
return observable
}
// 생략
}
|
단위 테스트#
GUI가 없어 그나마 테스트하기 쉬웠다. XCode에서 제공하는 Test 도구를 이용하여 단위 테스트를 구현했다.
API 문서에 나와 있는 샘플을 참고하여 테스트 케이스에 사용했다.

초록색이 이렇게 마음이 편할 줄 몰랐다.