Swift 함수와 클로저 | 함수 정의, 클로저, 고차 함수
이 글의 핵심
Swift 함수와 클로저: 함수 정의, 클로저, 고차 함수. 함수 정의·클로저 (Closures).
시리즈 안내
#03 | 📋 전체 목차 | 이전: #02 변수와 타입 · 다음: #04 클래스
들어가며
Swift에서 함수(function) 는 이름이 있고 호출 시그니처가 고정된 실행 단위이고, 클로저(closure) 는 주변 문맥을 캡처할 수 있는 함수 블록이에요. 둘 다 일급 시민(first-class citizen) 취급이라, 변수에 담기고 인자·반환으로 굴러다니죠. UIKit 콜백, 비동기 핸들러, map 같은 컬렉션 연산이 이 패턴이랑 잘 맞는 이유가 여기 있습니다.
이 글은 Swift 시리즈 세 번째로, 선언·호출 규칙부터 클로저 수명·캡처, @escaping / @autoclosure, 메서드·서브스크립트·연산자, 그리고 고차 연산·성능까지 한 흐름으로 정리합니다. 코드 예시는 Swift 5 이상을 기준으로 합니다.
독자는 #02 변수와 타입의 값/참조, Optional, 기본 제어 흐름에 익숙하다고 가정합니다. C/Java 계열에서 넘어온다면, Swift의 이름이 있는 인자(외부 레이블) 와 guard / if let 패턴이 클로저와 같이 쓰인다는 점을 염두에 두면, UIKit / SwiftUI 샘플 코드의 호출부가 훨씬 읽힐 것입니다. 반대로 이 글은 async / actor / 구조적 동시성 전부를 다루지는 않고, async 글로 이어갑니다.
용어: 본문에서 “함수”는 func로 선언한 이름 붙은 호출 가능 단위, “클로저”는 문맥을 캡처할 수 있는 블록 전반(익명·이름·중첩)을 둔러댑니다. Apple 문서도 같은 모델을 씁니다.
1. 함수 기초: 선언, 매개변수, 반환값
func 키워드로 정의하며, 매개변수 목록 뒤에 반환 타입 ->를 씁니다. 반환이 없으면 () 또는 Void로 표기할 수 있으며, 반환 키워드 return은 제어 흐름을 호출자에게 넘깁니다. Void 는 빈 튜플 ()의 타입 별칭이므로, () -> Void 와 () -> () 는 같은 함수 타입으로 취급됩니다(스타일상 전자가 흔함).
@discardableResult (별도) 는 “반환값을 무시해도 된다”는 경고를 끄는 속성이며, 체이닝/플루언트 API에서 쓰입니다(과용 시 의미 없는 무시를 숨깁니다).
@inlinable / @_transparent / @_optimize(speed) 는 최고 성능 경로에서 라이브러리 저자가 주로 쓰는 구역이며, 일반 앱 코드는 먼저 알고리즘과 I/O를 줄이는 편이 비용·효과 면에서 낫습니다(섹션 18).
func logLine(_ text: String) {
print("[LOG] \(text)")
}
func maxValue(_ a: Int, _ b: Int) -> Int {
if a >= b { return a }
return b
}
// 본문이 단일 식이면 return 생략 가능(단, 반환이 있을 때)
func minValue(_ a: Int, _ b: Int) -> Int { a < b ? a : b }
매개변수는 기본적으로 상수입니다. 본문에서 매개변수에 재할당이 필요하면 지역 복사본을 쓰거나(값 타입) inout을 사용합니다(뒤 절).
튜플 반환은 관련 없는 작은 집계를 한 번에 꺼낼 때 유용합니다. 이름 붙은 튜플 멤버는 호출 측에서 가독성이 좋아집니다. (더 큰 “묶음”은 struct로 승격하는 쪽이 타입으로서 명확합니다.)
다중 반환과 Optional — 실패가 있는 연산(Int 파싱, 네트워크)은 Optional / Result / throws 를 오류 경로로 분리하세요. “튜플 (값, Bool)” 같은 구식 신호는 enum 연관 값·Result 쪽이 유지보수에 유리한 경우가 많습니다.
func quotRem(_ a: Int, _ b: Int) -> (q: Int, r: Int) {
(a / b, a % b)
}
let r = quotRem(7, 3)
print(r.q, r.r) // 2 1
2. 함수 호출: Argument labels, parameter names
Swift는 외부 매개변수 이름(레이블) 과 내부 매개변수 이름을 분리할 수 있습니다. 호출자가 읽는 문장은 외부 이름에 의해 결정됩니다. 외부 레이블을 생략하려면 첫 토큰에 _를 씁니다.
// 외부: to, from / 내부: name, sender
func greetPerson(to name: String, from sender: String) {
print("\(sender) → \(name)")
}
greetPerson(to: "팀", from: "나")
// 외부 레이블 없이 호출
func addIntegers(_ a: Int, _ b: Int) -> Int { a + b }
_ = addIntegers(2, 3)
초기자 이외 API에서 to / in / at 같이 전치사를 쓰면, 호출부가 “문장처럼” 읽힙니다. 다만 첫 인자에도 레이블을 둘지, _로 생략할지는 팀 컨벤션으로 통일하는 것이 좋습니다(예: UIView 애니메이션 API는 섞여 있으므로 문서/스타일 가이드를 따릅니다).
Swift API Design Guidelines (요지): 불필요한 단어는 드러내지 않고, 첫 인자는 메서드 베이스 이름과 읽힌다. 그래서 insert(_:at:) 형태가 자주 나옵니다. 오픈소스/내부 라이브러리를 쓸 때는 public / open API의 이름이 곧 문서인 셈이므로, 레이블을 억지로 줄이기보다 호출문을 몇 번 읽어 보는 것이 빠릅니다.
기본값 + 오버로드: Swift는 기본 매개변수로 오버로드 다수를 대체할 수 있어 C++ 스타일의 void foo() / void foo(int) 나열이 덜합니다. 대신 이진 호환을 깨지 않으려면 새 인자 뒤에만 기본값을 추가하는 식의 발전이 일반적입니다.
3. 기본 매개변수 값
매개변수 뒤에 = 기본값을 두면, 호출에서 생략한 인자는 기본값으로 채워집니다. 기본값이 있는 인자는 보통 목록 뒤쪽에 두는 것이 호출 측에서 유리합니다(부분 인자를 자연스럽게 전달).
func connect(host: String, port: Int = 443, useTLS: Bool = true) {
print("\(host):\(port) TLS=\(useTLS)")
}
connect(host: "api.example.com")
connect(host: "dev.local", port: 8080, useTLS: false)
기본 인자는 한 번 평가되어 호출에 사용됩니다(클로저가 아닌 “값” 기본 인자). 복잡한 기본은 뒤 절의 @autoclosure를 참고합니다.
라벨 생략 시 혼동: greet(name:) 만 쓰면 greeting 은 기본값이고, greeting: 을 건너뛴 것인지 의도적으로 기본인지는 호출부만으로는 같아 보입니다. 팀에서 명시적 키워드를 선호하면, 기본값이 있어도 이름을 적게 하는 스타일을 쓰기도 합니다.
func buildQuery(_ path: String, useCache: Bool = true, timeout: Double = 30) { }
buildQuery("/v1") // 전부 기본
buildQuery("/v2", useCache: false) // 타임아웃은 기본
4. 가변 매개변수 (Variadic parameters)
Type... 문법은 0개 이상의 인자를 받고, 본문에서는 [Type]으로 사용합니다. 가변 인자 다음에 기본이 아닌 고정 인자를 두는 형태(예: 뒤에 closure)는 Swift 버전/진단에 따라 제한이 있을 수 있으므로, 실무에선 배열 인자로 대체하는 경우도 흔합니다.
func average(_ values: Double...) -> Double {
guard !values.isEmpty else { return 0 }
return values.reduce(0, +) / Double(values.count)
}
_ = average(1, 2, 3, 4)
가변 인자 뒤에 단일 추가 인자가 필요하다고 컴파일러가 막는 경우, [Double]에 한 번에 넘기는 편이 단순합니다.
5. inout 매개변수: 참조에 가까운 복사 제어
inout은 호출 측 변수의 메모리 위치를 가리키며, Callee에서 값을 쓰고 Caller에서 다시 읽는 패턴(스왑, 부분 갱신)에 쓰입니다. 상수나 리터럴을 넣을 수 없고, 호출부에서 &를 붙입니다. 구조체 등 값 타입은 복사-쓰기 최적화(COW) 뒤로 추상화됩니다.
func swapInts(_ a: inout Int, _ b: inout Int) {
(a, b) = (b, a)
}
var x = 1, y = 2
swapInts(&x, &y)
inout을 남용하면 “함수가 값을 바꾼다”는 부수 효과가 커져 읽기 어려울 수 있습니다. 작은 핵심에만 쓰고, 도메인 로직에선 mutating 메서드나 새 값을 반환하는 형태로 일관시키는 것이 일반적입니다.
6. 함수 타입: 일급 객체로서의 함수
이름이 없는 함수 값의 타입은 (Int, Int) -> Int처럼 씁니다. Void는 ()의 별칭이므로, () -> Void는 “인자 없이 아무것도 반환하지 않는 클로저” 타입으로 자주 씁니다.
typealias BinaryIntOp = (Int, Int) -> Int
let mul: BinaryIntOp = { $0 * $1 }
_ = mul(3, 4)
// 인스턴스 메서드를 “함수 값”으로 넘기기 (KeyPath / 메서드 참조)
struct Point { var x: Double, y: Double }
let points = [Point(x: 1, y: 2), Point(x: 3, y: 4)]
let xs = points.map(\.x)
_ = xs
함수 타입은 캡처가 있으면 달리 동작(클로저 저장 방식)할 수 있으니, unsafeBitCast 등으로 위치 수명을 강제하지 않습니다.
7. 함수를 매개변수로: 고차 함수 (Higher-order)
다른 함수를 인자로 받는 것이 고차 함수입니다. 배열 sort(by:), map, filter가 대표적입니다. 커스텀 API는 콜백·검사용 프레디킷에 자주 쓰입니다.
func withLogging<T>(_ work: () throws -> T) rethrows -> T {
print("start")
defer { print("end") }
return try work()
}
rethrows는 인자로 받은 클로저가 throw할 때만 본문이 throw한다는 뜻입니다(표준 map의 변형 API에서 흔함).
8. 함수를 반환: 팩토리·전략 패턴
함수 반환은 설정에 따라 달리 동작하는 연산을 캡슐화할 때 쓰입니다(커서 기반 읽기, 임시 하한/상한, 테이블 룩업).
func makeAdder(_ base: Int) -> (Int) -> Int {
{ x in base + x }
}
let add5 = makeAdder(5)
_ = add5(3) // 8
반환된 클로저가 base를 캡처하므로, 캡처 수명(순환 참조 등)을 이후 “클로저 캡처” 절에서 다룹니다.
9. 중첩 함수, 클로저 맛보기
func 는 다른 func 안에 둘 수 있으며, 바깥 스코프의 let / var 를 읽고, 필요하면 캡처를 갱신합니다(단, 값 타입의 var 캡처는 복사 세맨틱과 함께 주의). 중첩은 private 헬퍼를 정의 직가까이 두어 응집도를 높일 때 씁니다.
func runBatch(count: Int) {
var seen = 0
func step() {
seen += 1
if seen < count { step() }
}
step()
}
클로저는 in으로 본문을 열고, “이름이 없는” 함수에 가깝습니다. 뒤 절에서 문법을 완전히 정리합니다.
let add: (Int, Int) -> Int = { a, b in a + b }
10. 클로저: 완전 가이드, Trailing closure
클로저는 (1) 전역/중첩 함수, (2) 익명 클로저, (3) 캡처가 모두 같은 클로저 가족입니다. 익명 형태:
let nums = [1, 2, 3, 4]
let squares = nums.map({ (n: Int) -> Int in n * n })
타입 추론이 되면 (Int) -> Int 를 생략할 수 있고, 인자는 $0 $1 으로 줄입니다.
후행 클로저(Trailing closure) : 마지막 인자가 클로저이면, 괄호 밖으로 뺄 수 있습니다. 유일한 인자이면 () 를 생략해도 됩니다.
_ = [1, 2, 3].map { $0 * 2 }
UIView.animate(withDuration: 0.2) {
// 애니메이션
} completion: { _ in
// 완료
}
completion: 처럼 레이블 뒤에 다시 후행을 붙이는 다중 trailing closure 문법( Swift 5.3+ ) 은 UIKit / SwiftUI에서 광범위하게 쓰입니다.
단순화 순서(기억용): 타입 생략 → return 생략(단일 표현식) → 인자를 $0… 로 축약. 과도한 축약은 짝수/인덱스처럼 의미가 애매한 경우 가독성을 해칠 수 있으니, filter 안에서는 name.hasPrefix 처럼 이름을 쓰는 편이 낫습니다.
KeyPath, 메서드 값, throws / rethrows 클로저
\.member KeyPath 는 프로퍼티에 대한 읽기 전용 참조이며, map / mapValues / 정렬 key 등으로 잘 맞습니다. WritableKeyPath 는 mutable subscript와 함께 쓰는 고급 패턴입니다.
struct User: Equatable { let name: String; let score: Int }
let users = [User(name: "A", score: 10), User(name: "B", score: 20)]
let top = users.max(by: { $0.score < $1.score }) // “점수 비교”
let byName = users.sorted { $0.name < $1.name }
_ = (top, byName)
throws 클로저는 map / forEach 표준 시그니처가 아니므로(버전/오버로드는 확인), try가 필요한 변환은 루프나, compactMap + 별도 Result / rethrows 래퍼(직접 구현)로 풀기도 합니다. JSONDecoder 여러 개 디코딩은 대표적입니다. rethrows 는 인자 클로저가 throw할 때만 본 API가 throw한다는 의미(예: Array.map의 throw 대응)입니다.
@Sendable 는 동시성이 붙는 클로저(다른 actor/Task에 넘김)에서 캡처가 안전한지(값 복사·경쟁 완화)를 표시·검사하는 쪽으로 이해하면 됩니다. UI 앱이면 Swift async/await 이후 콤바인/액터 글을 함께 읽는 것이 좋습니다.
11. 클로저 캡처: Capture list, weak / unowned
실제 앱 붙이면서 한 번은 그랬어요. UIViewController에서 네트워크 요청 completion 안에 self를 그냥 잡고 있었더니, 화면은 내려갔는데 인스턴스가 생각보다 오래 사는 느낌이 났죠. Instruments 열고 Allocations 보면서야 “아, 이거 클로저 캡처 때문에 self를 강하게 잡고 있었구나” 하고 눈이 떠졌습니다. 그때부터는 비동기·UI 쪽에서 [weak self]랑 guard let self를 습관처럼 쓰는 이유를 체감으로 이해했어요. 팀마다 똑같은 말이 반복되는 데엔 이유가 있습니다.
클로저는 생성 시점에 주변의 값/참조를 캡처합니다. 참조 타입(클래스 인스턴스)을 캡처하면 강한 참조가 늘어 순환 참조(retain cycle) 가 될 수 있어, [weak self] / [unowned self] 캡처 목록을 씁니다.
final class Work {
var onDone: (() -> Void)?
func start() {
onDone = { [weak self] in
self?.finished()
}
}
func finished() { }
}
weak:self가 해제될 수 있으므로self?(옵셔널)로 호출. UI 이벤트, 네트워크 완료에 기본으로 안전.unowned: 수명이 보장될 때(명확한 부모-자식 생명주기. 잘못 쓰면 크래시).self가 항상 살아 있다는 엄한 전제가 있을 때만.
[weak a = self, unowned b = other] 처럼 캡처 이름을 붙이거나(구버전 스타일), guard let self 패턴( Swift 5.7+ guard let self else { return } )으로 옵셔널 self를 바인딩하는 코드가 권장됩니다.
값 타입(구조체) 캡처는 보통 var / let 복사이므로, 클로저 여러 인스턴스가 다른 시점에 다른 복사본을 캡처할 수 있습니다(디버깅 시 “왜 이 값이지?”를 봅니다).
12. @escaping: 이스케이핑 클로저
@escaping 은 클로저가 함수가 반환된 뒤에도 살아남을 수 있음을 컴파일에 알리는 속성입니다. GCD, URLSession, DispatchQueue 등 “나중에 호출” API가 전부 여기에 해당합니다. 기본(클로저 매개변수)는 non-escaping 이어서, self를 암시적으로 캡처해도 일시인 경우가 많고, @escaping이면 self를 쓸 때 명시가 필요한 경우가 있습니다(팀 스타일).
func later(_ work: @escaping () -> Void) {
DispatchQueue.main.async {
work()
}
}
@escaping 클로저를 프로퍼티에 저장하거나(핸들러), 백그라운드에 넘기면, 그 수명이 호출이 끝난 뒤로 늘어납니다. 그래서 취소 토큰이나 weak 설계를 함께 고려합니다.
13. @autoclosure: 자동으로 클로저로 감싸기
@autoclosure 는 인자를 즉시 평가하지 않고 클로저로 감싸 둡니다(지연). assert / assertionFailure / && 의 짧은 회로(단, &&는 별도 메커니즘)는 개념적으로 유사하나, API 설계 상으로는 비용이 큰 계산을 “필요할 때만” 돌릴 때 쓰는 경우가 큽니다. @autoclosure(escaping) 도 가능합니다(문서화와 주의 필요).
func when(_ cond: Bool, _ body: @autoclosure () -> String) {
if cond { print(body()) }
}
when(2 > 1, "ok") // "ok" 는 cond가 true일 때만 평가(개념 예시; 실제로는 inlining 등으로 다름)
과용하면 “호출부가 값을 넣은 것인지, 늦게 평가하는지”가 혼란을 줍니다. 진단·로그·디버그 한정, 혹은 DSL이 아닌 이상, 일반 API에는 그냥 () -> String 명시형이 더 낫다는 팀이 많습니다.
14. 메서드: Instance methods, type methods
인스턴스 메서드는 self에 바인딩됩니다. struct / enum의 메서드가 필드를 바꾸면 mutating 키워드가 필요합니다. 클래스는 참조이므로 mutating이 없고, static / class 는 타입에 붙습니다(class는 서브클래스에서 오버라이드용).
struct Counter {
private var n = 0
mutating func inc() { n += 1 }
var value: Int { n }
}
enum Math {
static func hypot(_ a: Double, _ b: Double) -> Double {
(a * a + b * b).squareRoot()
}
}
스위프트 프로퍼티 래퍼·@objc 셀렉터·@MainActor 는 별 글(동시성, UI)에서 이어갑니다.
15. Subscripts: 서브스크립트
subscript 키워드는 다중 인자·읽기 전용/읽기-쓰기·에러(throws) 도 지원하며(조합은 플랫폼/버전에 맞게), “연산자처럼” 컬렉션 문법을 씁니다.
struct Box<T> {
private var store: [String: T] = [:]
subscript(key: String) -> T? {
get { store[key] }
set { store[key] = newValue }
}
}
var b = Box<String>()
b["a"] = "1"
_ = b["a"]
범위(Range 등) 를 받는 subscript 는 문자열, 버퍼 뷰에서 흔합니다. 인덱스 경계는 문서/전제(혹은 precondition)를 밝힙니다.
throws 서브스크립트(가능한 Swift 버전)는 I/O·디코딩 경계 래퍼에 쓸 수 있으나, 가독성 측면에선 func load() 처럼 이름이 붙는 API가 팀에 더 잘 먹히는 경우가 많습니다(실패 throw는 호출 측 try가 전파되므로 subscript 문맥이 과해질 수 있음).
// 개념 스케치(플랫폼/버전에 맞는 시그니처로 조정)
struct Guarded {
subscript(safe i: Int) -> Int? {
get { nil }
}
}
let g = Guarded()
_ = g[safe: 0]
다중 subscript (인자 수·타입이 다름)는 C++ operator[] 오버로드와 비슷하게 분기에 쓰이지만, API 표면이 늘어나면 역시 method·enum으로 흡수를 검토합니다.
16. 연산자 오버로딩: Custom operators
operator 선언(전행/후행)과 우선순위 그룹 precedencegroup을 지정한 뒤, 전역 func로 구현할 수 있습니다. == / + 는 프로토콜 준수(Equatable, Addable 등) 쪽이 더 “스위프트닉”이지만, 도메인 DSL이나 기하·행렬 같은 한정 맥락에서 커스텀 infix를 쓰기도 합니다.
infix operator ⊕: AdditionPrecedence
struct Vec2 { let x: Double, y: Double }
func ⊕(lhs: Vec2, rhs: Vec2) -> Vec2 {
Vec2(x: lhs.x + rhs.x, y: lhs.y + rhs.y)
}
팀 스타일 가이드에서 커스텀 연산자는 거의 막는 경우가 많으므로(검색·입력 어려움), 이름이 있는 메서드 plus 가 대안이 됩니다.
17. 실전 패턴: map, filter, reduce, 체이닝
Sequence / Collection에 대한 고차 연산은 의도를 짧게 드러냅니다. 한 번에 한 단계씩 읽힌다는 점이 장점입니다.
let numbers = [1, 2, 3, 4, 5, 6]
let v = numbers
.filter { $0 % 2 == 0 } // 짝수
.map { $0 * $0 } // 제곱
.reduce(0, +) // 합
// 4*4 + 6*6 = 16+36 = 52
flatMap / compactMap 은 “옵셔널 제거+평탄화”·“중첩 시퀀스 평탄화”에 쓰입니다(문맥에 따라 deprecated 된 시그니처는 피하고 compactMap을 사용).
체이닝은 “중간 컬렉션”이 생기는 만큼 대용량 배열이면 lazy (지연)과 조합하거나, 한 번 루프로 합치는 reduce(into:) / 인덱스 루프로 최적화를 검토합니다(다음 절).
let strings = ["1", "2", "x", "3"]
let ints: [Int] = strings.compactMap { Int($0) } // [1, 2, 3]
reduce(into:)·집계·질의, 정렬
reduce(0, +) 는 익숙하지만, 가변 누적이 필요한 경우 reduce(into:_:) 가 한 번 할당으로 끝나는 경우가 많습니다(특히 Dictionary·Set으로 모을 때). “모든 요소가 조건을 만족”·“하나라도”는 allSatisfy / contains(where:)가 의도를 드러냅니다.
var freq: [String: Int] = ["a": 1, "b": 1]
let words = ["a", "c", "a", "b"]
_ = words.reduce(into: &freq) { dict, w in
dict[w, default: 0] += 1
}
let allPositive = [1, 2, 3].allSatisfy { $0 > 0 }
let hasZero = (1...5).contains(where: { $0 == 0 })
_ = (allPositive, hasZero)
정렬은 sort / sorted (in-place / 새 배열)로 나뉘고, Comparable을 준수하면 sorted() 만으로 끝납니다. 다중 기준은 튜플 비교(예: ($0.lname, $0.fname))로 한 번에 sorted { … } 하는 패턴이 흔합니다.
첫/마지막 조건: first(where:) / last(where:); 인덱스: firstIndex(where:) — O(n) 선형 검사이므로, 대용량+반복이면 인덱스/맵을 먼저 구축하는 편이 낫습니다.
18. 성능: 인라인, 최적화 메모
@inlinable: 모듈 경계(라이브러리) 퍼블릭 API에서, 클라이언트가 본문을 볼 수 있어 제네릭·작은 래퍼를 인라인하도록 힌트(주의: 바이너리 호환·ABI 이슈는 문서 확인).- COW(복사-쓰기): 큰
Array복제가 “즉시”가 아닐 수 있으나, 둘 이상 쓰기면 복제가 납니다.map·filter는 새 배열. - 클로저 할당: 짧은 클로저는 스택/인라이닝이 잘 될 수 있으나, 캡처가 늘면 힙 할당이 붙을 수 있어(미세 벤치마크) 핫 루프는 인라인 for와 비교합니다.
lazy: 체이닝마다 중간 배열이 없어질 수 있으나(시퀀스), 캐릭터는 다시 평가될 수 있어 side-effect가 있는 변환엔 쓰지 않습니다.
let big = Array(1...1_000_000)
_ = big.lazy.filter { $0 % 2 == 0 }.map { $0 * 2 }
ContiguousArray / withUnsafeBufferPointer 는 특수 상황에서만 고려하며(오디오, 고성능), 일반 UI/네트워크는 가독성을 우선합니다.
@inlinable 은 (주로 프레임워크 소스) 공개 inline 가능한 작고 안정한 래퍼에 붙이며, ABI 문서(Apple / Swift)로 이진 호환과 공개(abi) 를 확인합니다. 앱 코드만 쓰는 팀이면, 일반 private + 컴파일러 최적화에 맡기는 경우가 대부분입니다.
// 개념 예시(실제는 모듈·가시성과 함께 설계)
@inlinable
public func clamp(_ v: Int, _ lo: Int, _ hi: Int) -> Int {
min(max(v, lo), hi)
}
프로파일은 Instruments Time Profiler·Allocations로 “클로저 체이닝 vs for”를 의심 지점에만 측정하세요. 최적화 전·후에 동일 입력으로 비교하는 것이 핵심입니다.
19. 실전에서 배운 것들
레이블은 팀이 읽기 편한 쪽이면 됩니다. to:, for: 덕에 호출이 문장처럼 읽히면 좋고, 뜻없이 전부 _로 박으면 “이 인자 뭐였지?” 싶을 때가 있어요. inout은 짧은 스왑·일부 갱신에 쓰는 편이 직관적이고, 도메인 전체를 흔드는 암시적 가변은 피하는 게 나중에 덜 아프습니다.
클로저 한 줄로 줄이는 건 OK인데, filter에서 $0만 쭉 나오면 짝수인지 인덱스인지 헷갈릴 땐 이름을 써도 됩니다. 비동기·UI는 weak를 기본으로, unowned는 수명이 정말 보일 때만. @escaping은 나중에 돌릴 핸들러에 맞게 표기하는 일인데, 컴파일이 잡아주면 메시지를 한번 읽어보세요. map이랑 filter를 이어 붙이는 것도 2~4단 정도는 의도가 읽히는데, 열몇 단이면 중간에 브레이크 포인트 찍기가 힘듦니다. 커스텀 연산자나 이상한 Unicode는 검색·리뷰가 힘들어서 팀에선 잘 막는 편이고, subscript는 도메인 키에 맞을 때 쓰는 게 맞아요. 모든 읽기를 []로 몰아넣으면 실패·로그 추적이 꼬일 수 있고요. 성능은 병목 재고 나서 lazy나 inout을 논하거나, unsafe 쪽은 정말 필요한지부터요. 문자열·범위는 String.Index 루트로 가는 쪽이 Swift답고, 다른 언어 습관대로 Int 인덱스를 끼워 넣으면 여기서만 터집니다.
새 API를 추가했다면 퍼블릭인지, 실패는 throw / Result / Optional 중 어디에 둘지, 클로저엔 @escaping·@Sendable이 필요한지 정도만 한 번씩 훑으면 됩니다. Xcode가 self나 @escaping Fix-it을 제안해도, 수명이 먼저인지 항상 짚고 가면 덜 밟습니다. 막힐 땐 swiftc -typecheck로 작은 파일만 떼서 타입체크해보는 것도 괜찮아요(프로젝트 전체보다 원인이 좁혀질 때가 있습니다).
정리
- 함수:
func, 외부/내부 레이블, 기본/가변/inout, 튜플 반환. - 함수 타입 & 고차: 일급 값,
map/filter/reduce와 콜백 설계. - 클로저: 후행, 단축 인자, 캡처·
weak/unowned,@escaping/@autoclosure. - 타입에 붙는 기능:
mutating,static/class,subscript, 선별적 연산자. - 성능·품질:
lazy·인라인·측정, 그리고 일관된 스타일.
함수·클로저는 이후 프로토콜 확장, 제네릭 제약, 오류 처리 글에서 같은 문법을 반복하게 됩니다. 여기서 호출 규칙과 캡처를 익혀 두면 그때 기호만 덜 낯설어집니다.
다음 단계
관련 글
- JavaScript 함수 | 함수 선언, 화살표 함수, 콜백, 클로저 완벽 정리
- Kotlin 함수 | 함수 정의, 람다, 고차 함수
- C++ 기본 인자 |
- C++ 람다 캡처 에러 |
- C++ 이름 은닉 |
자주 묻는 질문 (FAQ)
Q. map에서 인덱스가 필요하다면?
A. enumerated()를 쓰거나, for (i, v) in array.enumerated() 형태로 변환·검사를 먼저 수행하세요. 인덱스+값이 동시에 필요할 때 zip / indices를 함께 고려합니다.
Q. trailing closure는 언제 피하는 편이 좋을까?
A. 여러 클로저가 “대칭”이 아닐 때(첫 클로저는 짧고 둘째는 긴 경우), 라벨이 없으면 어느 블록인지 읽기 어렵기 때문입니다. 그때는 첫 클로저도 괄호 안에 두는 등 가독성을 택하세요(팀 룰에 따름).
Q. unowned는 언제 써도 안전한가?
A. self가 항상 클로저보다 오래 산다는 강한 모델(예: 자식 → 부모)에서만. 의심되면 weak가 낫고, guard let으로 조기 return 하세요.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Kotlin 함수 | 함수 정의, 람다, 고차 함수
- Kotlin 테스팅 | JUnit, MockK, 테스트 작성법
- TypeScript 데코레이터 | Decorators 완벽 가이드
이 글에서 다루는 키워드 (관련 검색어)
Swift, 함수, 클로저, 고차함수 등으로 검색하시면 이 글이 도움이 됩니다.