BIBI BLOG

🚀 [SwiftUI] Async/await로 Debounce 적용하기 본문

iOS/Swift

🚀 [SwiftUI] Async/await로 Debounce 적용하기

BIBI⭐️ 2025. 3. 9. 16:22
728x90

🚀 [SwiftUI] Async/await로 Debounce 적용하기 (AsyncAlgorithms 활용)

Swift의 Concurrency 기능이 도입되면서, 우리는 Combine을 덜 사용하고 async/await과 AsyncStream을 더 많이 활용하게 되었습니다.
하지만! Combine에서 제공하는 debounce, throttle 같은 기능이 async/await에서는 기본적으로 제공되지 않아요.

👉 하지만 AsyncAlgorithms 라이브러리를 사용하면 이 문제를 해결할 수 있습니다! 🎉
👉 오늘은 AsyncAlgorithms을 활용하여 SwiftUI에서 debounce를 적용하는 방법을 쉽게 설명해 드릴게요!


1️⃣ Debounce란?

Debounce(디바운스)란 사용자가 빠르게 입력할 때, 일정 시간 동안 입력이 멈추면 한 번만 실행되는 기능입니다.
예를 들어, 검색창에서 사용자가 타이핑할 때 매번 API 요청이 가는 것을 방지하고 싶다면 debounce를 사용하면 됩니다!

📌 예시:

  • 디바운스가 없을 때: "S", "Sw", "Swi", "Swif", "Swift" → 총 5번 API 호출 (비효율적)
  • 디바운스를 적용하면: "Swift" → 1번만 API 호출됨! (최적화)

2️⃣ 기본적인 SwiftUI 코드

아래와 같이 TextField(입력창)를 만들어 사용자 입력을 받는 기본 코드가 있습니다.

import SwiftUI
import AsyncAlgorithms

struct ContentView: View {
    @State private var query = ""

    var body: some View {
        TextField("검색어를 입력하세요...", text: $query)
    }
}

하지만 문제점!
현재 코드는 사용자가 글자를 입력할 때마다 API를 호출할 가능성이 있습니다.
👉 이를 해결하려면 입력을 일정 시간 기다렸다가(0.3초) API를 호출하는 debounce 기능을 추가해야 합니다!


3️⃣ AsyncAlgorithms을 활용한 Debounce 적용

Swift의 AsyncAlgorithms을 사용하면 Combine 없이도 debounce를 쉽게 구현할 수 있습니다! 🎯

📌 AsyncChannel을 사용하여 debounce 적용하기

import SwiftUI
import AsyncAlgorithms

struct ContentView: View {
    @State private var query = ""
    private let queryChannel = AsyncChannel<String>() // ✅ 입력값을 처리할 채널

    var body: some View {
        TextField("검색어를 입력하세요...", text: $query)
            .task(id: query) { // ✅ 입력값이 변경될 때마다 실행
                await queryChannel.send(query)
            }
            .task { // ✅ AsyncChannel을 debounce 처리 후 API 요청
                for await query in queryChannel.debounce(for: .seconds(0.3)) {
                    await fetchSearchResults(for: query)
                }
            }
    }

    private func fetchSearchResults(for query: String) async {
        print("검색 실행: \(query)") // ✅ 여기에 실제 API 호출 코드 추가 가능!
    }
}

이제 사용자가 글자를 입력해도 일정 시간(0.3초) 동안 입력이 없을 때만 API를 호출합니다! 🚀
AsyncChannel<String>()을 사용하여 query 값을 관리하고, debounce 기능을 적용했습니다.


4️⃣ Debounce 기능을 손쉽게 재사용하기 (View Extension 만들기)

여러 TextField에서 debounce를 쉽게 적용할 수 있도록 View 확장(extension)을 만들 수 있습니다.

📌 View Extension으로 재사용 가능하게 만들기

import SwiftUI
import AsyncAlgorithms

extension View {
    func debounce<T: Sendable & Equatable>(
        _ query: Binding<T>,
        using channel: AsyncChannel<T>,
        for duration: Duration,
        action: @Sendable @escaping (T) async -> Void
    ) -> some View {
        self
            .task {
                for await query in channel.debounce(for: duration) {
                    await action(query)
                }
            }
            .task(id: query.wrappedValue) {
                await channel.send(query.wrappedValue)
            }
    }
}

이제 View에서 debounce를 쉽게 적용할 수 있습니다!
TextField에서 debounce를 1줄로 적용 가능!


5️⃣ Debounce를 쉽게 적용한 코드

위에서 만든 debounce 확장을 사용하면 1줄로 debounce 적용 가능!

import SwiftUI
import AsyncAlgorithms

struct ContentView: View {
    @State private var query = ""
    private let queryChannel = AsyncChannel<String>() // ✅ AsyncChannel 생성

    var body: some View {
        TextField("검색어를 입력하세요...", text: $query)
            .debounce($query, using: queryChannel, for: .seconds(0.3), action: fetchSearchResults)
    }

    private func fetchSearchResults(for query: String) async {
        print("검색 실행: \(query)")
    }
}

✅ debounce($query, using: queryChannel, for: .seconds(0.3), action: fetchSearchResults)
이제 debounce를 쉽게 사용할 수 있습니다! 🎉


6️⃣ MVVM 패턴에서 Observable을 사용하여 적용하기

👉 MVVM 패턴에서는 ViewModel을 사용하여 로직을 분리하면 더 깔끔합니다!

📌 ViewModel 만들기

import Observation
import AsyncAlgorithms

@Observable
final class ViewModel {
    var query = ""
    @ObservationIgnored let queryChannel = AsyncChannel<String>()

    func fetchSearchResults(for query: String) async {
        print("검색 실행: \(query)")
    }
}
 

📌 View에서 적용하기

import SwiftUI

struct ContentView: View {
    @State private var viewModel = ViewModel() // ✅ ViewModel 사용

    var body: some View {
        TextField("검색어를 입력하세요...", text: $viewModel.query)
            .debounce($viewModel.query, using: viewModel.queryChannel, for: .seconds(0.3), action: viewModel.fetchSearchResults)
    }
}

MVVM 구조에서도 쉽게 debounce 적용 가능!
ViewModel에서 query 상태를 관리하여 코드가 더 깔끔해짐!


🎯 마무리

✅ Swift의 AsyncAlgorithms을 활용하면 Combine 없이도 debounce를 구현할 수 있습니다!
✅ AsyncChannel을 사용하여 비동기 데이터 흐름을 쉽게 관리할 수 있습니다.
View 확장을 사용하면 1줄로 debounce 적용이 가능!
MVVM 패턴에서도 쉽게 활용할 수 있습니다.

 

 

 

자료

debounce-with-async-await-asyncalgorithms-in-swiftui-and-observable-macro

728x90
Comments