카테고리 없음

iOS 5주차

늠재 2025. 9. 29. 15:37

iPhone 에뮬레이터 실행화면

 

AI에게 AppDelegate 설명을 부탁했다.

 

Swift의 AppDelegate 코드 설명해줄게!

이 코드는 iOS 앱의 진입점 역할을 하는 AppDelegate 클래스를 정의한 거야. 주요 내용을 설명해 볼게:

1. 기본 구조

swift

@main
class AppDelegate: UIResponder, UIApplicationDelegate {
    // 메소드들...
}
  • @main: 앱의 시작점을 지정하는 Swift 5.3부터 도입된 어트리뷰트. 이전에는 @UIApplicationMain을 사용했어.
  • UIResponder 상속: 이벤트 응답 체인의 일부가 되게 해줌
  • UIApplicationDelegate 프로토콜: 앱의 생명주기와 관련된 메소드 구현을 요구함

2. 앱 시작 메소드

swift

func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
    // 앱 시작 시 커스터마이징할 수 있는 부분
    return true
}
  • 앱이 처음 실행될 때 호출됨
  • 여기서 앱의 초기 설정을 수행할 수 있음 (데이터베이스 설정, 서비스 초기화 등)
  • true 반환은 앱이 정상적으로 시작됐음을 의미

3. Scene 관련 메소드

swift

func application(_ application: UIApplication, configurationForConnecting connectingSceneSession: UISceneSession, options: UIScene.ConnectionOptions) -> UISceneConfiguration {
    return UISceneConfiguration(name: "Default Configuration", sessionRole: connectingSceneSession.role)
}
  • iOS 13부터 도입된 멀티윈도우 지원을 위한 Scene 관련 메소드
  • 새 Scene(화면)이 생성될 때 어떤 설정을 사용할지 지정

swift

func application(_ application: UIApplication, didDiscardSceneSessions sceneSessions: Set<UISceneSession>) {
    // 사용자가 Scene을 버렸을 때 호출됨
}
  • 사용자가 멀티태스킹 UI에서 앱 화면을 닫았을 때 호출됨
  • 해당 Scene과 관련된 리소스를 정리하는 코드를 여기에 작성하면 돼

MARK 주석

swift

// MARK: UISceneSession Lifecycle
  • 코드 구조화를 위한 주석으로, Xcode의 점프 바에서 섹션으로 표시됨
  • 코드 가독성을 높이는 Swift 개발 관행이야

SceneDelegate 코드도 질문해보았다.

 

SceneDelegate 코드도 설명해줄게!

이 파일은 iOS 13부터 도입된 멀티윈도우 지원을 위한 핵심 클래스야. AppDelegate와 함께 앱의 생명주기를 관리하는 중요한 역할을 해.

swift

class SceneDelegate: UIResponder, UIWindowSceneDelegate {
    var window: UIWindow?
    // 메소드들...
}

주요 메소드들을 하나씩 살펴보자:

1. scene(_:willConnectTo:options:)

swift

func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
    guard let _ = (scene as? UIWindowScene) else { return }
}
  • Scene이 처음 생성될 때 호출돼
  • 여기서 window를 설정하고 앱의 UI를 초기화하는 코드를 작성해
  • guard let 구문은 scene이 UIWindowScene 타입인지 확인하고, 아니면 함수를 종료시킴

2. sceneDidDisconnect(_:)

swift

func sceneDidDisconnect(_ scene: UIScene) {
    // Scene이 시스템에 의해 해제될 때 호출됨
}
  • Scene이 백그라운드로 이동하거나 세션이 버려질 때 호출됨
  • 리소스 정리 작업을 여기서 수행하면 돼

3. sceneDidBecomeActive(_:)

swift

func sceneDidBecomeActive(_ scene: UIScene) {
    // Scene이 비활성 상태에서 활성 상태로 전환될 때 호출
}
  • 앱이 포그라운드에 오고 사용자와 상호작용할 준비가 됐을 때 호출
  • 일시 중지됐던 작업들을 다시 시작하기 좋은 시점

4. sceneWillResignActive(_:)

swift

func sceneWillResignActive(_ scene: UIScene) {
    // Scene이 활성 상태에서 비활성 상태로 전환될 때 호출
}
  • 전화가 오거나 다른 앱으로 전환될 때 같은 일시적인 중단이 발생할 때 호출됨
  • 중요 데이터를 저장하거나 UI 업데이트를 일시 중지하는 코드를 넣으면 좋아

5. sceneWillEnterForeground(_:)

swift

func sceneWillEnterForeground(_ scene: UIScene) {
    // 백그라운드에서 포그라운드로 전환될 때 호출
}
  • 앱이 다시 화면에 표시될 때 호출
  • 백그라운드에서 변경된 설정을 되돌리는 코드를 넣기 좋은 곳

6. sceneDidEnterBackground(_:)

swift

func sceneDidEnterBackground(_ scene: UIScene) {
    // 포그라운드에서 백그라운드로 전환될 때 호출
}
  • 앱이 화면에서 사라질 때 호출
  • 데이터 저장, 공유 리소스 해제 등의 작업을 수행하기 좋은 시점

iOS 13 이전에는 AppDelegate에서 모든 생명주기 관리를 했지만, 이제는 SceneDelegate가 UI와 관련된 생명주기를, AppDelegate는 앱 전체의 생명주기를 관리하는 구조로 나뉘어졌어. 멀티윈도우 지원이 필요한 아이패드 앱이나 Mac Catalyst 앱에서 특히 중요한 파일이지!

 

ViewController 코드도 설명해줄게!

swift

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }
}

이건 iOS 앱의 화면을 담당하는 가장 기본적인 컨트롤러 클래스야. 엄청 심플하게 기본 구조만 갖춘 상태인데:

  1. UIViewController 클래스를 상속받아서 만들어진 커스텀 뷰 컨트롤러야.
  2. viewDidLoad() 메소드는 뷰가 메모리에 로드된 후에 호출되는 메소드로, 화면 초기화 작업을 여기서 많이 해.
  3. super.viewDidLoad()는 부모 클래스(UIViewController)의 viewDidLoad 메소드를 먼저 실행시켜주는 부분이야.

지금은 아무 기능도 없는 빈 화면 상태인데, 여기에 UI 요소들(버튼, 레이블 등)을 추가하고 액션을 연결해서 실제 기능을 구현하게 될 거야. 앱 개발 시작할 때 Xcode가 기본으로 생성해주는 템플릿 코드라고 보면 돼!

 

함수의 타입 (자료형,자료형,…) -> 리턴형 (Int, Int) -> Int 리턴형이 Void형이면 ()

함수명 (외부매개변수명: 외부매개변수명: ...)

BMI 계산기

import Foundation

//let weight = 60.0

//let height = 170.0

func calcBMI(weight: Double, height: Double) -> String{

    let bmi = weight / (height*height*0.0001) // kg/m*m

    let shortendBmi = String(format: "%.1f", bmi)

    var body = ""

    if bmi >= 40 {

        body = "3단계 비만"

    } else if bmi >= 30 && bmi < 40 {

        body = "2단계 비만"

    } else if bmi >= 25 && bmi < 30 {

        body = "1단계 비만"

    } else if bmi >= 18.5 && bmi < 25 {

        body = "정상"

    } else {

        body = "저체중"

    }

    return "BMI:\(bmi), 판정:\(body)"

}

print(calcBMI(weight: 60.0, height:170.0))

 

switch case 문으로도 변환해보기

import Foundation

func calcBMI(weight : Double, height : Double) -> String {

    let bmi = weight / (height*height*0.0001) // kg/m*m

    let shortenedBmi = String(format: "%.1f", bmi)

    var body = ""

    switch bmi {

    case 0.0..<18.5:

        body = "저체중"

    case 18.5..<25.0:

        body = "정상"

    case 25.0..<30.0:

        body = "1단계 비만"

    case 30.0..<40.0 :

        body = "2단계 비만"

    default :

        body = "3단계 비만"

    }

    return "BMI:\(shortenedBmi), 판정:\(body)"

}

print(calcBMI(weight:69.5, height: 183.0))

 

1급 객체와 1급 시민

 

 

 

toUp의 자료형

swift는 변수나 상수에 함수를 집어넣을 수 있다.

 

// 정수를 받아서 1을 더한 값을 반환하는 함수 선언
func up(num: Int) -> Int {
    return num + 1
}

// 정수를 받아서 1을 뺀 값을 반환하는 함수 선언
func down(num: Int) -> Int {
    return num - 1
}

// 함수를 상수에 할당 - 이게 일급 객체의 핵심 특징!
// 함수가 값처럼 다뤄질 수 있어서 변수나 상수에 저장 가능
let toUp = up 

// 원래 함수를 직접 호출 (argument label 'num:' 사용)
print(up(num:10))  // 결과: 11

// 함수를 저장한 상수를 통해 함수 호출 (argument label 생략)
// 일급 객체로서 함수를 변수에 저장하면 호출 시 파라미터 레이블을 생략해야 함
print(toUp(10))    // 결과: 11

// 또 다른 함수를 상수에 할당 (일급 객체 활용)
let toDown = down

// 원래 함수 호출 (실행만 하고 결과는 출력 안 함)
down(num:10)       // 결과 값 9는 사용되지 않음

// 원래 함수 호출 결과 출력
print(down(num:10))  // 결과: 9

// 함수를 저장한 상수를 통해 호출 (argument label 생략)
print(toDown(8))     // 결과: 7

 

 

func upDown(Fun: (Int) -> Int, value: Int) {

let result = Fun(value)

print("결과 = \(result)")

}

print(type(of: upDown)) // 자료형 : ((Int) -> Int, Int) -> ()

upDown(Fun:toUp, value: 3)

upDown(Fun:toUp, value: 10) //toUp(10)

upDown(Fun:toDown, value: 10) //toDown(10)

 

((Int) -> Int, Int) -> () AI에게 물어보니,

  1. (Int) -> Int: 이건 정수를 받아서 정수를 반환하는 함수 타입이야. 예를 들면 아까 봤던 up이나 down 함수 같은 거지!
  2. Int: 그냥 정수 하나
  3. ((Int) -> Int, Int): 이 부분은 매개변수 목록을 나타내는데, 두 개의 매개변수를 받는다는 뜻이야.
    • 첫 번째 매개변수: (Int) -> Int 타입의 함수
    • 두 번째 매개변수: Int 타입의 정수
  4. -> (): 이건 반환 타입이 Void라는 뜻으로, 아무것도 반환하지 않는다는 의미야.

(Int) -> Int를 한 묶음으로 보면 반환형이 잘 보인다.

함수가 함수를 리턴하는 경우가 나오게 된다.

AI의 설명

 

// 불리언 값에 따라 다른 함수를 반환하는 고차 함수
func decideFun(x: Bool) -> (Int) -> Int {
    // 매개변수는 Bool, 반환 타입은 '정수를 받아 정수를 반환하는 함수'
    if x {
        return toUp  // true면 toUp 함수를 반환 (숫자를 1 증가시키는 함수)
    } else {
        return toDown  // false면 toDown 함수를 반환 (숫자를 1 감소시키는 함수)
    }
}

// decideFun 함수를 호출하고 결과 함수를 r에 저장
// x에 false를 전달했으므로 toDown 함수가 반환됨
let r = decideFun(x: false)  // 실질적으로 let r = toDown과 같음

// r의 타입 출력 - 함수 타입인 (Int) -> Int 출력됨
print(type(of: r))  // "(Int) -> Int" 출력

// r 함수 실행 (실제로는 toDown 함수 실행)
// 10에서 1을 뺀 9가 출력됨
print(r(10))  // toDown(10)과 같으므로 9 출력

 

🔹 고차함수 활용도가 높은 순

  1. Haskell
    • 순수 함수형 언어 → 모든 함수가 1급 객체, 고차 함수 사용이 기본
    • map, filter, fold 등 고차 함수가 필수적임
  2. Scala
    • 함수형 + 객체지향 혼합 언어
    • 컬렉션 처리, 익명 함수(람다), 고차 함수 활용 빈도가 매우 높음
  3. Clojure
    • Lisp 계열 함수형 언어
    • 함수가 1급 객체이고 컬렉션 처리 시 고차 함수를 적극적으로 사용
  4. F#
    • .NET 기반 함수형 언어
    • 고차 함수와 패턴 매칭을 중심으로 프로그래밍
  5. JavaScript (ES6 이상)
    • 함수가 1급 객체
    • map, filter, reduce, 클로저 등에서 고차 함수 활용 빈도 높음
    • 특히 프론트엔드에서 콜백, 이벤트 처리 등에서도 자주 사용
  6. Swift
    • 함수가 1급 객체, map, filter, reduce 등의 고차 함수 제공
    • UI나 데이터 처리에서 람다/클로저 활용 많음
  7. Python
    • map, filter, reduce, sorted 등 고차 함수 제공
    • 전통적 Python 스타일에서는 반복문(for) 사용 비중이 커서, 순수 FP보다는 보조적 사용
  8. Java (8 이상)
    • 람다와 Stream API 도입 이후 고차 함수 활용 가능
    • 하지만 기존 Java 코드 스타일에서 반복문 위주라, 고차 함수 사용 빈도는 상대적으로 낮음
  9. C++
    • C++11 이상에서 람다 도입, std::function, 알고리즘(std::transform) 지원
    • 하지만 여전히 전통적 반복문 위주로 작성되는 경우가 많음

🔹 요약

  • FP 중심 언어 → 고차 함수 필수적 (Haskell, Scala, Clojure, F#)
  • 멀티 패러다임 언어 → 필요에 따라 자주 사용 (JavaScript, Swift, Python)
  • 주로 절차/객체지향 언어 → 고차 함수 사용 빈도 낮음 (Java, C++)

 클로저

 특정 작업(함수)과 그 작업이 일어난 곳(환경 또는 상태)을 모두 기억하고 있는 도구

 Swift에서는 일반적으로 클로저 표현식을 클로저로 표현

 익명함수 또는 독립적인 코드 블록으로 클로저의 하위 개념

 

AI 답변

클로저는 그냥 '이름 없는 함수'라고 생각하면 돼. 코드 블록을 변수처럼 저장하고 전달할 수 있게 해주는 거야. 아까 우리가 본 일급 객체 개념이랑 연결되는 부분이지!

기본 문법은 이렇게 생겼어:

swift

{ (매개변수) -> 반환타입 in
    실행 코드
}

실제 예시로 보면 더 쉽게 이해될 거야:

swift

// 일반 함수
func add(a: Int, b: Int) -> Int {
    return a + b
}

// 같은 기능의 클로저
let addClosure = { (a: Int, b: Int) -> Int in
    return a + b
}

print(addClosure(5, 3)) // 8

근데 Swift는 클로저 문법을 엄청 간결하게 만들 수 있어서 실제로는 이렇게 많이 써:

swift

// 타입 추론으로 더 간결하게
let addClosure = { (a, b) in a + b }

// 배열에서 사용하는 예시
let numbers = [1, 2, 3, 4, 5]
let doubled = numbers.map { $0 * 2 } // [2, 4, 6, 8, 10]

클로저의 진짜 장점은 주변 변수를 "캡처"해서 나중에도 사용할 수 있다는 건데, 이게 바로 "클로저(closure)"라는 이름의 유래야. 주변 환경을 "닫아서(close)" 가지고 다닌다는 의미지!

 

func add(x: Int, y: Int) -> Int {

    return x+y

}

//print(add(x:10, y:20))

 

let add1 = { (x: Int, y: Int) -> Int in

    return x+y

}

print(add1(10,15)) //25

 

바꾸는 방법 내 말로 정리

// 1. 함수 이름, 선언구 다 지우고

// 2. 중괄호 맨 앞으로 옮기고

// 3. 자료형 뒤에 in이라고 해서 클로저인거 알려주고

// 4. 상수 하나 만들어서 그 안에 집어넣어주기

 

클래스에서 만들어진 실체를 객체라 하고, 객체들 중 실제로 사용중인 객체를 인스턴스라고 한다.

영어권에서는 객체들을 모두 인스턴스라고만 하지 object라는 단어를 사용하진 않는다.

Swift에서는 클래스랑 구조체가 얼핏 보면 비슷해 보이는데, 결정적인 차이점이 딱 있어! 바로 **값 타입(Value Type)**이냐 **참조 타입(Reference Type)**이냐의 차이야. 이게 사용하는 방식에 엄청난 영향을 줘.

쉽게 비유하자면,

  • 구조체 (Struct) = 복사본 (값 타입)
    • 카드를 복사해서 주면, 내 카드랑 네 카드랑 따로따로 관리돼. 네 카드를 막 칠해도 내 카드는 멀쩡!
  • 클래스 (Class) = 원본 리모컨 (참조 타입)
    • TV 리모컨을 주면, 네가 그 리모컨으로 채널을 바꾸면 내 TV 채널도 바뀌어! 둘 다 같은 TV를 조작하는 거지.

그럼 이제 이걸 iOS 앱 개발에서 어떻게 쓰는지 알려줄게!

1. 클래스 (Class) 활용

언제 쓰냐?

  • 상속이 필요할 때: 어떤 객체가 다른 객체의 특성을 물려받아야 할 때. UIViewController나 UIView처럼 애플 프레임워크의 기본 구성 요소들은 대부분 클래스로 되어있고, 이걸 상속받아서 내 화면이나 UI 요소를 만드는 경우가 많아.
    • class MyViewController: UIViewController { ... }
  • 하나의 인스턴스를 여러 곳에서 공유하고 싶을 때: 앱 전체에서 설정이나 데이터를 공유하는 싱글톤 객체(예: UserDefaults.standard)나, 공통된 네트워크 매니저 같은 객체를 만들 때 사용해.
    • 예: 앱 전체의 사용자 정보를 담는 UserManager 클래스
  • 인스턴스의 생명 주기가 길고 복잡할 때: 메모리에 오랫동안 유지되어야 하고, 다양한 상태 변화를 가지는 객체들 (예: 앱 델리게이트, 씬 델리게이트)
  • Objective-C와의 상호 운용성: Objective-C 코드를 써야 한다면 클래스를 써야 해.

장점: 상속을 통한 확장성, 타입 캐스팅(어떤 타입인지 확인하고 변환), ARC(자동 참조 카운팅)를 통한 메모리 관리.

2. 구조체 (Struct) 활용

언제 쓰냐?

  • 간단한 데이터 묶음: 좌표(CGPoint), 크기(CGSize), 색상(UIColor도 내부적으로는 구조체 기반), 간단한 사용자 정보 (User 모델) 등 '값'을 표현할 때 주로 써.
    • struct User { var name: String; var age: Int }
  • 복사 시 독립적인 객체가 필요할 때: 어떤 데이터를 복사해서 넘겨줬을 때, 원본 데이터에 영향을 주지 않고 복사본만 따로 수정하고 싶을 때. Swift의 기본 타입들(Int, String, Array, Dictionary 등)은 모두 구조체라서 값 타입으로 동작해!
  • 생명 주기가 짧고 가벼운 객체: 함수 내에서 잠깐 사용하고 버려지는 객체들. 스택 영역에 할당돼서 메모리 관리가 클래스보다 효율적이야.
  • Codable 프로토콜을 이용한 데이터 모델링: 서버에서 JSON 데이터를 받아올 때, Swift에서 이 JSON을 파싱해서 Swift 객체로 만들 때 구조체를 엄청 많이 써.
    • struct Product: Codable { var id: String; var name: String; var price: Int }

장점: 값 타입이라 데이터 변경 시 안전함(예상치 못한 사이드 이펙트 방지), 메모리 효율성, 스레드 안전성(멀티스레딩 환경에서 공유 문제 발생 X).

⭐️ iOS 개발 팁 (언제 뭘 쓸지 고민될 때)

Swift는 기본적으로 구조체를 더 선호하는 경향이 있어! 애플 공식 문서에서도 대부분의 경우 구조체를 사용하는 것을 권장하거든.

  1. 일단 구조체를 먼저 고려해 봐.
  2. 클래스가 꼭 필요한 경우에만 클래스를 써. (상속, 참조 의미가 중요한 경우, Objective-C 호환 등)
    • 클래스가 필요한 조건:
      • 다른 클래스의 기능을 물려받아야 할 때 (상속).
      • 특정 타입의 객체를 여러 곳에서 하나의 인스턴스로 공유하며 상태를 변경해야 할 때.
      • ARC가 관리하는 메모리 힙에 저장되어야 할 때.

class Man {

    var age : Int

    var weight : Double

}

이 코드를 바로 Xcode에 넣으면 에러가 날 거야. 왜냐면 Swift에서는 클래스의 모든 저장 프로퍼티(age, weight)는 초기값이 있거나, 아니면 **초기화 구문(init())**을 통해 반드시 초기화되어야 하거든. 😬

지금처럼 그냥 선언만 해두면 Man 클래스로 객체(인스턴스)를 만들 때 age랑 weight에 뭐가 들어갈지 모르니까 컴파일러가 "야 이거 초기화 안 됐잖아!" 하고 에러를 뿜는 거지.

오류 해결 방법:

  1. 초기값 지정: //stored property는 초기값이 있어야 함                                                                                                                  선언과 동시에 초기값을 줘버리는 방법.이러면 인스턴스 만들 때 let someMan = Man()처럼 기본값으로 바로 만들 수 있어.
  2. class Man { var age: Int = 0 // 초기값 0 var weight: Double = 0.0 // 초기값 0.0 }
  3. 초기화 구문(init()) 추가: 인스턴스 생성 시점에 값을 받아서 초기화하는 방법. 이 방식이 훨씬 일반적이야.이렇게 해두면 let 늠재 = Man(age: 23, weight: 65.5) 이런 식으로 인스턴스를 만들 수 있게 돼!
  4. class Man { var age: Int var weight: Double // 초기화 구문(initializer) 추가 init(age: Int, weight: Double) { self.age = age // 전달받은 age 값으로 프로퍼티 초기화 self.weight = weight // 전달받은 weight 값으로 프로퍼티 초기화 } }

그래서 이 Man 클래스는 뭐냐?

Man이라는 '설계도' 또는 '청사진'을 만든 거야. 이 설계도를 가지고 실제 사람(Man의 인스턴스) 객체들을 만들 수 있어. 각 객체는 고유한 age와 weight 값을 가질 수 있겠지!

 

nil 이용하기

class Man {

    var age : Int?

    var weight : Double?

}

초기값을 주어도 되고 initializer를 주어도 되는데 둘 다 해도 안될 건 없지만 굳이 둘 다 안해도 됨

메소드

class Man{

    var age : Int = 1

    var weight : Double = 3.5

    func display(){ //인스턴스 메서드

        print("나이=\(age), 몸무게=\(weight)")

    }

}

var x : Int = 10

var kim : Man = Man() // 자동적으로 이니셜라이저가 눈에 안보이게 생성된다.

// 이니셜라이저에 한해서 .Init()을 생략할 수 있다. 자바에서 호출할 때도 같은 개념이다.

print(kim.age)

print(kim.weight)

kim.display()

 

최종적으로 클래스 호출할 땐 var kim = Man()으로 줄여쓸 수 있다.