[iOS] @escaping completion과 Async, Await의 차이점

강치우·2024년 1월 15일
0

수입푸드

목록 보기
5/13

요즘 뭔가 면접질문에 대해 공부하다보니 자연스럽게 비동기 쪽을 자주 접하게 되는거 같다. 문법 공부를 시작한지 얼마 되지 않아서 @escaping completion을 잘 알진 못해서 지금이라도 알아야 될 것 같아 호다닥 공부해보자.

@escaping completion 클로저와 Swift 5.5부터 도입된 asyncawait는 모두 비동기 프로그래밍을 처리하는 방법이다.

그러나 이 두 접근 방식은 작동 방식과 사용의 편리성에서 몇 가지 중요한 차이점을 가지고 있다.

@escaping completion

정의

@escaping 클로저는 함수가 반환된 후에도 호출될 수 있는 클로저입니다.
비동기 작업을 완료한 후 결과를 전달하는 데 사용된다

만약 내가 매개변수로 받은 completion에 저장된 클로저를 3초 후에 실행시키고 싶음
그래서 다음과 같이 코드를 짜보면


func chunrangsung(completion: () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

이렇게 짤 경우

Escaping closure capures non-escaping parameter 'completion'

라는 오류를 뱉을거임 왜 이런에러가 뜨냐?

우리가 일반적으로 위처럼 아무런 키워드 없이 파라미터로 받는 클로저는 모두 non-escaping closure임

이름 그대로 탈출이 불가능한 클로저임 함수의 "흐름"을 탈출하지 않는 클로저란 뜻으로, 한 마디로 함수가 종료되고 나서 클로저가 실행될 수 없다는 말임

따라서non-escaping closure의 경우

함수 내부에서 직접 실행하기 위해서만 사용한다. 파라미터로 받은 클로저는 변수나 상수에 대입할 수 없고, 중첩 함수 내부에서 클로저를 사용할 경우, 중첩함수를 리턴할 수 없다.

함수의 실행 흐름을 탈출하지 않아, 함수가 종료되기 전에 무조건 실행 되어야 한다고함.

그러면 파라미터로 받은 closure는 비동기로 사용못하냐? 할 때 사용하는 키워드가 바로
@escaping임.


사용

주로 콜백 패턴에서 사용됩니다. 비동기 작업이 완료된 후에 결과를 클로저를 통해 반환한다.

아래 함수는 @escaping 키워드를 붙혀준 함수임


func chunrangsung(completion: @escaping () -> ()) {
    DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
        completion()
    }
}

이런 식으로 함수의 타입 선언하기 전에

@escaping이란 키워드를 붙여주면,

이 클로저는 함수의 실행 흐름에 상관 없이 실행되는 클로저다! 라고 알려주는 것임


문제점

콜백 중첩(콜백 지옥)으로 인해 코드가 복잡해질 수 있습니다.
에러 처리가 더 어려울 수 있으며, 콜백 내에서 발생하는 강한 참조 순환을 주의해야 한다.

@escaping completion이 나온지 꽤 된 키워드이다 보니 코드가 난잡해지고, 에러처리가 힘들뿐더러 콜백지옥에 빠질 수도 있다고 한다 ㅎㄷㄷ.. 무서운 친구임 조심해서 잘 써야함


Async/Await

정의

asyncawait는 Swift의 비동기 프로그래밍을 위한 현대적인 접근 방식입니다. 비동기 함수를 동기 함수처럼 쉽게 작성할 수 있게 해줍니다.

흠 쉽게 작성해준다고? 쉬우면 얼마나 쉽다고 비슷하겠지 함 보자고

우선 Async/Await를 사용하지 않은 예시를 한번 보자

func fetchUserData(completion: @escaping (User?, Error?) -> Void) {
    // 사용자 데이터 요청
    fetchFromAPI("userEndpoint") { userData, error in
        guard let userData = userData, error == nil else {
            completion(nil, error)
            return
        }

        // 추가적인 설정 데이터 요청
        fetchFromAPI("settingsEndpoint") { settingsData, error in
            guard let settingsData = settingsData, error == nil else {
                completion(nil, error)
                return
            }

            // 최종 사용자 객체 생성
            let user = User(data: userData, settings: settingsData)
            completion(user, nil)
        }
    }
}

// 사용 예시
fetchUserData { user, error in
    if let user = user {
        print("User fetched: \(user)")
    } else if let error = error {
        print("Error: \(error)")
    }
}

흠 이 코드에서는 여러 비동기 작업을 순차적으로 수행해야 하며, 각 단계마다 에러 처리와 결과 처리가 필요하다. 코드가 중첩되어 복잡해지고, 가독성이 떨어지는거 같다.


사용

async 키워드는 비동기 함수를 정의할 때 사용됩니다. await는 해당 비동기 함수가 결과를 반환할 때까지 기다리도록 합니다.

그럼 Async/Await를 사용한 개선된 예시를 보자

func fetchUserData() async throws -> User {
    // 사용자 데이터 요청
    let userData = try await fetchFromAPI("userEndpoint")

    // 추가적인 설정 데이터 요청
    let settingsData = try await fetchFromAPI("settingsEndpoint")

    // 최종 사용자 객체 생성
    return User(data: userData, settings: settingsData)
}

// 사용 예시
Task {
    do {
        let user = try await fetchUserData()
        print("User fetched: \(user)")
    } catch {
        print("Error: \(error)")
    }
}

중첩된 콜백이 제거되었고 각 비동기 작업을 순차적으로 명확하게 나타낼 수 있었고,
표준적인 try/catch 문을 사용하여 에러를 처리할 수 있어 에러 처리도 간편해진 코드를 볼 수 있다.

async/await를 사용하면 코드가 훨씬 간결해지고, 동기적 코드와 유사한 방식으로 비동기 작업을 처리할 수 있는거 같다.


장점

코드가 훨씬 더 읽기 쉽고 이해하기 쉬워집니다. 에러 처리가 간편해지며, 중첩된 콜백 없이 여러 비동기 작업을 순차적으로 수행할 수 있습니다.

async/await가 비동기 프로그래밍을 얼마나 간소화하고 개선하는지 알 수 있는거 같았다. 특히 여러 비동기 작업이 연속적으로 필요한 경우, async/await의 장점이 더욱 두드러지게 되는거 같았다.


정리하며

이렇게 오늘은 정리하면서@escaping completionasync/await의 각각 사용법과 예시, 각각의 차이점을 조금 정리해봤는데 확실히 async/await를 사용한 경우의 코드가 간결해지는것도 볼 수 있었고, 간편해진 모습을 볼 수 있었다.

하지만 아직 현업에서는 레거시 코드들이 많이 있을거고, 레거시 코드엔 분명히 @escaping completion로 만들어진 함수도 많이 남아있을 것이 분명하다. 그래서 async/await가 편하다고 해서 이것만 사용하는 것 보다 @escaping completion를 한번 더 사용해 보는 것도 나쁘지 않겠다는 생각이 든다. 그럼 20000

profile
자허블을 좀 더 좋아하긴 합니다.

0개의 댓글