위 글을 보고 번역정리한 글, 자세한건 위 글을 봐주3
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
이다.
깃허브 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
}
이해를 돕기 위해 로그를 출력하는 모습, 그리고 cancel()
을 사용하여 취소할 수 있다.
let token = GithubAPI.repos(username: "V8tr")
.print()
.sink(receiveCompletion: { _ in },
receiveValue: { print($0) })
token.cancel()
다른 일반적인 작업은 요청을 하나씩 실행하는 것입니다. 이번에는 첫 번째 레포지토리를 가져온 다음, 여기에 해당하는 이슈들을 가져옵니다.
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을 사용하여 요청을 병렬로 받거나 순차적으로 받는 방식에 대해서 알아보았습니다.
실제 프로젝트에서도 사용해볼법한 코드라고 생각이 되어 알아두면 좋겠다는 생각을해씀니다 ~