Search

Swift의 클로저에 대해 알아보기

목차

3.

클로저란?

클로저는 코드의 일부분을 캡슐화하여 나중에 호출할 수 있는 기능입니다. 흔히 익명 함수나 lamda로 알려져 있으며 함수와 매우 유사하지만 더 간결한 형태로 사용할 수 있습니다. 예를 들어, 배열의 정렬이나 비동기 작업 등에 자주 사용됩니다.

배열 정렬 예시 코드

let numbers = [5, 3, 8, 1, 4] let sortedNumbers = numbers.sorted(by: { (a: Int, b: Int) -> Bool in return a < b }) print(sortedNumbers) // [1, 3, 4, 5, 8]
Swift
복사
TMI: 위 코드보다 더 간결하게 작성하는 두 가지 방법

비동기 작업 예시 코드

func fetchData(completion: @escaping (String) -> Void) { let data = "데이터 로드 완료" completion(data) } fetchData { result in print(result) } // 데이터 로드 완료
Swift
복사

캡처란?

캡처를 번역하면 ‘포획’이라는 뜻인데 클로저의 캡처란 클로저 내부에서 사용되는 외부 변수나 상수를 클로저가 포획하는 것을 의미합니다. 포획된 변수는 클로저가 생성된 스코프를 벗어난 이후에도 메모리에 계속 유지됩니다.

캡처 예시 코드

func add(_ value: Int) -> () -> Int { var sum = 0 return { sum += value return sum } } let result = add(5) print(result()) // 5 print(result()) // 10
Swift
복사
클로저 캡처 현상이 관리되지 않으면 메모리 누수와 같은 문제가 발생할 수 있습니다. 유튜브 앱을 예시로 들어보겠습니다.
유저가 자신의 구독 채널 목록을 확인하기 위해 해당 탭으로 이동하면 앱은 서버에 요청을 보내 구독한 채널 데이터를 불러오고 화면에 표시합니다.
class SubscriptionViewController: UIViewController { var subscriptionList: [String] = [] func fetchSubscriptionList() { NetworkManager.fetchSubscriptions { subscriptions in self.subscriptionList = subscriptions self.updateUI() } } func updateUI() { // UI 업데이트 코드 } }
Swift
복사
이 과정에서 비동기 네트워크 요청을 수행하고 요청이 완료되었을 때 클로저를 통해 결과를 반환합니다. 여기서 만약 클로저와 클래스 또는 구조체의 인스턴스를 강하게 캡처하게 되면 서로 강한 참조 사이클(strong reference cycle)이 발생하여 유저가 해당 화면을 벗어난 이후에도 메모리에서 해제되지 않는 문제가 발생합니다. 왜냐하면 비동기 작업은 함수가 종료된 이후에도 클로저가 호출될 가능성이 있기 때문입니다.
스파이더맨이 self라고 생각하면 됨
따라서 메모리 누수가 생기고 메모리 누수가 누적되면 앱의 메모리 사용량이 증가하여 결국 앱이 느려지거나 심한 경우에는 강제 종료될 수 있습니다.
이를 해결하기 위해 [weak self]를 사용하여 self약한 참조로 캡처하도록 합니다. [weak self]를 사용하면 클로저 내부에서 self를 사용할 때 강한 참조가 일어나지 않으며 self가 메모리에서 해제되면 self는 ARC(Automatic Reference Counting)를 통해 자동으로 nil이 됩니다.
NetworkManager.fetchSubscriptions { [weak self] subscriptions in self?.subscriptionList = subscriptions self?.updateUI() }
Swift
복사
그러면 유저가 화면을 떠난 후에도 안전하게 인스턴스가 메모리에서 해제됩니다.
TMI: guard let self = self else { return }

클로저의 메모리 할당

클로저는 크게 두 가지 메모리 영역에서 작동합니다.

다음 중 어느 영역에서 작동될까요?

영역
설명
특징
코드
• 프로그램의 실행 코드가 저장되는 공간 • 함수나 메서드, 명령어 등의 바이너리 코드 저장
• 읽기 전용으로 되어 있어 코드가 변경되지 않게 보호 • 실행되는 동안 프로그램의 명령어가 이 영역에 위치
데이터
• 전역 변수와 정적 변수가 저장되는 공간 • 이 변수들은 프로그램의 실행 내내 메모리에 존재
• 초기화된 데이터(let a = 10)와 초기화되지 않은 데이터(let b: Int?)가 서로 다른 구역에 저장 • 프로그램이 종료될 때까지 이 변수들은 메모리에 유지
• 동적으로 할당된 메모리를 저장하는 공간 • 주로 런타임에 메모리를 할당하고 해제하는 용도로 사용
• 개발자가 직접 메모리 할당 및 해제를 관리해야 하거나 ARC가 힙 메모리를 관리하여 직접 해제하지 않도록 도와줌 • 메모리가 크기가 크고 할당과 해제의 시점이 명확하지 않아 스택보다 느림
스택
• 함수 호출과 관련된 지역 변수나 함수의 매개변수, 반환 주소 등을 저장하는 공간
• LIFO(Last In First Out) 방식으로 데이터가 관리됨. 즉, 가장 마지막에 들어간 데이터가 가장 먼저 나옴 • 자동으로 메모리를 할당하고 해제하므로 빠르고 효율적임 • 함수가 호출될 때마다 스택 프레임이 생성되고 함수가 끝나면 해당 프레임이 해제됨
클로저가 Escaping인지 Non-Escaping인지에 따라 메모리 영역이 달라집니다.

Escaping 클로저와 Non-Escaping 클로저 차이점

Escaping 클로저는 함수가 끝난 후에도 호출될 가능성이 있는 클로저입니다. 따라서 함수의 실행이 종료된 후에도 클로저가 여전히 존재해야 하기 때문에 힙 메모리에 저장되어야 합니다. 힙 메모리는 함수 호출의 생명주기를 벗어난 데이터나 객체를 보관하는 데 사용되기 때문입니다.
func fetchData(completion: @escaping () -> Void) { DispatchQueue.global().async { // 비동기 작업 수행 completion() } }
Swift
복사
반면, Non-Escaping 클로저는 함수의 실행 중에만 유효한 클로저입니다. 따라서 함수가 호출될 때 스택에 할당되어 실행되고 종료되면 스택에서 해제됩니다. 이 클로저는 함수가 끝나는 즉시 메모리에서 제거되므로 스택 영역에 저장되는 것입니다.
func fetchData(completion: () -> Void) { completion() }
Swift
복사
Escaping
힙 영역
함수가 종료된 후에도 클로저가 살아남아 나중에 호출 가능
Non-Escaping
스택 영역
함수 내부에서만 사용되고 함수 종료와 함께 사라짐

스노우, 히터

스 - 택 노우 - Non-Escaping

눈처럼 쌓였다가 사라지기 때문

히 - 터 - Escaping(’ㅌ’과 ‘E’ 생김새가 비슷..)

히터처럼 겨울에 체온을 따뜻하게 유지시켜주고 언제든 사용할 수 있기 때문