YATA

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

YATA

Telegra.ph

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

가장 먼저 시작한 작업은 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”로 다음처럼 직접 구현을 해줬다.

class Page: Mappable {
    // 중간 생략
    
    var content: [Node]?

    // 중간 생략

    func mapping(map: Map) {
        // 중간 생략
	
        content     <- (map["content"], NodeArrayTransform())

        // 중간 생략
    }
}

JSON에서 읽을 때와 JSON으로 쓸 때에 대한 것을 구현했다.

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의 추상화는 다음과 같다.

struct CreateAccountParameter {
    let shortName: String
    let authorName: String?
    let authorUrl: String?
    
    func toDictionary() -> [String: Any] {
        // 생략
    }
}

enum TelegraphApi {

    case createAccount(parameter: CreateAccountParameter)

    // 생략
}

이를 사용하는 코드는 대략 다음과 같다. Moya의 RxSwift 확장을 이용했다.

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 문서에 나와 있는 샘플을 참고하여 테스트 케이스에 사용했다.

unit test

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

Comments