URLSession, Combine, Codable을 사용한 네트워킹

rbw·2023년 3월 31일
0

TIL

목록 보기
79/97

https://www.vadimbulavin.com/modern-networking-in-swift-5-with-urlsession-combine-framework-and-codable/

위 글을 보고 번역정리한 글, 자세한건 위 글을 봐주3


Implementing Networking Agent

agent 라는 HTTP 클라이언트를 만들어서 사용을 하고 이씀

struct Agent {    
    struct Response<T> {
        let value: T
        let response: URLResponse
    }
    
    func run<T: Decodable>(_ request: URLRequest, _ decoder: JSONDecoder = JSONDecoder()) -> AnyPublisher<Response<T>, Error> {
        return URLSession.shared
            .dataTaskPublisher(for: request)
            .tryMap { result -> Response<T> in
                let value = try decoder.decode(T.self, from: result.data) 
                return Response(value: value, response: result.response)
            }
            .receive(on: DispatchQueue.main)
            .eraseToAnyPublisher()
    }
}

run이라는 메소드를 사용하여, agent 내부 구조체인 Response<T>AnyPublisher로 감싸서 반환하는 모습 ~

파라미터로 URLRequest를 받고있고, 다른 파라미터는 기본값을 가지는데 이는 JSONDecoder이다.

HTTP 요청 추가

깃허브 API를 사용하고있고, 이를 위해 먼저 네임스페이스를 열거형으로 작성하였다.

enum GithubAPI {
    static let agent = Agent()
    static let base = URL(string: "https://api.github.com")!
}

첫 번째로 구현할 엔드포인트는 레포지토리 리스트입니다.

extension GithubAPI {
    static func repos(username: String) -> AnyPublisher<[Repository], Error> {
        let request = URLRequest(url: base.appendingPathComponent("users/\(username)/repos"))

        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }
}
struct Repository: Codable {
    // Skipping for brevity
}

HTTP 요청 생성 및 취소

이해를 돕기 위해 로그를 출력하는 모습, 그리고 cancel()을 사용하여 취소할 수 있다.

let token = GithubAPI.repos(username: "V8tr")
    .print()
    .sink(receiveCompletion: { _ in },
          receiveValue: { print($0) })

token.cancel()

Chaining Requests

다른 일반적인 작업은 요청을 하나씩 실행하는 것입니다. 이번에는 첫 번째 레포지토리를 가져온 다음, 여기에 해당하는 이슈들을 가져옵니다.

extension GithubAPI {
    static func issues(repo: String, owner: String) -> AnyPublisher<[Issue], Error> {
        let request = URLRequest(url: base.appendingPathComponent("repos/\(owner)/\(repo)/issues"))

        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }
}

아래 코드는 요청을 순차적으로(하나씩) 실행합니다.

let me = "V8tr"
// 아래 요청은 sink로 구독할 때까지 수행되지 않습니다.
let repos = GithubAPI.repos(username: me)
let firstRepo = repos.compactMap { $0.first } 

// 1
let issues = firstRepo.flatMap { repo in
    GithubAPI.issues(repo: repo.name, owner: me)
}
let token = issues.sink(receiveCompletion: { _ in },
                        receiveValue: { print($0) })

1번 코드에서 flatMap 컴바인의 연산자를 사용하여서 두 개의 요청을 결합합니다. 첫 번째 레포지토리를 반환하고, 해당 레포의 이슈를 반환합니다.

마지막 sink()로 요청을 수행합니다.

요청을 병렬로 실행

HTTP 요청이 서로 독립적인 경우 병렬로 실행하고 결과를 결합할 수 있습니다. 이렇게 하면 전체 로드 시간이 가장 느린 요청이랑 같기 때문에 연결하는 방식보다 속도가 빨라집니다.

독립적이지 않은 경우에는 사용하믄 안댐니다.

위에서 사용한 run 메소드를 리팩토링을 먼저 진행하겠습니다.

extension GithubAPI {

    static func run<T: Decodable>(_ request: URLRequest) -> AnyPublisher<T, Error> {
        // 함수를 한 번 래핑하여서 반복적으로 사용하기 쉽게 리팩토링 한 거 같습니다
        return agent.run(request)
            .map(\.value)
            .eraseToAnyPublisher()
    }
    
	...

    static func repos(org: String) -> AnyPublisher<[Repository], Error> {
        return run(URLRequest(url: base.appendingPathComponent("orgs/\(org)/repos")))
    }
    
    static func members(org: String) -> AnyPublisher<[User], Error> {
        return run(URLRequest(url: base.appendingPathComponent("orgs/\(org)/members")))
    }
}

// 이제 두 요청을 병렬로 호출하고 결과를 결합해 보겠습니다.
let members = GithubAPI.members(org: "apple")
let repos = GithubAPI.repos(org: "apple")
let token = Publishers.Zip(members, repos)
    .sink(receiveCompletion: { _ in },
          receiveValue: { (members, repos) in print(members, repos) })

두 개의 요청을 생성하고, 마지막 token에서 결합된 요청을 생성하고 실행합니다. Zip을 사용하여 두 요청이 모두 완료될 때까지 기다렸다가 튜플로 전달합니다.


마무리하며

이번에도 네트워킹 호출에 관련된 글을 읽어보았는데, Combine을 사용하여 요청을 병렬로 받거나 순차적으로 받는 방식에 대해서 알아보았습니다.

실제 프로젝트에서도 사용해볼법한 코드라고 생각이 되어 알아두면 좋겠다는 생각을해씀니다 ~

profile
hi there 👋

0개의 댓글