[iOS] URLSession 코드 비교 시리즈 02 : Combine

Emily·2025년 5월 20일
1

URLSession

목록 보기
2/3

같은 출력값을 여러가지 방법을 통해 구현해보며 각각의 프레임워크에 대한 이해도를 높여보고자 한다. 이번 포스팅에서는 시리즈 01에서 completionHandlerDispatchQueue를 통해 구현했던 API 통신을 Combine을 사용하여 구현할 것이다. (통신한 URL과 데이터 모델, 커스텀 에러 코드는 이전 포스팅에 정리되어 있다.)

01) CurrentValueSubject 선언

let todos: CurrentValueSubject<[Todo], CustomError> = .init([])

데이터와 에러를 업데이트 받고 값을 방출할 Publisherview model에 선언하고 controller에서 구독하여 화면과 바인딩하도록 한다.

02) fetchTodos 메소드 정의

dataTaskPublisher 메소드는 네트워크 통신 결과에 따라 Output (data: Data, response: URLResponse) 또는 Failure (error: URLError) 타입의 퍼블리셔를 반환한다. 에러 발생 시 mapError를 통해 에러 타입을 CustomError로 변환하여 반환하고, 네트워크 통신 성공 시 tryMap을 통해 응답 데이터를 반환한다. tryMap 과정에서 발생할 수 있는 에러도 커스텀으로 정의한 에러로 던져(throw) 준다. 그 다음 data를 디코딩하여 그 결과를 CurrentValueSubject에 전달한다.

func fetchTodos() {
    let urlString = "https://jsonplaceholder.typicode.com/todos"
        
    guard let url = URL(string: urlString) else {
        // 1. URL 유효성 체크
        todos.send(completion: .failure(CustomError.invalidURL))
        return
    }
        
    // 2. 네트워크 요청(Request)
    URLSession.shared.dataTaskPublisher(for: url)
        // 3. 요청 실패인 경우 error 반환
        .mapError { error -> CustomError in
            return CustomError.etc(error)
        }
        // 4. response를 HTTPURLResponse로 타입 캐스팅
        .tryMap { (data: Data, response: URLResponse) -> Data in
            guard let response = response as? HTTPURLResponse else {
                throw CustomError.invalidResponse
            }
                
            // 5. 응답 코드 체크 (200번대여야 통신 성공)
            guard (200..<300).contains(response.statusCode) else {
                throw CustomError.badResponse(response.statusCode)
            }
                
            return data
        }
        // 6. 디코딩
        .decode(type: [Todo].self, decoder: JSONDecoder())
        .mapError { error in
            if error is CustomError {
                return error
            }
            return CustomError.decodingFailed
        }
        // 7. 결과값을 publisher인 todos에 전송
        .sink(receiveCompletion: { [weak self] completion in
            switch completion {
            case .failure(let error):
                self?.todos.send(completion: .failure(error as? CustomError ?? CustomError.etc(error)))
            case .finished:
                print("[URLSession] dataTask finished")
            }
        }, receiveValue: { [weak self] todos in
            self?.todos.send(todos)
        })
        .store(in: &cancellables)
}

03) todos를 구독하여 데이터 바인딩

receive 메소드를 통해 결과 처리를 메인 스레드에서 이루어지도록 했다.

private func bind() {
	viewModel.todos
		.receive(on: DispatchQueue.main)
		.sink { completion in
            switch completion {
            case .failure(let error):
                print(error.debugMessage)
            case .finished:
                return
            }
        } receiveValue: { [weak self] todos in
            self?.todoTableView.reloadData(todos: todos)
        }
        .store(in: &cancellables)
}

에러 발생 테스트는 시리즈 01에서 모두 다뤘으므로 생략한다. 다음에는 async await를 사용하여 구현해보려 한다.

profile
iOS Junior Developer

2개의 댓글

comment-user-thumbnail
2025년 5월 22일

어 이거 뭔가 Rx에서 먹어본듯한 맛이에요

1개의 답글