Notice
Recent Posts
Recent Comments
Link
«   2025/03   »
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
Archives
Today
Total
관리 메뉴

J

[Combine] Network 통신 (Upbit API 예제 포함) 본문

Combine

[Combine] Network 통신 (Upbit API 예제 포함)

yujaehui 2024. 11. 12. 19:24

Combine을 사용하여 동일한 네트워크 요청과 데이터 바인딩을 처리를 예제를 통해 알아보도록 하겠습니다.
Combine은 Swift의 선언적 프로그래밍 방식으로 데이터 스트림을 처리하며, 네트워크 통신과 같은 비동기 작업에도 유용합니다.


1. Network 통신 결과 Model

import Foundation

typealias Markets = [Market] // Markets는 Market의 배열

struct Market: Hashable, Decodable {
    let market, koreanName, englishName: String // 각 마켓 정보

    enum CodingKeys: String, CodingKey {
        case market
        case koreanName = "korean_name" // JSON 필드에 매핑
        case englishName = "english_name" // JSON 필드에 매핑
    }
}

1. Combine 기반 Network 클래스

Combine의 Future를 사용하여 네트워크 요청을 처리하도록 requestUpbitAPI() 메서드를 작성합니다.

import Foundation
import Combine

final class Network {
    static let shared = Network() // 싱글톤 패턴 사용
    private init() {} // 외부에서 인스턴스를 생성하지 못하도록 private 처리

    /// Upbit API에서 마켓 데이터를 요청
    func requestUpbitAPI() -> AnyPublisher<Markets, Error> {
        // 유효한 URL인지 검사
        guard let url = URL(string: "https://api.upbit.com/v1/market/all") else {
            // 잘못된 URL일 경우 즉시 실패하는 Publisher 반환
            return Fail(error: NetworkError.invalidURL).eraseToAnyPublisher()
        }
        
        // URLSession을 통해 네트워크 요청
        return URLSession.shared.dataTaskPublisher(for: url)
            .tryMap { output in
                // HTTP 응답 상태 코드 확인
                guard let response = output.response as? HTTPURLResponse, response.statusCode == 200 else {
                    throw NetworkError.invalidResponse
                }
                // 응답 데이터를 반환
                return output.data
            }
            .decode(type: Markets.self, decoder: JSONDecoder()) // JSON 디코딩
            .receive(on: DispatchQueue.main) // UI 업데이트를 위해 메인 스레드에서 실행
            .eraseToAnyPublisher() // AnyPublisher 타입으로 변환하여 반환
    }
}

enum NetworkError: Error {
    case invalidResponse
}

2. 뷰모델 구현

Combine의 @Published와 AnyCancellable을 사용하여 데이터를 구독하고 관리하는 뷰모델을 작성합니다.

import SwiftUI
import Combine

@MainActor
final class MarketsViewModel: ObservableObject {
    // @Published 속성을 사용하여 UI에 실시간 데이터 바인딩
    @Published var markets: [Market] = [] // 마켓 데이터 저장
    @Published var isLoading: Bool = false // 로딩 상태 표시
    @Published var errorMessage: String? = nil // 에러 메시지 저장

    private var cancellables = Set<AnyCancellable>() // Combine 구독 관리

    /// Upbit 마켓 데이터를 가져오는 함수
    func fetchMarkets() {
        isLoading = true // 로딩 상태 시작
        errorMessage = nil // 이전 에러 메시지 초기화

        // Network 클래스의 requestUpbitAPI() 호출
        Network.shared.requestUpbitAPI()
            .sink(receiveCompletion: { [weak self] completion in
                guard let self = self else { return }
                self.isLoading = false // 로딩 상태 종료
                switch completion {
                case .finished:
                    break // 성공적으로 완료된 경우
                case .failure(let error):
                    self.errorMessage = error.localizedDescription // 에러 메시지 저장
                }
            }, receiveValue: { [weak self] fetchedMarkets in
                self?.markets = fetchedMarkets // 받은 데이터를 업데이트
            })
            .store(in: &cancellables) // 구독을 cancellables에 저장하여 관리
    }
}

3. SwiftUI 뷰 구현

Combine과 뷰모델을 사용하여 데이터를 표시하는 SwiftUI 뷰를 작성합니다.

struct MarketsView: View {
    @StateObject private var viewModel = MarketsViewModel() // 뷰모델 초기화 및 관찰
    
    var body: some View {
        NavigationView {
            VStack {
                // 로딩 상태일 때 ProgressView 표시
                if viewModel.isLoading {
                    ProgressView("Loading markets...")
                }
                // 에러 발생 시 에러 메시지 표시
                else if let errorMessage = viewModel.errorMessage {
                    Text("Error: \(errorMessage)")
                        .foregroundColor(.red)
                        .multilineTextAlignment(.center)
                        .padding()
                }
                // 데이터가 로드되었을 때 List로 표시
                else {
                    List(viewModel.markets, id: \.self) { market in
                        VStack(alignment: .leading) {
                            Text(market.koreanName) // 한국어 이름
                                .font(.headline)
                            Text(market.englishName) // 영어 이름
                                .font(.subheadline)
                                .foregroundColor(.secondary)
                        }
                    }
                }
            }
            .navigationTitle("Markets") // 네비게이션 타이틀 설정
            .onAppear {
                viewModel.fetchMarkets() // 뷰가 나타날 때 데이터 로드
            }
            .refreshable {
                viewModel.fetchMarkets() // 당겨서 새로고침 동작
            }
        }
    }
}

4. 주요 구성 요소 설명

4.1 Network 클래스

  • Fail와 tryMap을 사용해 에러 처리를 명시적으로 수행.
  • Combine의 decode를 사용해 JSON을 Swift 객체로 변환.
  • UI와 상호작용을 위해 receive(on:)으로 메인 스레드에서 처리.

4.2 ViewModel

  • Combine의 sink로 네트워크 결과를 처리:
    • 성공 시 데이터를 업데이트.
    • 실패 시 에러 메시지를 표시.
  • cancellables를 사용해 구독을 관리하며 메모리 누수 방지.

4.3 SwiftUI View

  • @StateObject로 뷰모델을 초기화하고 데이터 변화에 반응.
  • 상태에 따라 로딩, 에러, 데이터를 표시하는 조건 분기.
  • refreshable을 사용해 사용자 친화적인 새로고침 제공.

5. 실행 흐름

  1. MarketsView가 나타날 때 onAppear에서 fetchMarkets 호출.
  2. Network 클래스가 API 요청을 수행하고 데이터를 반환.
  3. MarketsViewModel이 Combine 구독을 통해 데이터를 업데이트하거나 에러를 처리.
  4. SwiftUI가 @Published 속성을 통해 UI를 동적으로 업데이트.

6. 결과

네트워크 통신 성공 네트워크 통신 실패 (잘못된 URL)