Swift 로컬 계산을 디바운스하려면 입력이 바뀔 때마다 이전 지연 작업을 취소한 다음, 잠시 멈춘 뒤 계산하면 됩니다. 이 macOS 튜토리얼에서는 최신 유효 입력이 안정된 뒤에만 업데이트되는 합성 사각형 타일 계산기를 만듭니다.
계산기가 정확히 무엇을 해야 하는지 정의하기
독자가 할 일은 간단합니다. Mac 앱의 숫자 입력란에 지연된 미리보기를 추가합니다. 정사각형 층으로 이루어진 그래픽을 떠올려 보세요. 첫 번째 층에는 타일 하나, 두 번째 층에는 네 개, 세 번째 층에는 아홉 개가 있습니다. 층 수를 입력하면 모든 층의 타일 수를 계산합니다. 모든 값은 메모리에만 저장되며 합성 숫자를 사용합니다.
이 예제의 입력 규칙은 다음과 같습니다.
0부터1000까지의 10진 정수를 허용합니다. 양 끝값도 포함됩니다.- 파싱하기 전에 앞뒤 공백을 제거합니다.
- 입력이 비어 있으면 안내 문구를 즉시 표시하고, 유효하지 않으면 오류를 표시합니다.
- 입력이 유효하면 이전 결과를 지우고
300밀리초 동안 기다린 뒤 계산합니다. - 다른 편집이 발생하거나 뷰가 사라지면 대기 중인 계산을 취소합니다.
결과는 1부터 요청한 층 수까지 각 수의 제곱을 더한 값입니다. 공식은 n * (n + 1) * (2 * n + 1) / 6입니다. 층이 3개일 때의 예시 계산은 1 + 4 + 9 = 14입니다. 0의 결과는 0입니다. 이는 수학적 예시이지, 앱에서 측정한 결과가 아닙니다.
300밀리초 간격은 상호작용을 위한 예시 선택입니다. 이는 후행 디바운스를 만듭니다. 새로 편집할 때마다 대기 시간이 다시 시작됩니다. 계속 편집하면 계산이 무기한 미뤄질 수 있습니다. 입력이 안정된 뒤의 미리보기가 유용할 때 이 동작을 선택하세요. 모든 개별 편집에 응답해야 하는 명령에는 다른 규칙이 필요합니다.
취소 시 지연 경로를 중단해야 하는 이유
작업 핸들을 사용하면 이전에 예약한 작업을 취소할 수 있습니다. Swift 작업은 생성 직후 실행을 시작할 수 있으며, 핸들을 버려도 작업은 중단되지 않습니다. 다음 편집에서 취소할 수 있도록 핸들을 저장하세요. Apple의 Task 문서에는 두 동작이 모두 설명되어 있습니다.
지연에는 try await Task.sleep(for: .milliseconds(300))를 사용합니다. Apple 문서에 따르면 이는 기반 스레드를 차단하지 않고 작업을 일시 중단하며, 지연이 끝나기 전에 취소되면 CancellationError를 던집니다. duration 오버로드는 macOS 13 이상에서 사용할 수 있습니다. Task.sleep 문서
취소는 여전히 협력적으로 이루어집니다. cancel()를 호출해도 임의의 계산 코드가 강제로 중단되지는 않습니다. 지연 경로는 sleep에서 오류가 발생하면 반환해야 하며, 계산 전에 취소 여부를 다시 확인해야 합니다. Task.cancel 문서, Task.checkCancellation 문서
흔한 실수는 sleep에 try?를 사용한 뒤 계산을 바로 이어가는 것입니다. 그러면 던져진 취소 오류가 무시되고 다음 문장이 실행됩니다. 이 예제에서는 오류를 잡으면 계산이 오래된 결과를 만들기 전에 작업을 종료합니다.
완전한 macOS 예제 만들기
Xcode에서 인터페이스로 SwiftUI, 언어로 Swift를 선택해 macOS 앱 프로젝트를 만듭니다. Swift 6 툴체인을 사용하고 배포 대상을 macOS 14 이상으로 설정합니다. 이 예제는 최신 인자 없는 onChange(of:initial:_:) 오버로드를 사용하며, Apple은 이를 macOS 14부터 사용할 수 있다고 명시합니다. SwiftUI onChange 문서
생성된 앱 진입점 파일을 아래의 완전한 프로그램으로 바꿉니다. 생성된 ContentView.swift 파일을 타깃에서 제거하거나 내용을 바꿔 ContentView 선언이 하나만 남도록 합니다. 타깃에 @main으로 표시된 앱 선언은 정확히 하나만 유지합니다.
import SwiftUI
import Combine
import Foundation
@MainActor
final class TileCalculator: ObservableObject {
@Published private(set) var result: Int?
@Published private(set) var status = "Enter a layer count."
private var pending: Task<Void, Never>?
func schedule(_ text: String) {
pending?.cancel()
pending = nil
result = nil
let cleaned = text.trimmingCharacters(in: .whitespacesAndNewlines)
guard !cleaned.isEmpty else {
status = "Enter a layer count."
return
}
guard let layers = Int(cleaned, radix: 10),
(0...1000).contains(layers) else {
status = "Use a whole number from 0 to 1000."
return
}
status = "Waiting for typing to pause..."
pending = Task { @MainActor [weak self] in
do {
try await Task.sleep(for: .milliseconds(300))
try Task.checkCancellation()
} catch {
return
}
guard let self else { return }
let total = layers * (layers + 1) * (2 * layers + 1) / 6
self.result = total
self.status = "Calculated for \(layers) layers."
self.pending = nil
}
}
func stop() {
pending?.cancel()
pending = nil
result = nil
status = "Calculation paused."
}
}
@MainActor
struct ContentView: View {
@State private var input = "3"
@StateObject private var calculator = TileCalculator()
var body: some View {
VStack(alignment: .leading, spacing: 12) {
Text("Square-layer tile counter")
.font(.headline)
TextField("Layers: 0 to 1000", text: $input)
.textFieldStyle(.roundedBorder)
Text(calculator.status)
if let total = calculator.result {
Text("Total tiles: \(total)")
.font(.title2)
}
}
.padding(24)
.frame(minWidth: 360, minHeight: 180)
.onAppear {
calculator.schedule(input)
}
.onChange(of: input) {
calculator.schedule(input)
}
.onDisappear {
calculator.stop()
}
}
}
@main
@MainActor
struct LocalTileCalculatorApp: App {
var body: some Scene {
WindowGroup {
ContentView()
}
}
}
뷰는 @StateObject로 계산기 객체를 소유합니다. Apple 문서에 따르면 이 방식은 컨테이너 입력이 바뀌어도 모델 인스턴스를 유지하며, 뷰의 정체성이 바뀌면 새 인스턴스를 만듭니다. 게시된 모델 속성이 표시할 상태와 결과를 갱신합니다. StateObject 문서
모델과 작업 클로저에는 @MainActor가 명시적으로 표시되어 있습니다. Apple은 이 액터의 실행기가 메인 디스패치 큐와 동등하다고 정의합니다. 따라서 이 구현에서는 예약, 취소, 결과 할당이 같은 액터에서 이루어집니다. sleep은 작업을 일시 중단하고, 대기 후 짧은 산술식이 해당 액터에서 실행됩니다. MainActor 문서
입력과 결과 규칙이 오래된 미리보기를 막는 방식
유효성 검사 전에 취소합니다. 사용자가 유효한 숫자를 지우거나 유효하지 않은 값으로 바꿔도 이전에 대기 중인 계산은 취소됩니다. 유효성 검사에서 일찍 반환해도 이전 지연 작업이 계속 실행되어 화면에 결과를 표시하는 상황이 생기지 않습니다.
프로그램은 편집할 때마다 result을 지웁니다. 이는 의도적인 표시 규칙입니다. 새 값이 대기하는 동안 결과가 사라지므로, 현재 텍스트의 답처럼 이전 합계를 보여 주지 않습니다. 이전 결과라는 명확한 표시와 함께 결과를 유지하는 대안도 가능하지만, 그러려면 표시 규칙을 다르게 정해야 합니다.
Foundation의 trimmingCharacters(in:)은 문자열 양 끝에서 일치하는 문자를 제거합니다. .whitespacesAndNewlines를 사용하면 3 같은 입력이 3가 됩니다. 내부 문자는 그대로 유지됩니다. Foundation 문자열 양끝 공백 제거 문서
Swift 정수 파서는 기본적으로 진법 10을 사용하고, 앞에 부호가 붙는 것을 허용하며, 유효하지 않거나 표현할 수 없는 값에는 nil를 반환합니다. 따라서 여기서는 +3과 003을 허용하고, 3.5, 1,000, 3 0은 거부합니다. 파싱된 음수는 별도의 범위 검사에서 거부됩니다. Int.init 문서
상한은 산술 연산도 제한합니다. 층이 1000개일 때 분자는 2,003,001,000이고 합계는 333,833,500입니다. 허용된 모든 입력은 이 비음수 중간값의 범위 안에 있습니다. 한도를 높이려면 최종 결과뿐 아니라 중간 곱셈도 검사해야 합니다. 이미 발생한 오버플로는 나중에 6으로 나눈다고 복구되지 않습니다.
각 지연 작업은 해당 편집에서 검증한 layers 값을 캡처합니다. 대기한 뒤 입력란을 다시 읽지 않습니다. 작업이 취소 확인을 통과하면 결과를 할당하기 전까지 추가 await 지점이 없습니다. 다른 메인 액터 예약 호출은 이 동기 계산 및 할당 구간에 끼어들 수 없습니다. Swift 언어 가이드에서는 액터 격리와 명시적으로 표시된 일시 중단 지점을 설명합니다. 동시성 — Swift 프로그래밍 언어
이 설명은 계산을 동기적이고 매우 짧게 유지하는 데 달려 있습니다. 나중에 비동기 작업을 추가한다면, 해당 작업이 반환된 뒤 취소 여부와 요청이 여전히 최신인지 다시 확인하세요. 버전 식별자를 사용하면 사용자가 이전 입력값으로 돌아오는 경우를 포함해 연속된 요청을 구분할 수 있습니다.
합성 입력으로 동작 확인하기
앱을 빌드하고 실행한 뒤 다음 순서로 확인합니다. 예상 결과는 프로그램의 규칙에 따른 것이며, 정확한 실행 시점은 스케줄링에 따라 달라집니다.
| 동작 | 예상 동작 |
|---|---|
초기 입력 3로 창 열기 |
대기 상태를 표시한 뒤 Total tiles: 14를 표시합니다. |
입력을 4으로 바꾸기 |
이전 합계를 지운 뒤 Total tiles: 30을 표시합니다. |
1를 입력한 다음 첫 지연이 끝나기 전에 10 입력하기 |
지연을 다시 시작하고, 안정된 값으로 Total tiles: 385를 표시합니다. |
4을 입력한 다음 지연이 끝나기 전에 지우기 |
Enter a layer count.를 표시하고 합계는 표시하지 않습니다. |
3.5 또는 1001 입력하기 |
범위 안내를 즉시 표시하고 합계는 표시하지 않습니다. |
0 입력하기 |
지연 후 Total tiles: 0을 표시합니다. |
1000 입력하기 |
지연 후 Total tiles: 333833500을 표시합니다. |
취소를 확인할 때는 지연이 끝나기 충분히 전에 다음 편집을 하세요. 경계 시점에는 다음 편집이 처리되기 전에 이전 결과가 이미 완료되었을 수 있습니다. 그래도 다음 편집은 해당 결과를 지우고 새 요청을 시작합니다. 지연은 정확한 실행 시각을 보장하지 않습니다. Swift 언어 가이드에서는 sleep이 요청한 시간 이상 작업을 일시 중단한다고 설명합니다. Swift 동시성 문서
위 산술 예시는 정수 계산으로 별도 확인했습니다. 조사 환경에서 Swift 컴파일러를 사용할 수 없어 macOS 프로그램은 검토만 했으며, 컴파일하거나 실행하지는 않았습니다.
취소를 뷰의 수명과 연결하기
사라짐 처리기는 stop()를 명시적으로 호출합니다. Apple 문서에 따르면 onDisappear은 뷰가 사라진 뒤 실행되며, 정확한 시점은 뷰 유형에 따라 다릅니다. 이를 정확한 창 이벤트 시한이 아니라 뷰 정리 지점으로 취급하세요. 나타남 처리기는 뷰가 나타날 때마다 현재 입력으로 작업을 다시 예약합니다. SwiftUI onDisappear 문서
취소를 잡는 부분은 의도적으로 공유 상태를 바꾸거나 저장된 핸들을 지우지 않고 반환합니다. 이전에 취소된 작업이 새 작업이 저장된 뒤 재개될 수 있습니다. 이전 작업이 무조건 정리하도록 두면 새 핸들이 지워질 수 있습니다. 이 프로그램에서는 예약 및 중단 메서드가 취소된 핸들을 지우고, 성공적으로 완료된 작업만 자신의 활성 핸들을 지웁니다.
디바운싱은 작업이 시작되는 시점을 제어합니다. 긴 동기 계산을 응답성 있게 만들지는 않으며, 이미 할당된 결과를 취소로 되돌릴 수도 없습니다. 범위가 제한된 이 공식에는 간결한 패턴이 유용합니다. 이전 핸들을 취소하고, 캡처한 입력을 검증한 뒤, 대기 시간 동안 일시 중단하고, 취소되면 종료한 다음 메인 액터에서 짧은 계산 결과를 반영합니다. 이렇게 하면 Mac 인터페이스에서 입력이 안정된 뒤의 미리보기를 예측 가능하게 제공할 수 있습니다.
출처
- Task — Apple.
- sleep(for:tolerance:clock:) — Apple.
- cancel() — Apple.
- checkCancellation() — Apple.
- onChange(of:initial:_:) — Apple.
- StateObject — Apple.
- MainActor — Apple.
- trimmingCharacters(in:) — Apple.
- init(_:radix:) — Apple.
- Concurrency — The Swift Programming Language — Swift.org.
- onDisappear(perform:) — Apple.