현업에서 Swift Combine sink 사용 시 주의사항과 팁

작성일 :

현업에서 Swift Combine sink 사용 시 주의사항과 팁

Swift Combine은 애플의 리액티브 프로그래밍 프레임워크로, 비동기 이벤트 처리와 데이터 스트림 관리에 탁월한 성능을 발휘합니다. Combine의 핵심 요소 중 하나인 sink는 Publisher로부터 전달된 데이터를 처리하는 Subscriber를 생성하는 데 사용됩니다. 현업에서 Combine을 활용할 때는 다양한 주의사항과 팁이 필요합니다. 이 글에서는 sink를 사용할 때 주의할 점과 유용한 팁들을 살펴보겠습니다.

1. 메모리 관리

주의사항

Combine을 사용할 때 가장 중요한 점 중 하나는 메모리 관리입니다. sinkAnyCancellable 객체를 반환하며, 이를 통해 구독을 관리합니다. AnyCancellable을 적절히 저장하지 않으면 구독이 즉시 취소되어 예상치 못한 동작이 발생할 수 있습니다.

AnyCancellable을 클래스의 프로퍼티로 저장하여 구독을 유지하는 것이 좋습니다. 예를 들어, ViewController에서 Combine 구독을 설정할 때는 다음과 같이 할 수 있습니다:

swift
import Combine

class MyViewController: UIViewController {
    private var cancellables = Set<AnyCancellable>()

    override func viewDidLoad() {
        super.viewDidLoad()

        let publisher = Just("Hello, Combine!")
        publisher
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Finished")
                case .failure(let error):
                    print("Error: \(error)")
                }
            }, receiveValue: { value in
                print("Received value: \(value)")
            })
            .store(in: &cancellables)
    }
}

이 예제에서는 cancellables라는 Set<AnyCancellable>에 구독을 저장하여, ViewController가 해제될 때 자동으로 구독이 취소되도록 합니다.

2. 메인 스레드에서 UI 업데이트

주의사항

비동기 작업을 수행한 후 UI를 업데이트할 때는 항상 메인 스레드에서 수행해야 합니다. Combine은 기본적으로 백그라운드 스레드에서 작업을 수행하므로, UI 업데이트 시 메인 스레드로 전환해야 합니다.

receive(on:) 연산자를 사용하여 메인 스레드에서 데이터를 수신하도록 설정할 수 있습니다.

swift
import Combine

let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
    .map { $0.data }
    .decode(type: Todo.self, decoder: JSONDecoder())

publisher
    .receive(on: DispatchQueue.main)
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    }, receiveValue: { todo in
        // UI 업데이트는 메인 스레드에서 안전하게 수행
        print("Received todo: \(todo)")
    })
    .store(in: &cancellables)

struct Todo: Codable {
    let id: Int
    let title: String
    let completed: Bool
}

이 예제에서는 receive(on:)을 사용하여 메인 스레드에서 데이터를 수신하고, 안전하게 UI를 업데이트합니다.

3. 오류 처리

주의사항

비동기 작업에서는 예외 상황을 처리하는 것이 매우 중요합니다. Combine에서 발생하는 오류를 적절히 처리하지 않으면 앱이 예기치 않게 종료되거나, 오류 원인을 파악하기 어려울 수 있습니다.

Combine의 sinkreceiveCompletion 클로저를 통해 오류를 처리할 수 있습니다. 또한, catch 연산자를 사용하여 오류가 발생했을 때 대체 Publisher를 제공할 수 있습니다.

swift
import Combine

let url = URL(string: "https://jsonplaceholder.typicode.com/todos/1")!
let publisher = URLSession.shared.dataTaskPublisher(for: url)
    .tryMap { data, response -> Data in
        guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else {
            throw URLError(.badServerResponse)
        }
        return data
    }
    .decode(type: Todo.self, decoder: JSONDecoder())
    .catch { error in
        Just(Todo(id: 0, title: "Fallback todo", completed: false))
    }

publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    }, receiveValue: { todo in
        print("Received todo: \(todo)")
    })
    .store(in: &cancellables)

struct Todo: Codable {
    let id: Int
    let title: String
    let completed: Bool
}

이 예제에서는 catch 연산자를 사용하여 오류가 발생했을 때 대체 값을 제공하고, sink에서 오류를 처리합니다.

4. 구독 취소

주의사항

비동기 작업이 완료되거나 ViewController가 해제될 때 구독을 적절히 취소하지 않으면 메모리 누수가 발생할 수 있습니다.

AnyCancellableSet<AnyCancellable>에 저장하여, 필요한 시점에 구독을 취소할 수 있습니다. 또한, 구독을 명시적으로 취소할 수도 있습니다.

swift
import Combine

class MyViewModel {
    private var cancellables = Set<AnyCancellable>()

    func fetchData() {
        let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
            .map { $0.data }
            .decode(type: Todo.self, decoder: JSONDecoder())

        publisher
            .sink(receiveCompletion: { completion in
                switch completion {
                case .finished:
                    print("Finished")
                case .failure(let error):
                    print("Error: \(error)")
                }
            }, receiveValue: { todo in
                print("Received todo: \(todo)")
            })
            .store(in: &cancellables)
    }

    func cancelSubscriptions() {
        cancellables.forEach { $0.cancel() }
        cancellables.removeAll()
    }
}

struct Todo: Codable {
    let id: Int
    let title: String
    let completed: Bool
}

이 예제에서는 cancellables에 구독을 저장하고, cancelSubscriptions 메서드를 통해 구독을 명시적으로 취소합니다.

5. 스케줄링

주의사항

비동기 작업을 수행할 때, 적절한 스케줄링이 필요합니다. 스케줄링을 제대로 하지 않으면 UI가 응답하지 않거나, 불필요한 백그라운드 작업이 발생할 수 있습니다.

subscribe(on:)receive(on:) 연산자를 사용하여 적절한 스케줄링을 설정할 수 있습니다.

swift
import Combine

let publisher = URLSession.shared.dataTaskPublisher(for: URL(string: "https://jsonplaceholder.typicode.com/todos/1")!)
    .map { $0.data }
    .decode(type: Todo.self, decoder: JSONDecoder())
    .subscribe(on: DispatchQueue.global(qos: .background))
    .receive(on: DispatchQueue.main)

publisher
    .sink(receiveCompletion: { completion in
        switch completion {
        case .finished:
            print("Finished")
        case .failure(let error):
            print("Error: \(error)")
        }
    }, receiveValue: { todo in
        print("Received todo: \(todo)")
    })
    .store(in: &cancellables)

struct Todo: Codable {
    let id: Int
    let title: String
    let completed: Bool
}

이 예제에서는 subscribe(on:)을 사용하여 백그라운드 스레드에서 작업을 수행하고, receive(on:)을 사용하여 메인 스레드에서 결과를 처리합니다.

결론

Combine의 sink는 비동기 이벤트 처리에서 중요한 역할을 합니다. 현업에서 sink를 사용할 때는 메모리 관리, 메인 스레드에서의 UI 업데이트, 오류 처리, 구독 취소, 적절한 스케줄링 등 다양한 요소를 고려해야 합니다. 이러한 주의사항과 팁을 염두에 두고 Combine을 사용하면, 보다 안정적이고 효율적인 비동기 로직을 구현할 수 있습니다. Combine과 sink의 강력한 기능을 활용하여 복잡한 비동기 프로그래밍 문제를 해결하고, 더욱 직관적인 코드를 작성해 보세요.