VesselWheel

클로저로 만드는 타이머 매서드(feat. 비동기화) 본문

Xcode Study

클로저로 만드는 타이머 매서드(feat. 비동기화)

JasonYang 2024. 1. 4. 09:05

들어가기 앞서, 타이머 기능을 위해서는 클로저의 탈출 클로저(Esacaping closure)의 비동기 실행에 대한 이해가 필요하다. 

탈출 클로저(Esacaping closure) 란?

더보기

코드의 순차적 실행과 비동기의 실행 순서

// 순차적 실행
func sequentialExecutionExample() {
    print("Start")

    // 1. 첫 번째 작업
    for i in 1...3 {
        print("Task \(i)")
    }

    // 2. 두 번째 작업
    print("Next Task")

    // 3. 세 번째 작업
    let result = 5 + 3
    print("Result: \(result)")

    print("End")
}

sequentialExecutionExample()
/*
위의 코드는 함수 sequentialExecutionExample 내에서 순차적으로 실행됩니다.
각각의 작업은 순서대로 실행되며, 한 작업이 끝나야 다음 작업이 실행됩니다. 
이 예시에서는 
'Start', 'Task 1', 'Task 2', 'Task 3', 'Next Task', 'Result: 8', 'End'
와 같은 순서로 출력됩니다.
*/
func asynchronousExecutionExample() {
    print("Start")

    // 1. 비동기로 실행되는 작업
    DispatchQueue.global().async {
        for i in 1...3 {
            print("Async Task \(i)")
        }
    }

    // 2. 순차적으로 실행되는 작업
    print("Next Task")

    // 3. 또 다른 비동기 작업
    DispatchQueue.global().async {
        let result = 5 + 3
        print("Async Result: \(result)")
    }

    // 4. 끝 부분
    print("End")
}

asynchronousExecutionExample()

/*
위의 코드는 비동기적으로 실행되는 예시입니다. 
DispatchQueue.global().async를 사용하여 클로저가 다른 스레드에서 비동기적으로 실행됩니다. 
따라서 비동기 작업은 순차적인 흐름을 방해하지 않고 별도의 스레드에서 실행됩니다.

실행 결과는 
'Start', 'Next Task', 'End' 순서로 출력되고, 
비동기 작업은 나중에 완료되어 
'Async Task 1', 'Async Task 2', 'Async Task 3', 'Async Result: 8'와 같이 
순서는 보장되지 않는 시점에 출력됩니다. 
이는 비동기 작업이 별도의 스레드에서 동작하기 때문에, 
주 스레드의 작업과 병행적으로 실행됨을 보여줍니다.
*/
  • 이스케이핑 클로저(escaping closure)
    • 어떤 함수의 내부에 존재하는 클로저(함수)를 외부 변수에 저장하는 경우
    • 이스케이핑 클로저는 클로저가 메서드의 인자로 전달됐을 때, 메서드의 실행이 종료된 후 실행되는 클로저(비동기)
    • 이 경우 파라미터 타입 앞에 @escaping이라는 키워드를 명시해야 합니다.
      • 예를들어, 비동기로 실행되거나 completionHandler로 사용되는 클로저의 경우
    • 클로저를 메서드의 파라미터로 넣을 수 있습니다.
// 1) 외부 변수 저장
var defaultFunction: () -> () = { print("출력") }

func escapingFunc(closure: @escaping () -> ()) {
		// 클로저를 실행하는 것이 아니라  aSavedFunction 변수에 저장. 
		// 함수는 변수와 달리 기본적으로 외부 할당이 불가능
    defaultFunction = closure        
}

// 2) GCD 비동기 코드
func asyncEscaping(closure: @escaping (String) -> ()) {
    
    var name = "iOS튜터"
    
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) { //3초뒤에 실행하도록 만들기
        closure(name)
    }
}

asyncEscaping { str in
    print("name : \(str)")
}
  • @escaping 를 사용하는 클로저에서 self의 요소를 사용할 경우, self를 명시적으로 언급해야 합니다.
    • 예시 코드
var completionHandlers: [() -> Void] = []
func someFunctionWithEscapingClosure(completionHandler: @escaping () -> Void) {
    completionHandlers.append(completionHandler)
}

func someFunctionWithNonescapingClosure(closure: () -> Void) {
    closure()    // 함수 안에서 끝나는 클로저
}

class SomeClass {
    var x = 10
    func doSomething() {
        someFunctionWithEscapingClosure { self.x = 100 } // 명시적으로 self를 적어줘야 합니다.
        someFunctionWithNonescapingClosure { x = 200 }
    }
}

let instance = SomeClass()
instance.doSomething()
print(instance.x)
// Prints "200"

completionHandlers.first?()
print(instance.x)
// Prints "100"

- 해석 

주어진 코드는 escaping closure와 nonescaping closure를 사용하는 예시입니다.

1. `someFunctionWithEscapingClosure(completionHandler:)` 함수:

- `completionHandler` 매개변수는 escaping closure입니다. escaping closure는 함수를 벗어난 후에도 호출될 수 있는 closure입니다.

completionHandlers.append(completionHandler)

- `completionHandlers` 배열에 `completionHandler`를 추가합니다.

 

2. `someFunctionWithNonescapingClosure(closure:)` 함수:

- `closure` 매개변수는 nonescaping closure입니다. nonescaping closure는 함수 내에서만 호출되는 closure입니다.

- `closure()`를 호출하여 closure를 실행합니다.

 

3. `SomeClass` 클래스:

- `x`라는 속성(프로퍼티)을 가지고 있습니다.

- `doSomething()` 메서드에서 `someFunctionWithEscapingClosure`와 `someFunctionWithNonescapingClosure`를 호출합니다.

- `someFunctionWithEscapingClosure`의 클로저에서는 `self.x = 100`으로 속성 값을 변경합니다.

- `someFunctionWithNonescapingClosure`의 클로저에서는 `x = 200`으로 속성 값을 변경합니다.

 

4. 인스턴스 생성 및 실행:

- `SomeClass`의 인스턴스를 생성합니다.

- `doSomething()` 메서드를 호출하여 속성 값을 변경합니다.

- `print(instance.x)`를 호출하여 속성 값을 출력합니다. 이때 결과는 "200"이 됩니다.

왜냐하면 `someFunctionWithNonescapingClosure`의 클로저는 함수 내에서 실행되기 때문에 `x`의 값을 직접 변경할 수 있습니다.

 

completionHandlers.first?()

5. `completionHandlers` 배열의 첫 번째 클로저 실행:

- `completionHandlers` 배열의 첫 번째 클로저를 실행합니다. 이때 `self.x = 100`이 실행되어 속성 값이 변경됩니다.

- `print(instance.x)`를 호출하여 속성 값을 출력합니다. 이때 결과는 "100"이 됩니다.

escaping closure는 함수 외부에서 호출될 수 있으므로 해당 closure가 완료될 때까지 함수가 종료되지 않습니다. 이 예시에서는 `completionHandlers` 배열에 클로저를 추가하고, 배열의 첫 번째 클로저를 실행한 결과로서 `instance.x`의 값이 변경되었습니다.


클로저로 만드는 타이머 매서드

import Foundation


class KioskTimer {
    func run(completion: @escaping () -> Void) {
        DispatchQueue.global(qos: .userInitiated).async {
            let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { timer in
                completion()
            })
            RunLoop.current.add(timer, forMode: .default)
            RunLoop.current.run()
        }
    }
}

주어진 코드는 `KioskTimer`라는 클래스를 정의하고 있습니다.

이 클래스는 비동기적으로 실행되는 타이머 기능을 제공

1. `run(completion: @escaping () -> Void)` 메서드: - `run` 메서드는 클로저 형태의 `completion` 매개변수를 입력으로 받습니다. 이 클로저는 타이머가 완료될 때 실행될 코드를 담고 있습니다.

- `@escaping` 키워드는 클로저가 비동기적으로 실행될 수 있음을 나타냅니다. 즉, `run` 메서드가 리턴된 후에도 `completion` 클로저가 실행될 수 있습니다.

 

2. `DispatchQueue.global(qos: .userInitiated).async { ... }`:

- `DispatchQueue`는 Grand Central Dispatch(GCD)를 사용하여 비동기적인 작업을 관리하는 클래스입니다.

- `global(qos: .userInitiated)`는 우선순위가 높은 글로벌 디스패치 큐를 생성합니다. 이 큐에서 비동기적으로 작업이 실행될 것입니다.

- `async { ... }`는 클로저를 비동기적으로 실행하는 메서드입니다. 여기서는 타이머를 생성하고 실행하는 코드가 작성될 것입니다.

 

3. `let timer = Timer.scheduledTimer(withTimeInterval: 5.0, repeats: true, block: { timer in ... })`: - `Timer.scheduledTimer(withTimeInterval:repeats:block:)` 메서드는 주어진 시간 간격(`withTimeInterval`)마다 반복되는 타이머를 생성합니다.

- 여기서는 5.0초 간격으로 반복되는 타이머를 생성하고 있습니다.

- `block` 매개변수에는 타이머가 실행될 때 호출될 클로저가 작성될 것입니다. 클로저의 매개변수로는 타이머 자체가 전달됩니다.

 

4. `RunLoop.current.add(timer, forMode: .default)`:

- `RunLoop`은 이벤트 처리 및 스케줄링을 관리하는 객체입니다. 여기서는 현재 실행 중인 `RunLoop`에 타이머를 추가하고 있습니다.

- `timer` 객체가 `RunLoop`에서 동작하도록 설정합니다.

- `.default`는 기본 모드로 타이머를 추가하라는 의미입니다.

 

5. `RunLoop.current.run()`:

- `RunLoop`을 실행하는 메서드입니다. 이 메서드를 호출하면 `RunLoop`이 실행되고 이벤트를 처리하며, 타이머도 실행될 것입니다. 위의 코드는 비동기적으로 실행되는 타이머를 생성하고 실행하는 기능을 제공합니다.

`run` 메서드를 호출하면 5초마다 `completion` 클로저가 실행되며, 이를 반복합니다. 이 코드는 주로 백그라운드에서 동작해야 하는 타이머 기능을 구현할 때 사용될 수 있습니다.


Closure란?

더보기
  • 클로저는 이름없는 함수 즉, 코드 블록을 말합니다.
  • 클로저는 상수나 변수의 참조를 캡쳐(capture)해 저장할 수 있습니다
    • 스위프트의 클로저는 주변 환경에 있는 변수나 상수를 캡처하여 저장하고, 이를 나중에 사용할 수 있도록 합니다. 이것은 클로저가 생성될 때 클로저가 참조하는 변수 또는 상수의 값에 대한 복사본을 유지하고 저장하는 메커니즘입니다
    • 값(value) 캡처: 클로저가 변수나 상수의 값을 캡처합니다. 이때, 클로저 내부에서 캡처한 값이 변경되어도 원본 값은 변경되지 않습니다.
    • 참조(reference) 캡처: 클로저가 변수나 상수의 참조를 캡처합니다. 따라서 클로저 내에서 해당 변수나 상수를 변경하면 원본 값도 변경됩니다.
// 값 캡처
func makeIncrementer(forIncrement amount: Int) -> () -> Int {
    var total = 0
    
    // 클로저를 반환합니다.
    let incrementer: () -> Int = {
        // total 변수를 캡처하여 저장합니다.
        total += amount
        return total
    }
    
    return incrementer
}

let incrementByTen = makeIncrementer(forIncrement: 10)

print(incrementByTen()) // total = 10, 결과: 10
print(incrementByTen()) // total = 20, 결과: 20

// 참조 캡처
class SimpleClass{
    var value: Int = 10
}

func createClosure() -> (() -> Int) {
    // 참조 캡처를 사용하여 SimpleClass의 인스턴스를 캡처합니다.
	let instance = SimpleClass()

    // 클로저가 참조하는 인스턴스의 속성을 업데이트합니다.
    let closure: () -> Int = {
        instance.value *= 2
        return instance.value
    }
    
    return closure
}

// 클로저 생성
let myClosure = createClosure()

print(myClosure()) // 20
print(myClosure()) // 40

// 클로저 내부에서 참조된 인스턴스의 속성을 변경하였으므로 원본에도 영향을 줍니다.
  • 클로저를 사용하는 이유는 뭘까요? 가장 일반적으로는 기능을 저장하기 위해 사용합니다.
  • 클로저는 비동기 처리가 필요할 때 사용할 수 있는 코드 블록입니다.(반드시 비동기에만 사용하는 것은 아님)
  • 클로저는 클래스와 마찬가지로 참조 타입(reference type)입니다.

<예시>

{ (parameters) -> return type in
    // 구현 코드
}

// 함수와 클로저 비교
func pay(user: String, amount: Int) {
    // code
}

let payment = { (user: String, amount: Int) in
    // code
}
func func1(_ param: String) -> String {
    return param + "!"
}

func func2(name: String) -> String {
    return name + "***"
}

// 함수를 변수에 할당가능(변수가 함수를 가르키게 됨)
var a: (String) -> String = func1

a("안녕")

a = func2

a("hello")

// 함수(클로저)를 변수에 할당해서
let closure1 = { (param: String) -> String in         // 클로저 리터럴
    return param + "!"
}

// 사용(실행)
closure1("스티브")

<활용 예제>

/// 예시1
// 1) (클로저를 파라미터로 받는 함수)정의

func closureFunc2(closure: () -> ()) {
    print("시작")
    closure()
}

// 파라미터로 사용할 함수/클로저를 정의
func doneFunc() {          // 함수를 정의
    print("종료")
}

let doneClosure = { () -> () in      // 클로저를 정의
    print("종료")
}

// 함수를 파라미터로 넣으면서 실행 (그동안에 배운 형태로 실행한다면)
closureFunc2(closure: doneFunc)

closureFunc2(closure: doneClosure)


// 2) 함수를 실행할때 클로저 형태로 전달 (클로저를 사용하는 이유)
closureFunc2(closure: { () -> () in
    print("프린트 종료")           // 본래 정의된 함수를 실행시키면서, 클로저를 사후적으로 정의 가능
})                              // (활용도가 늘어남)

closureFunc2(closure: { () -> () in
    print("프린트 종료 - 1")
    print("프린트 종료 - 2")
    
})

/// 예시2
// 1) (클로저를 파라미터로 받는 함수)정의
func closureCaseFunction(a: Int, b: Int, closure: (Int) -> Void) {
    let c = a + b
    closure(c)
}

// 2) 함수를 실행할 때 (클로저 형태로 전달)
closureCaseFunction(a: 1, b: 2, closure: { (n) in    // 사후적 정의
    print("plus : \(n)")
})

closureCaseFunction(a: 1, b: 2) {(number) in      // 사후적 정의
    print("result : \(number)")
}

closureCaseFunction(a: 4, b: 3) { (number) in      // 사후적 정의
    print("value : \(number)")
}

/*
 파라미터 생략 등 간소화 문법
 */

// 함수의 정의

func performClosure(param: (String) -> Int) {
    param("Swift")
}

// 문법을 최적화하는 과정
// 1) 타입 추론(Type Inference)
performClosure(param: { (str: String) in
    return str.count
})

performClosure(param: { str in
    return str.count
})

// 2) 한줄인 경우, 리턴을 안 적어도 됨(Implicit Return)
performClosure(param: { str in
    str.count
})

// 3) 아규먼트 이름을 축약(Shorthand Argements)
performClosure(param: {
    $0.count
})

// 4) 트레일링 클로저
performClosure(param: {
    $0.count
})

performClosure() {
    $0.count
}

performClosure { $0.count }

let closureType1 = { (param) in
    return param % 2 == 0
}

let closureType2 = { $0 % 2 == 0 }

// 축약 형태로의 활용
let closureType3 = { (a: Int, b:Int) -> Int in
    return a * b
}

let closureType4: (Int, Int) -> Int = { (a, b) in
    return a * b
}

let closureType5: (Int, Int) -> Int = { $0 * $1 }

 

https://bbiguduk.gitbook.io/swift/language-guide-1/closures