iOS 개발/Swift

[Swift] 클로저 (Closures)

꽁치대디 2022. 4. 22. 23:28

Closure 란?

  • 사용자의 코드 안에서 전달되어 사용할 수 있는 로직을 가진 코드 블럭
  • 1급 객체의 역할을 할 수 있음
    • 1급 객체란 전달 인자로 보낼 수 있고, 변수/상수 등으로 저장하거나 전달할 수 있으며 함수의 리턴값이 될 수도 있음
  • 함수 또한 클로저의 한 형태로, 이름이 있는 클로저
  • 참조 타입

Closure 의 형태

  • 전역 함수는 이름을 가지고 어떠한 값도 캡쳐하지 않는 클로저
  • 중첩 함수는 이름을 가지고 둘러싼 함수로부터 값을 캡쳐할 수 있는 클로저
  • 클로저 표현식은 주변 컨텍스트에서 값을 캡쳐할 수 있는 경량 구문으로 작성된 이름이 없는 클로저

Closure Expressions

클로저 표현식 (Closure Expressions) 은 간단하고 집중적인 구문으로 인라인 클로저를 작성하는 방법입니다.

 

클로저 표현식은 명확성이나 의도를 잃지 않고 짧은 형태로 클로저를 작성하기 위한 몇가지 구문 최적화를 제공합니다.

정렬 메소드

sorted(by:) 메소드를 예로 들어보겠습니다. 아래 예제의 클로저 표현식은 알파벳 역순으로 String 타입 배열을 정렬하는 경우입니다.

 

sorted(by:) 메소드는 배열 아이템과 같은 타입의 두 인자를 사용하는 클로저를 허용하고 값이 정렬된 후 첫번째 값이 두번째 값의 앞 또는 뒤에 표시되어야 하는지 여부를 나타내는 Bool 값을 반환합니다.

 

첫번째 값이 두번째 값 앞에 나타나는 경우 true 를 반환하고 반대의 경우에는 false 를 반환합니다.

 

 

backward 메소드의 인자 s1 은 첫번째 문자열을, s2 는 두번째 문자열을 가지고 있습니다.

 

s1 > s2 가 만족된다면 s1 이 사전순으로 보았을때 s2 보다 뒤에 있다는 뜻이고, true 를 반환하게 됩니다.

클로저 표현구

클로저 표현구는 일반적으로 다음과 같은 형태를 띕니다.

 

 

sorted(by:) 의 예제처럼 클로저를 메소드 형태로 선언하여 사용하는 방법도 있지만, 위의 형태와 같이 사용할 수도 있습니다.

 

 

이 경우 함수 바디가 매우 간결하기 때문에 한줄로 표현하는 것도 가능합니다.

 

 

컨텍스트로 타입 추론 (Inferring Type from Context)

sorted(by:) 메소드는 문자열 배열에서 호출되기 때문에 인자는 (String, String) -> Bool 타입의 함수여야 합니다. 그렇기 때문에 이를 클로저 표현식 정의에 명시할 필요가 없고, 이를 생략할 수 있습니다.

 

또한 단일 표현 클로저같은 경우 여기서 return 까지 생략하여 단일 표현식을 이용해 암시적으로 값을 반환할 수 있습니다.

 

 

짧은 인자 이름 & 연산자 메소드

인라인 클로저에 $0 $1 $2 등 클로저의 인자값으로 참조하는데 사용할 수 있는 짧은 인자 이름을 사용할 수 있습니다.

 

이를 이용하면 선언에 인자 목록을 생략할 수 있고 짧은 인자의 개수와 타입은 함수 타입에서 추론됩니다. 또한 클로저 표현식이 바디만으로 구성되기 때문에 in 키워드도 생략 가능합니다.

 

이보다 짧게 표현하는 방식은 연산자를 이용하는 것입니다. 하지만 저는 이런 방법이 헷갈릴수 있다고 생각하기 때문에 짧은 인자 이름 방식을 선호합니다.

 

 

Trailing Closures

함수의 마지막 인자로 클로저 표현식을 전달해야하고 클로저 표현식이 긴 경우 후행 클로저 (Trailing Closure) 로 작성하는 것이 유용할 수 있습니다.

 

후행 클로저는 함수의 인자이지만 함수의 소괄호 부분이 끝난 후 작성하며, 인자 라벨을 작성하지 않아도 됩니다.

 

 

클로저가 함수의 유일한 인자일 경우 함수를 호출할 때 함수명 뒤에 소괄호를 작성하지 않아도 됩니다.

 

저같은 경우는 코딩테스트 문제를 풀며 Array 내부 값을 다루는 경우 map 함수를 사용할때 자주 사용하곤 합니다.

 

 

함수가 여러개의 클로저를 인자로 가진다면 첫번째 후행 클로저의 인자 라벨을 생략하고 남은 후행 클로저의 라벨은 표기합니다.

 

 

이런 방식으로 함수를 작성하면 요청이 성공하거나 실패할때의 경우를 모두 고려하여 코드를 명확하게 분리할 수 있습니다.

Capturing Values

클로저는 정의된 컨텍스트에서 상수와 변수를 캡쳐할 수 있습니다.

 

그러면 클로저는 상수와 변수를 정의한 원래 범위가 더 이상 존재하지 않더라도 바디 내에서 해당 상수와 변수의 값을 참조하고 수정할 수 있습니다.

 

중첩함수는 바깥함수의 어떠한 인자도 캡쳐할 수 있고, 바깥 함수 내 정의된 상수와 변수를 캡쳐할수도 있습니다.

 

 

위 함수에서 중첩된 incrementer 함수는 둘러싸인 컨텍스트의 runningTotalamount 2개의 값을 캡쳐합니다.

 

incrementer 함수는 값을 캡쳐한 후 호출될 때마다 2개의 변수값을 증가시키는 클로저로 makeIncrementer 에 의해 반환됩니다.

 

increment 함수는 파라미터가 없으며 외부함수의 runningTotal 과 amount 에 대한 reference 를 캡쳐하고 이를 함수 내에서 사용합니다. 이렇게 되면 makeIncrementer 호출이 종료될 때 runningTotalamount 의 값이 사라지지 않고 다음 호출 시 사용할 수 있습니다.

 

 

새로 생성된 incrementBySeven 은 캡쳐된 값에 영향을 받지 않지만, incrementByTen 은 이전에 캡쳐된 값에 영향을 받아 값이 증가하는 것을 확인할 수 있습니다.

클로저는 Reference Type

위의 예제에서 생성한 incrementByTenincrementBySeven 은 상수로 선언되었지만 이 상수들이 참조하는 클로저는 캡쳐한 변수를 증가시킵니다. 이는 클로저가 Reference Type 이기 때문입니다.

 

Class 또한 Reference Type 이며 상수로 인스턴스를 선언할지라도 내부 값은 변경할 수 있는 것과 같은 원리입니다. 그렇기 때문에 클로저 또한 다른 상수 혹은 변수에 클로저를 할당한다면 같은 클로저를 참조하게 된다는 것을 알 수 있습니다.

 

 

Escaping Closures

함수에 인자로 클로저를 전달하지만 함수가 반환된 후 호출되는 클로저를 함수를 탈출하다 라고 합니다.

 

클로저를 파라미터로 갖는 함수를 선언할 때 이 클로저는 탈출을 허락한다는 의미로 파라미터 타입 전에 @escaping 키워드를 작성합니다.

 

클로저가 탈출할 수 있는 한가지 방법은 함수 바깥에서 정의된 변수에 저장되는 것입니다.

 

예를 들어 비동기적 작업을 시작하는 대부분의 함수는 완료 핸들러로 클로저를 사용합니다. 이 함수는 작업을 시작한 후 반환되지만 작업이 완료될때까지 클로저가 호출되지 않습니다.

 

 

escaping 클로저에 self 캡쳐는 순환참조가 생기기 쉽습니다.

 

일반적으로 클로저는 클로저 내부에서 변수를 사용하여 암시적으로 변수를 캡쳐하지만 이 경우에는 명시적이어야 합니다.

 

self 를 캡쳐하려면 사용할 때 명시적으로 self 를 작성하거나 클로져의 캡쳐 목록에 self 를 포함합니다. self 를 명시적으로 작성하는데 의도를 표현하고 참조 사이클이 없음을 확인하도록 상기시켜줍니다.

 

 

위 코드에서 someFunctionWithEscapingClosure() 에 전달된 클로저는 명시적으로 self 를 참조합니다.

 

반대로 someFunctionWithNonescapingClosure() 에 전달된 클로저는 escpaing 클로저가 아닙니다. 그러므로 암시적으로 self 를 참조할 수 있습니다.

 

self 가 구조체 혹은 열거형 인스턴스이면 (Value Type) 항상 암시적으로 self 를 참조할 수 있습니다. 그러나 escaping 클로저는 이 경우에 self 에 대한 변경 가능한 참조를 캡쳐할 수 없습니다.

 

 

Autoclosures

자동클로저는 함수에 인자로 전달되는 표현식을 래핑하기 위해 자동으로 생성되는 클로저입니다.

 

인자를 가지지 않으며 호출될 때 내부에 래핑된 표현식의 값을 반환합니다. 이러한 구문상의 편의를 통해 명시적 클로저 대신 일반 표현식을 작성하여 함수의 파라미터 주위의 중괄호를 생략할 수 있습니다.

 

클로저가 호출될 때까지 코드 내부 실행이 되지 않기 때문에 자동 클로저는 판단을 지연시킬 수 있습니다. 판단 지연은 코드 판단 시기를 제어할 수 있기 때문에 사이드 이펙트가 있거나 계산이 오래 걸리는 코드에 유용합니다.

 

 

클로저 내부 코드에 의해 customersInLine 배열의 첫번째 요소는 삭제되지만 클로저가 실제로 호출되기 전까지 삭제되지 않습니다.

 

클로저가 호출되지 않으면 클로저 내부 표현식은 판단되지 않습니다. 이것은 배열의 요소가 삭제되지 않는다는 의미입니다.

 

함수의 인자로 클로저를 전달하면 위와 같은 지연 판단과 동일한 동작을 가질 수 있습니다.

 

 

serve(customer:) 함수는 소비자의 이름을 반환하는 명시적 클로저를 가집니다. 아래 serve(customer:) 의 버전은 같은 동작을 수행하지만 명시적 클로저를 가지는 대신에 파라미터 타입에 @autoclosure 속성을 표기하여 자동 클로저를 가집니다.

 

 

이렇게 되면 클로저 대신 String 인수를 받는 것처럼 함수를 호출할 수 있습니다. customerProvider 파라미터 타입은 @autoclosure 속성으로 표시되므로 인자는 자동으로 클로저로 변환됩니다.

참고 링크

https://docs.swift.org/swift-book/LanguageGuide/Closures.html

 

Closures — The Swift Programming Language (Swift 5.6)

Closures Closures are self-contained blocks of functionality that can be passed around and used in your code. Closures in Swift are similar to blocks in C and Objective-C and to lambdas in other programming languages. Closures can capture and store referen

docs.swift.org

https://bbiguduk.gitbook.io/swift/language-guide-1/closuresSwift Docs 번역본

 

클로저 (Closures) - Swift

컨텍스트로 타입 유추 (Inferring Type From Context)

bbiguduk.gitbook.io