본문 바로가기
iOS

Swift API Ergonomics in One File: Labels, Types, and Calls

by Hwangminseo 2025. 9. 30.

UIKit 앱 뼈대(AppDelegate·SceneDelegate·ViewController)와 iOS 13+ 씬 생명주기 흐름을 코드로 직접 구성해 볼 것이다.
함수 타입·함수 이름 구분, 전달인자 레이블 규칙, #function, Xcode ⌥-클릭 Quick Help로 시그니처 읽는 법을 정리해 볼 것이다.
튜플 다중 반환·가변/inout·일급 함수·클로저(표현식·후행·단축 인자)와 클래스의 저장 프로퍼티/이니셜라이저 규칙을 예제로 점검해 볼 것이다.

UIKit 기반 iOS 앱의 최소 AppDelegate 예제

//
//  AppDelegate.swift
//  Example
//
//  Created by 1 on 2025/09/30.
//

import UIKit

@main
class AppDelegate: UIResponder, UIApplicationDelegate {

    // 앱이 시작될 때 한 번 호출된다.
    // 공통 초기화, SDK 설정, 로그 설정 등을 이 지점에서 수행한다.
    func application(_ application: UIApplication,
                     didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
        // 앱 런치 후 커스터마이징 지점
        return true
    }

    // MARK: - UISceneSession Lifecycle (iOS 13+)
    // 새 씬(윈도우/탭)을 만들 때 어떤 설정을 사용할지 반환한다.
    // 여러 씬을 지원하는 앱이라면 여기서 이름/역할에 맞는 설정을 선택한다.
    func application(_ application: UIApplication,
                     configurationForConnecting connectingSceneSession: UISceneSession,
                     options: UIScene.ConnectionOptions) -> UISceneConfiguration {
        return UISceneConfiguration(name: "Default Configuration",
                                    sessionRole: connectingSceneSession.role)
    }

    // 사용자가 씬을 닫는 등 세션이 폐기되었을 때 호출된다.
    // 해당 씬과 관련된 리소스를 정리하거나 저장되지 않은 데이터를 정리한다.
    func application(_ application: UIApplication,
                     didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
        // 필요한 정리 작업 수행
    }
}

didFinishLaunchingWithOptions에서 전역 초기화(로그, SDK, 테마)를 수행한다.
iOS 13+ 씬 라이프사이클 사용으로 화면 구성은 SceneDelegate가 맡는다.
configurationForConnecting에서 생성될 씬의 설정을 선택한다.
didDiscardSceneSessions에서 닫힌 씬의 리소스를 정리한다.
필요 시 런치 지점에 푸시, 분석, Appearance 설정 등을 추가해 확장한다.

UIKit iOS 13+의 씬 기반 생명주기를 사용하는 SceneDelegate 예제

//
//  SceneDelegate.swift
//  Example
//
//  Created by 1 on 2025/09/30.
//

import UIKit

class SceneDelegate: UIResponder, UIWindowSceneDelegate {

    var window: UIWindow? // 이 씬(Window)의 루트 컨테이너

    // 앱이 이 씬에 연결될 때 한 번 호출된다.
    // 스토리보드를 쓰면 window가 자동 연결되고, 코드 기반이면 여기서 직접 구성한다.
    func scene(_ scene: UIScene,
               willConnectTo session: UISceneSession,
               options connectionOptions: UIScene.ConnectionOptions) {
        guard let _ = (scene as? UIWindowScene) else { return }

        // (코드 기반 예시)
        // let windowScene = scene as! UIWindowScene
        // let win = UIWindow(windowScene: windowScene)
        // win.rootViewController = UINavigationController(rootViewController: ViewController())
        // win.makeKeyAndVisible()
        // self.window = win
    }

    // 이 씬의 세션 연결이 끊겼을 때(닫힘/제거) 호출된다. 재생성 가능한 리소스를 정리한다.
    func sceneDidDisconnect(_ scene: UIScene) { }

    // inactive → active 로 전환 시 호출. 일시 중지했던 작업을 재개한다.
    func sceneDidBecomeActive(_ scene: UIScene) { }

    // active → inactive 직전 호출. 타이머/애니메이션 일시 중지 등 처리한다.
    func sceneWillResignActive(_ scene: UIScene) { }

    // background → foreground 진입 직전. UI 갱신/세션 복구 준비를 한다.
    func sceneWillEnterForeground(_ scene: UIScene) { }

    // foreground → background 진입 직후. 데이터 저장, 공유 리소스 해제 등을 수행한다.
    func sceneDidEnterBackground(_ scene: UIScene) { }
}

AppDelegate가 전역 초기화를 맡고, SceneDelegate는 각 윈도우(화면) 단위의 상태와 전환을 관리한다.
scene(_:willConnectTo:)에서 스토리보드면 자동 연결, 코드 기반이면 윈도우·루트 뷰 컨트롤러를 직접 구성한다.
sceneDidBecomeActive/sceneWillResignActive는 활성화·비활성화 전환 시 작업 재개·일시중지를 처리한다.
sceneWillEnterForeground/sceneDidEnterBackground는 전경·배경 전환에 맞춰 UI 갱신, 저장, 리소스 해제를 수행한다.
sceneDidDisconnect는 씬이 닫힐 때 재생성 가능한 리소스를 정리한다.

UIKit 기반의 기본 ViewController 예제

//
//  ViewController.swift
//  Example
//
//  Created by 1 on 2025/09/30.
//

import UIKit

class ViewController: UIViewController {

    // 화면이 메모리에 로드된 직후 한 번 호출된다.
    // UI 초기화, 데이터 바인딩, 의존성 주입 등을 여기서 수행한다.
    override func viewDidLoad() {
        super.viewDidLoad()
        // 추가 설정 지점 (예: 배경색, 내비게이션 타이틀, 버튼 타깃 등)
        // self.view.backgroundColor = .systemBackground
        // self.title = "홈"
    }

    // 필요 시 viewWillAppear/viewDidAppear 등을 오버라이드해
    // 화면 전환 시점에 맞춘 갱신·애니메이션을 처리한다.
}

UIViewController가 화면 한 장의 뷰와 로직을 관리한다.
viewDidLoad는 화면이 메모리에 로드된 직후 한 번 호출되며, UI 초기화·데이터 바인딩·의존성 주입을 수행한다.
코드 기반일 경우 이 지점에서 배경색, 내비게이션 타이틀, 서브뷰 추가·오토레이아웃을 구성한다.
화면 전환 시점 갱신과 애니메이션은 viewWillAppear/viewDidAppear에서 처리한다.
관찰자·타이머 등 리소스 해제는 viewWillDisappear/viewDidDisappear 또는 deinit에서 정리한다.

4가지 Swift 함수

// 호출 레이블만 다르고 타입은 모두 동일하다.

// 1) 외부레이블 = 내부이름
func add(x: Int, y: Int) -> Int { x + y }
print(add(x: 10, y: 20))            // 출력: 30

// 2) 외부레이블(first/second)과 내부이름(x/y) 분리
func add(first x: Int, second y: Int) -> Int { x + y }
print(add(first: 10, second: 20))   // 출력: 30

// 3) 외부레이블 생략(언더스코어)
func add(_ x: Int, _ y: Int) -> Int { x + y }
print(add(10, 20))                  // 출력: 30

// 4) 첫 번째 생략, 두 번째만 레이블 지정
func add(_ x: Int, with y: Int) -> Int { x + y }
print(add(10, with: 20))            // 출력: 30

// 네 함수 모두 “함수 타입”은 동일하다: (Int, Int) -> Int
print(type(of: add(x:y:)))          // 출력: (Int, Int) -> Int
print(type(of: add(first:second:))) // 출력: (Int, Int) -> Int
print(type(of: add(_:_:)))          // 출력: (Int, Int) -> Int
print(type(of: add(_:with:)))       // 출력: (Int, Int) -> Int

4가지 함수는 매개변수의 “외부 레이블(호출 시 보이는 이름)”만 다르고, 내부 구현과 반환 타입은 동일하다.
외부 레이블은 호출 가독성을 높이며 _로 생략할 수 있다. 내부 이름은 함수 본문에서만 사용한다.
with처럼 자연어 레이블을 섞어 읽기 좋은 호출문을 만들 수 있다.
함수의 타입은 레이블과 무관하게 매개변수/반환 타입으로만 결정되어 모두 (Int, Int) -> Int이다.
여러 오버로드 중 특정 함수를 참조할 때는 add(x:y:), add(_:with:)처럼 레이블까지 포함해 지칭한다.

함수 타입과 함수 이름을 구별하는 방법

func tableView(_ tableView: UITableView,
               numberOfRowsInSection section: Int) -> Int {
    return items.count
}
// 함수 타입: (UITableView, Int) -> Int
// 함수 이름: tableView(_:numberOfRowsInSection:)

func tableView(_ tableView: UITableView,
               cellForRowAt indexPath: IndexPath) -> UITableViewCell {
    return cell
}
// 함수 타입: (UITableView, IndexPath) -> UITableViewCell
// 함수 이름: tableView(_:cellForRowAt:)

시그니처에서 매개변수의 타입만 왼쪽부터 순서대로 뽑아 괄호로 묶는다.
그 뒤에 -> 반환타입을 붙여 (Param1, Param2) -> Return 형태의 함수 타입을 적는다.
함수 이름은 기본 이름 뒤에 각 매개변수의 외부 레이블을 콜론과 함께 이어붙여 표기한다.
외부 레이블이 _이면 생략 레이블로 이름에 _를 그대로 넣어 쓴다(예: tableView(_:cellForRowAt:)).
오버로드 구분은 레이블+타입 조합으로 이루어지며, 프로토콜/익스텐션 메서드도 동일 규칙을 따른다.

식별자에 대해 도움말 보기

Xcode에서 식별자 위에 Option(⌥)+클릭하면 ‘Quick Help’ 팝오버가 열린다.
팝오버에는 해당 심볼의 선언 시그니처(타입·반환값)와 접근 수준이 표시된다.
문서 주석(///)이 있으면 요약 설명과 매개변수·반환값 설명이 함께 나온다.
‘Declared In’에서 정의된 파일/모듈을 보여 주며, 항목을 클릭하면 그 위치로 이동한다.
더 자세한 내용은 팝오버의 책 아이콘을 눌러 Developer Documentation에서 확인한다.

레이블이 붙은 튜플로 여러 값을 한 번에 반환하는 예제

import Foundation

// 두 정수 x, y를 받아 합·차·나눗셈 결과를 “레이블이 붙은 튜플”로 반환한다.
func sss(x: Int, y: Int) -> (sum: Int, sub: Int, div: Double) {
    let sum = x + y                  // 덧셈
    let sub = x - y                  // 뺄셈
    let div = Double(x) / Double(y)  // 같은 자료형끼리 연산 → Double로 변환
    return (sum, sub, div)           // (sum:…, sub:…, div:…) 형태로 반환
}

let result = sss(x: 10, y: 3)

print(type(of: sss))        // 출력: (Int, Int) -> (sum: Int, sub: Int, div: Double)
print(result.sum)           // 출력: 13
print(result.sub)           // 출력: 7
print(result.div)           // 출력: 3.3333333333333335

let x = String(format: "%.2f", result.div) // 소수 둘째자리까지 반올림해 문자열로 변환
print(x)                    // 출력: 3.33

type(of:)는 함수의 시그니처(타입)를 보여준다.

부동소수는 이진 표현 특성상 3.3333333333333335처럼 길게 보일 수 있으며, 화면 표시는 String(format:)으로 필요한 자리수만 포맷한다.

y가 0이면 분모가 0이므로 실제 코드에서는 guard y != 0 같은 방어 로직을 두는 것이 안전하다.

 

가변 인자 예제

func add(numbers: Int...) {
    var sum: Int = 0                 // 누적 합
    for num in numbers {             // Int... 는 내부에서 [Int]처럼 순회 가능
        sum += num
    }
    print(sum)                       // 결과 출력
}

add(numbers: 1, 2, 3)               // 출력: 6

가변 인자 Int...는 호출부에서 1, 2, 3처럼 여러 값을 쉼표로 전달한다.
함수 내부에서는 numbers가 [Int]처럼 동작하여 for-in으로 순회한다.
sum에 모든 항목을 누적하여 최종 합을 print로 출력한다.
읽기 좋게 하려면 반환형을 Int로 바꾸고 return sum을 사용해 호출부가 활용하게 할 수 있다.

일급 객체

func up(num: Int) -> Int {
    return num + 1
}

func down(num: Int) -> Int {
    return num - 1
}

let toUp = up          // 타입: (Int) -> Int  — 함수 참조를 상수에 저장
print(up(num: 10))     // 출력: 11
print(toUp(10))        // 출력: 11   // 주의: 함수 참조 호출 시 argument label을 쓰지 않는다

let toDown = down      // 타입: (Int) -> Int
print(down(num: 10))   // 출력: 9
print(toDown(10))      // 출력: 9

함수는 일급 객체라 변수/상수에 담을 수 있다.
let toUp = up처럼 참조로 저장하면 호출 시 라벨을 쓰지 않는다(예: toUp(10)).
함수 타입은 라벨과 무관하게 (Int) -> Int로 표현된다.
특정 구현을 지칭할 때는 up(num:)처럼 라벨까지 포함해 표시한다.

클로저 예제를 많이 사용하는 프로그래밍 언어

1 JavaScript / TypeScript 비동기·이벤트·함수형 유틸 전부 클로저 기반 콜백/Promise, map/filter/reduce () => {}
2 Swift iOS API가 클로저 중심 설계 콜백, completion, 고차함수 { }, trailing closure
3 Kotlin 컬렉션/코루틴이 람다에 의존 DSL, scope 함수, flow { }, it, inline
4 Python 내부함수/데코레이터로 캡처 사용 함수형 유틸, 데코레이터 def inner, nonlocal
5 Ruby 블록/프로시저가 일상 each/map, DSL do … end, ->
6 C# LINQ·이벤트에 람다 상시 쿼리, 이벤트 핸들러 x => x+1
7 Rust 이터레이터·클로저 조합 강력 map/filter, move 캡처 `move
8 Java 자바8 이후 보급, 범용성 ↑ 스트림, 콜백 y -> x+y
9 Go 존재는 하지만 패턴화 적음 핸들러, 지연 실행 func(...) {}
10 PHP 가능하지만 주류 패턴은 아님 간단 콜백 function() use($x){}

클로저 표현식 예제

// 함수 버전 -------------------------------------------------
func mul(x: Int, y: Int) -> Int {     // 명명된 함수
    return x * y
}
let r1 = mul(x: 10, y: 20)
print(r1) // 출력: 200

// 클로저 표현식 버전 -----------------------------------------
// { (매개변수) -> 반환타입 in 본문 }
let multiply: (Int, Int) -> Int = { (x: Int, y: Int) -> Int in
    return x * y
}

let r2 = multiply(10, 20)             // 상수에 담긴 클로저를 함수처럼 호출
print(r2) // 출력: 200

// 타입 추론·축약 형태 ----------------------------------------
// 매개변수/반환타입은 좌변에서 추론 → 생략 가능
let multiply2: (Int, Int) -> Int = { x, y in x * y }
print(multiply2(3, 4)) // 출력: 12

클로저 기본형은 { (매개변수) -> 반환타입 in 본문 }이다.
in 앞은 시그니처, 뒤는 실행 코드이다.
좌변에 함수 타입을 명시하면 매개변수 타입과 반환타입을 생략해 축약한다.
함수 mul과 클로저 multiply의 타입은 (Int, Int) -> Int로 동일하다.
클로저는 상수/변수에 담아 함수처럼 호출한다.

후행 클로저

// trailing closure 기본 예제
func someFun(cl: () -> Void) { cl() }

// 괄호 안에 클로저
someFun(cl: {
    print("A")            // 출력: A
})

// trailing closure (마지막 인자가 클로저면 소괄호 밖으로)
someFun {
    print("B")            // 출력: B
}

// 다른 인자 + 마지막 클로저
func calc(_ a: Int, _ b: Int, op: (Int, Int) -> Int) -> Int { op(a, b) }

let r1 = calc(3, 4, op: { x, y in x + y })
print(r1)                 // 출력: 7

let r2 = calc(3, 4) { x, y in x * y }   // trailing closure
print(r2)                 // 출력: 12

// 표준 라이브러리 예
let arr = [3, 1, 2]
print(arr.sorted(by: { $0 < $1 }))      // 출력: [1, 2, 3]
print(arr.sorted { $0 < $1 })           // 출력: [1, 2, 3]  // trailing

 

마지막 매개변수가 클로저면 클로저를 괄호 밖으로 뺄 수 있다.
인자가 클로저 하나뿐이면 () 자체도 생략 가능.
다른 인자가 있으면 ()는 유지하고 마지막 클로저만 밖으로.
가독성 때문에 Swift에서 권장되는 표기.

단축인자 (shorthand argument name)

let names = ["C", "A", "B"]

// 풀 버전
let s1 = names.sorted { (a: String, b: String) -> Bool in return a < b }
print(s1) // 출력: ["A", "B", "C"]

// 단축 인자($0, $1) 사용
let s2 = names.sorted { $0 < $1 }
print(s2) // 출력: ["A", "B", "C"]

// 더 축약: 비교 연산자 자체를 넘김
let s3 = names.sorted(by: <)
print(s3) // 출력: ["A", "B", "C"]

// 내림차순 예시
let s4 = names.sorted(by: >)
print(s4) // 출력: ["C", "B", "A"]

sorted는 정렬 기준을 클로저로 받는다.
$0, $1은 클로저의 첫 번째·두 번째 매개변수를 자동 참조한다.
비교만 하면 될 때는 { $0 < $1 } 대신 < 혹은 >를 바로 넘기면 된다.

Swift 클래스 선언

class Person {
    var age: Int = 0                      // stored
    var nextAge: Int { age + 1 }          // computed
    static var species = "Human"          // type property

    func birthday() { age += 1 }          // 인스턴스 메서드
    class func make() -> Person { Person() } // 타입 메서드(오버라이드 가능)
}

let p = Person()
p.age = 42
print(p.nextAge)   // 출력: 43
p.birthday()
print(p.age)       // 출력: 43
print(Person.species) // 출력: Human

클래스는 class 이름: 부모 { 프로퍼티와 메서드 } 형태로 선언한다.
프로퍼티는 저장(stored)·계산(computed)·타입(type)으로 구분한다.
저장 프로퍼티는 값을 보관하고, 계산 프로퍼티는 get/set으로 값을 계산한다.
타입 프로퍼티는 static/class로 선언하며 모든 인스턴스가 공유한다.
메서드는 인스턴스 메서드(객체가 호출, 상태 변경 가능)와 타입 메서드(static func/class func, 타입이 호출)로 나뉜다.

저장 프로퍼티

stored property(age, weight)를 기본값 없이 선언했는데, 클래스에는 이를 채워줄 지정 이니셜라이저가 없어서 컴파일러가 init()을 만들어 줄 수 없다

오류 해결 방법

// 1) 지정 이니셜라이저 제공
class Man {
    var age: Int
    var weight: Double
    init(age: Int, weight: Double) {
        self.age = age
        self.weight = weight
    }
}
// 2) 기본값 부여
class Man {
    var age = 0
    var weight = 0.0
}
// 3) 옵션이 유효한 모델이면 Optional로
class Man {
    var age: Int?
    var weight: Double?
}

기본 생성자로 객체를 만들고 인스턴스 메서드를 호출하는 Swift 클래스 예제

class Man {
    var age: Int = 1
    var weight: Double = 3.5

    init() {}                 // 기본 생성자(파라미터 없음)

    func display() {          // 인스턴스 메서드
        print("나이=\(age), 몸무게=\(weight)")
    }
}

var x: Int = 10

let kim: Man = Man()          // 생성자 호출 → 인스턴스 생성
print(kim.age)                // 출력: 1
kim.display()                 // 출력: 나이=1, 몸무게=3.5

Man()은 생성자를 호출한다. 메모리를 할당하고 age=1, weight=3.5로 초기화된 인스턴스를 만든다.
display는 인스턴스 메서드이다. 사용하려면 객체를 먼저 만든다.
init(){}를 명시했으므로 매개변수 없는 생성자가 존재한다. 속성에 기본값이 있어 바로 사용한다.
따라서 kim에 Man()으로 새 객체를 할당하고, 이후 kim.age와 kim.display()를 호출한다.