본문 바로가기

스폰지밥으로 공부하는 swift/swift 문법

앱개발 심화 키워드 요약

728x90

1-1. 프로퍼티 옵저버

값이 변경될 때 특정 동작을 수행함

 

프로퍼티 옵저버를 활용하면 프로퍼티의 값이 변경되기 전 특정 코드를 실행(willSet)하거나 값이 변경된 후(didSet)에 다른 코드를 실행할 수 있습니다! 이를 활용해서 속성의 변경 사항을 감지하고 추가 동작을 수행할 수 있어요.

class Order {
    var quantity: Int = 0 {
        didSet {
            updateStatistics()
        }
    }
    
    func updateStatistics() {
        // 주문 수량에 따른 통계 업데이트 로직
        print("주문 수량이 변경되었습니다. 통계를 업데이트합니다.")
    }
}

let spongebob = Order()
spongebob.quantity = 10 // 주문 수량이 변경됨

1-2. 타입캐스팅

인스턴스의 실제 타입을 확인하거나 해당 타입으로 변환해주는 것

 

타입 캐스팅은 런타임에 동적으로 타입을 확인하고 조건에 따라 다른 동작을 수행할 수 있게 해줍니다. 다양한 타입을 다룰 때 유용하며, 안전한 형 변환을 지원합니다.

// 집게사장이 스펀지밥과 뚱이 중 어떤 캐릭터가 주문을 처리하는지 확인

class Employee {}

class Spongebob: Employee {
    func takeOrder() {
        print("스펀지밥이 주문을 처리합니다.")
    }
}

class Patrick: Employee {
    func takeOrder() {
        print("뚱이가 주문을 처리합니다.")
    }
}

let employee: Employee = Spongebob()

if let spongebob = employee as? Spongebob {
    spongebob.takeOrder() // 스펀지밥이 주문을 처리
} else if let patrick = employee as? Patrick {
    patrick.takeOrder()
}
  • 다운 캐스팅 왜 / 어떨 때 써야됨?

1-3. 접근제한자

클래스, 구조체, 열거형, 프로퍼티, 메서드 등의 접근 범위를 제한해줌

 

접근 제한자를 사용하여 내부 구현을 숨기고 필요한 인터페이스만 노출시키는 등의 캡슐화(도어락같은 느낌)를 구현할 수 있습니다. 이를 통해 코드의 안정성과 유지 보수성을 높일 수 있습니다.

친구 집 비밀번호

class Cashier {
    private var orderCount: Int = 0
    
    func takeOrder() {
        orderCount += 1
        print("주문을 받았습니다.")
    }
}

class Spongebob {
    private let cashier = Cashier()
    
    func placeOrder() {
        cashier.takeOrder() // 다른 클래스에서는 직접 호출 불가능
    }
}

let spongebob = Spongebob()
spongebob.placeOrder() // 주문을 받았습니다.

1-4. 클로저

일급 객체로서, 코드 블록을 변수나 상수에 저장하고 전달할 수 있는 기능

 

주로 비동기 작업, 콜백 함수, 정렬 등에 사용됩니다. 클로저를 사용하면 코드의 재사용성과 가독성을 높일 수 있으며, 간결한 문법으로 작성할 수 있습니다.

 

  • 여기서 잠시, 일급객체란?
    일급객체는 프로그래밍 언어에서 변수나 데이터 구조에 할당이 가능하고, 함수의 매개변수로 전달되고, 반환값으로 사용할 수 있는 객체를 말해요.

    보다 직관적으로 이해하기 위해 외교관이라고 생각해 보시면 될 것 같습니다. 외교관은 어느 나라든 자유롭게 다닐 수 있고, 중앙선 한가운데 차를 세워놔도 함부러 견인을 할 수 없는 것 처럼 말이에요.만약 일급객체가 아니라면, 변수에 할당하거나 함수의 매개변수로 전달하는 등의 행위가 제한됩니다. 이는 코드를 유연하게 작성하는 데 어려움을 줄 수 있습니다. 일급객체를 사용하면 코드의 유지보수가 쉬워지며, 함수형 프로그래밍의 장점을 살릴 수 있습니다.

스펀지밥을 다시 불러 볼게요!

  • 스펀지밥이 일급시민이라면, 그는 어디서든 일할 수 있고 집게사장(함수)에게 전달되어 어떤 업무를 할지 결정할 수 있습니다.이런식으로, 함수를 변수에 할당하고 다른 함수에 전달하여 사용함으로써 코드의 유연성과 가독성을 높일 수 있어요.
// 스펀지밥을 일급객체로 활용
func doWork(worker: () -> Void) {
    print("일 시작 전에 노래 한 곡!")
    worker() // 전달된 일급시민(함수)이 업무를 수행
    print("일 끝나고 퇴근!")
}

// 스펀지밥의 업무 함수
func makeBurger() {
    print("스펀지밥: 햄버거 만들기 시작!")
}

// 일급시민(함수)를 전달하여 업무 수행
doWork(worker: makeBurger)
// 클로저를 사용하여 주문이 완료되면 스펀지밥이 고객에게 알림

class Spongebob {
    var notifyCustomer: (() -> Void)?
    
    func placeOrder() {
        // 주문 처리 로직...
        notifyCustomer?()
    }
}

let spongebob = Spongebob()
spongebob.notifyCustomer = {
    print("주문이 완료되었습니다. 고객에게 알림을 보냅니다.")
}

spongebob.placeOrder() // 주문완료 알림 전송

사용법 좀 자세히

1-5. 고차함수

다른 함수를 매개변수로 받거나 함수를 반환하는 함수

 

고차함수를 사용하면 반복문을 직접 작성하지 않고도 간결하게 데이터를 처리할 수 있습니다. 주요 고차함수로는 map, filter, reduce 등이 있으며, 함수형 프로그래밍의 특징을 지원합니다. 알고리즘 풀이 보다보면 자주 보실거에요!

struct Order {
    let name: String
    let quantity: Int
}

let orders = [
    Order(name: "햄버거", quantity: 2),
    Order(name: "피자", quantity: 1),
    Order(name: "스테이크", quantity: 3),
    Order(name: "샐러드", quantity: 0)
]

var filteredOrders: [Order] = []

for order in orders {
    if order.quantity > 0 {
        filteredOrders.append(order)
    }
}

for order in filteredOrders {
    print(order.name) // 햄버거, 피자, 스테이크
}
struct Order {
    let name: String
    let quantity: Int
}

let orders = [
    Order(name: "햄버거", quantity: 2),
    Order(name: "피자", quantity: 1),
    Order(name: "스테이크", quantity: 3),
    Order(name: "샐러드", quantity: 0)
]

let filteredOrders = orders.filter { order in // for문 사용하지 않고 간단히!!
    order.quantity > 0
}

for order in filteredOrders {
    print(order.name) // 햄버거, 피자, 스테이크
}

1-6. 예외처리

프로그램 실행 중 발생할 수 있는 예외 상황에 대한 처리 방법

 

예외를 미리 예측하고 처리함으로써 프로그램의 안정성을 높일 수 있습니다. 예외를 처리하는 방법으로는 do-catch문이 주로 사용되며, 오류 정보를 전달하고 복구할 수 있습니다.

enum OrderError: Error {
    case outOfStock // 품절 ㅜㅠ
    case invalidQuantity // 게살버거 10000개요...?
}

func placeOrder(quantity: Int) throws {
    guard quantity > 30 else {
        throw OrderError.invalidQuantity
    }
    
    // 재고 확인 로직...
    
    if isOutOfStock {
        throw OrderError.outOfStock
    }
    
    // 주문 처리 로직...
}

do {
    try placeOrder(quantity: 3)
    print("주문이 완료되었습니다.")
} catch OrderError.outOfStock {
    print("주문할 상품이 품절되었습니다.")
} catch OrderError.invalidQuantity {
    print("유효하지 않은 주문 수량입니다.")
} catch {
    print("알 수 없는 오류가 발생했습니다.")
}

1-7. ARC

ARC(Automatic Reference Counting)는 메모리 관리 기법

 

객체가 더 이상 사용되지 않을 때 자동으로 메모리에서 해제됩니다. 개발자가 명시적으로 메모리 관리를 신경쓰지 않아도 되므로 메모리 관리 부담을 줄여줍니다.

// 스펀지밥이 집게리아에서 주문을 처리하는 동안 메모리에서 해제되지 않도록 함

class Order {
    let customer: String
    
    init(customer: String) {
        self.customer = customer
        print("\\(customer)님의 주문이 생성되었습니다.")
    }
    
    deinit { // 이런 식으로 해제 시켜 줄 수 있음
        print("\\(customer)님의 주문이 해제되었습니다.")
    }
}

func placeOrder() {
    let order = Order(customer: "스폰지밥")
    // 주문 처리 로직...
}

placeOrder() // 스폰지밥님의 주문이 생성되었습니다. 스폰지밥님의 주문이 해제되었습니다.

1-8. 프로토콜

인터페이스(계약)를 정의하기 위한 일종의 명세서

 

프로토콜을 채택하는 타입은 프로토콜에서 요구한 속성과 메서드를 구현해야 합니다. 다중 상속과 비슷한 효과를 낼 수 있으며, 코드의 재사용성과 확장성을 높일 수 있습니다.

protocol OrderHandler {
    func takeOrder()
    func prepareOrder()
    func serveOrder()
}

class Spongebob: OrderHandler {
    func takeOrder() {
        print("스펀지밥이 주문을 받습니다.")
    }
    
    func prepareOrder() {
        print("스펀지밥이 주문을 준비합니다.")
    }
    
    func serveOrder() {
        print("스펀지밥이 주문을 서빙합니다.")
    }
}

class Krabs: OrderHandler {
    func takeOrder() {
        print("집게사장이 주문을 받습니다.")
    }
    
    func prepareOrder() {
        print("집게사장이 주문을 준비합니다.")
    }
    
    func serveOrder() {
        print("집게사장이 주문을 서빙합니다.")
    }
}

let spongebob = Spongebob()
spongebob.takeOrder() // 스펀지밥이 주문을 받습니다.

1-9. 확장

기존의 클래스, 구조체, 열거형 등에 새로운 기능을 추가하는 것

 

확장을 통해 기존 타입의 기능을 확장하거나 프로토콜을 채택한 타입에 기능을 추가할 수 있습니다. 이를 통해 기존 코드를 변경하지 않고도 타입을 확장할 수 있습니다.

class AViewConotroller: UIViewController() {
	override func viewDidLoad() {
		getDeviceWidth() // 함수 호출부
	}

	func getDeviceWidth() -> CGFloat {
	        return UIScreen.main.bounds.width
	    }
}

class BViewController: UIViewController() {
	override func viewDidLoad() {
		getDeviceWidth() // 함수 호출부
	}

	func getDeviceWidth() -> CGFloat {
	        return UIScreen.main.bounds.width
	}
}

getDeviceWidth()라는 메서드가 중복으로 선언이 되어있죠? 대부분의 개발자는 이러한 중복코드를 줄이고 싶어하는데요, 이렇게 선언해두면 중복으로 메서드 구현을 할 필요 없이 함수만 호출해서 사용 할 수 있습니다 :)

extension UIViewController {
	func getDeviceWidth() -> CGFloat {
	        return UIScreen.main.bounds.width
	}
}
class AViewConotroller() {
	override func viewDidLoad() {
		getDeviceWidth() // 함수 호출부
	}
}

class BViewConotroller() {
	override func viewDidLoad() {
		getDeviceWidth() // 함수 호출부
	}
}

1-10. 제네릭

타입에 의존하지 않고 일반화된 코드를 작성하는 기능.

 

제네릭을 사용하면 다양한 타입에 대해 동일한 코드를 작성할 수 있으며, 타입 안정성을 보장합니다. 컬렉션, 함수, 구조체 등 다양한 곳에서 활용됩니다.

struct Order<T> {
    let name: T
    
    init(name: T) {
        self.name = name
    }
}

let burgerOrder = Order(name: "햄버거")
let pizzaOrderCount = Order(name: 12)
let steakOrderPrice = Order(name: 18.9)

print(burgerOrder.name) // "햄버거"
print(pizzaOrderCount.name) // 12
print(steakOrderPrice.name) // 3.14

Any랑 다른 점

1-11. 비동기와 네트워킹

작업을 순차적으로 실행하지 않고 병렬로 처리하는 기법

 

비동기 작업은 네트워킹과 밀접한 관련이 있으며, 네트워크 요청과 응답을 비동기적으로 처리할 수 있습니다. 비동기와 네트워킹을 통해 응답성과 성능을 향상시킬 수 있습니다.

 

func orderFoodSync() {
    let pizza = orderPizza()  // 여기서 주문하고 기다림
    let chicken = orderChicken()
    let tteokbokki = orderTteokbokki()
    
    // 주문한 음식이 도착하면 처리
    eat(pizza)
    eat(chicken)
    eat(tteokbokki)
}

위 예시처럼 배달의 민족에서 다양한 음식(피자, 치킨, 엽떡 등)을 시킬 때 음식이 배달 된 이후 음식을 시키는 것이 동기 방식이고

func orderFoodAsync() {
    let dispatchGroup = DispatchGroup()

    var pizza: Food?
    var chicken: Food?
    var tteokbokki: Food?

    // 각 음식을 동시에 주문
    DispatchQueue.global().async(group: dispatchGroup) {
        pizza = orderPizza()
    }

    DispatchQueue.global().async(group: dispatchGroup) {
        chicken = orderChicken()
    }

    DispatchQueue.global().async(group: dispatchGroup) {
        tteokbokki = orderTteokbokki()
    }

    // 모든 주문이 완료될 때까지 기다리지 않음
    dispatchGroup.notify(queue: .main) {
        // 주문한 음식이 도착하면 처리
        eat(pizza!)
        eat(chicken!)
        eat(tteokbokki!)
    }
}

비동기 방식에서는 여러 음식을 동시에 주문하고, 각각이 도착하는대로 처리합니다. 이때 DispatchGroup을 사용하여 모든 주문이 완료될 때까지 기다리지 않고 계속 진행합니다.

1-12. Combine

 Combine은 Swift에서 제공하는 함수형 반응형 프로그래밍 프레임워크


비동기 데이터 스트림을 처리하기 위한 연산자들을 제공하며, 데이터의 흐름을 선언적으로 조작할 수 있습니다. 특히 UI 업데이트, 네트워킹, 사용자 입력 등에서 유용하게 사용됩니다.

import Combine

class OrderManager {
    var orderSubject = PassthroughSubject<String, Never>()
    
    func placeOrder(order: String) {
        // 주문 처리 로직...
        // 스트림에 주문을 발행
        orderSubject.send(order)
    }
}

let orderManager = OrderManager()

// 주문 스트림을 구독하여 처리
let cancellable = orderManager.orderSubject
    .sink { order in
        print("주문이 들어왔습니다: \\(order)")
    }

// 주문 발행
orderManager.placeOrder(order: "햄버거") // 주문이 들어왔습니다: 햄버거
orderManager.placeOrder(order: "피자") // 주문이 들어왔습니다: 피자

// 구독 취소
cancellable.cancel()

1-13. RXSwift

 RXSwift는 ReactiveX 프레임워크의 Swift 버전으로, 비동기 및 이벤트 기반 프로그래밍을 위한 라이브러리

 

Observable 시퀀스를 사용하여 데이터 스트림을 처리하고, 연산자를 통해 데이터를 변환하고 결합할 수 있습니다. 함수형 및 반응형 프로그래밍 패러다임을 지원합니다.

import RxSwift

class OrderManager {
    var orderSubject = PublishSubject<String>()
    
    func placeOrder(order: String) {
        // 주문 처리 로직...
        // 스트림에 주문을 발행
        orderSubject.onNext(order)
    }
}

let orderManager = OrderManager()

// 주문 스트림을 구독하여 처리
let disposable = orderManager.orderSubject
    .subscribe(onNext: { order in
        print("주문이 들어왔습니다: \\(order)")
    })

// 주문 발행
orderManager.placeOrder(order: "햄버거") // 주문이 들어왔습니다: 햄버거
orderManager.placeOrder(order: "피자") // 주문이 들어왔습니다: 피자

// 구독 해제
disposable.dispose()