[내일배움캠프] 251224 TIL

Bambu·2025년 12월 24일

내배캠 TIL

목록 보기
6/52

1. STEP 4. 비동기 프로그래밍 / 제네릭

1) 비동기 프로그래밍

동기와 비동기

  • 동기(Synchronous) : 작업이 완료될 때까지 대기
  • 비동기(Asynchronous) : 작업 완료를 기다리지 않고 바로 다음 작업 실행

GCD(Grand Central Dispatch)의 주요 큐

  • Main Queue : UI 작업물 처리
  • Global Queue : 백그라운드 작업 처리
  • Custom Queue : 사용자 정의 작업 큐
DispatchQueue.global().async { // 작업을 글로벌 큐로 디스패치, 비동기로 실행
	print("비동기 작업 실행") 
    // 비동기 작업이 실행되는 동안 기존의 큐 작업은 비동기 작업의 종료를 기다리지 않고 실행
    Dispatch Queue.main.async { // 작업을 메인 큐로 디스패치, 비동기로 실행
    	print("UI 업데이트")
        // UI 업데이트 작업 메인 큐에서 실행
    }
}

2) 직접 구현해보기

STEP 1: 비동기 프로그래밍 구현

  • 비동기 데이터 처리
DispatchQueue.global().async {
	[1, 2, 3, 4, 5].forEach { print($0) }
    
    DispatchQueue.main.async {
    	print("UI 업데이트 완료")
    }
}

// 1 2 3 4 5 출력

→ 위 코드로는 1, 2, 3, 4, 5 출력 후 메인 스레드 자체가 종료되므로(다른 코드가 없으니) 메인 큐를 실행하지 못해 "UI 업데이트 완료"가 출력되지 않음

DispatchQueue.global().async {
	[1, 2, 3, 4, 5].forEach { print($0) }
    
    DispatchQueue.main.async {
    	print("UI 업데이트 완료")
    }
}

dispatchMain() // 메인 런 루프(메인 스레드에서 발생하는 이벤트 처리 엔진) 실행

// 1 2 3 4 5 
// UI 업데이트 완료 출력

dispatchMain() 코드로 메인 런 루프 실행 - 메인 큐에 등록된 코드 실행 : "UI 업데이트 완료" 정상 출력

  • 네트워크 시뮬레이션
func loadData() {
	sleep(3)
    print("Data")
}

DispatchQueue.global().async {
	loadData()
    
    DispatchQueue.main.async {
    	print("데이터 로드 완료")
    }
}

dispatchMain()

// 3초 대기 후 "Data" , "데이터 로드 완료" 출력

STEP 2: 제네릭 구현

  • 제네릭 함수 작성
func swapValues<T>(_ a: inout T, _ b: inout T) {
	let temp = a
    a = b
    b = temp
}

var a = 10
var b = 20
swapValues(&a, &b)
print(a, b) // "20 10" 출력
}
  • 제네릭 스택 구현
struct Stack<T> {
    var arr: [T] = []
    
    // 스택 요소 추가
    mutating func push(_ element: T) {
        arr.append(element)
    }
    
    // 스택 요소 제거
    mutating func pop() {
        arr.removeLast()
    }
    
    // 스택 현재 상태 출력
    func status() {
        print(arr)
    }
}
var a = Stack(arr: ["a", "b"]) // 문자열 값으로 초기화하여 자동적으로 T를 String으로 타입 추론
a.push("c")
a.status() // ["a", "b", "c"] 출력

var b = Stack<Int>() // <Int>로 T의 타입을 선언
b.push(1)
b.status() // [1] 출력

2. iOS 퀴즈 - mutating

💡 왜 구조체 메소드에서는 mutating을 붙여야 할까?

  • 구조체는 값 타입!
    → 값 타입은 값 자체가 자기 자신이라 값을 변경하면 별개의 존재가 됨
struct Animal {
	var name: String
    var age: Int
}

var doggy = Animal(name: "Doggy", age: 3)
var doggy2 = doggy

doggy2.age = 4

print(doggy.age) // 3 출력
print(doggy2.age) // 4 출력

doggy2doggy를 할당하였지만 doggy2doggy는 별개의 인스턴스!
⇒ 그렇기 때문에 doggy2.age를 바꾸어도 doggy.age는 그대로 3을, doggy2.age는 변경된 값인 4를 출력

➡︎ 따라서 값 타입 내부 메소드에서 값 타입의 속성을 변경하면 별개의 인스턴스로 대체되어버림!

struct Animal {
	var name: String
    var age: Int
    
    func grow() {
    	age += 1
    }
}

var doggy = Animal(name: "Doggy", age; 3)
doggy.grow() // doggy는 Animal(name: "Doggy", age: 4)인 별개의 인스턴스로 대체

→ 단순히 doggyage가 4로 변하는 것이 아니라, Animal(name: "Doggy", age: 4)인 전혀 다른 별개의 인스턴스로 대체됨

💡 doggy2.age = 4에서는 인스턴스 대체가 일어나는가?

첫 번째 예시 코드에서는 메소드가 아닌 doggy2.age = 4로 직접 속성을 변경하였다.

이 경우에도 메소드와 동일하게 인스턴스가 대체되는 방식으로 작동하지만, 첫 번째 예시는 두 인스턴스 doggydoggy2가 다르다는 것을 보여주기 위한 것으로 해당 설명을 생략하였다.

  • mutating 키워드는 대체되어도 괜찮다!라는 것을 컴파일러에게 알리는 역할!
    → 값 타입의 경우, 컴파일러는 기본적으로 내부 속성이 '변하지 않는다'라고 가정하고 인스턴스를 상수(constant)로 고려함

⇒ 메소드 앞에 mutating 키워드를 붙임으로써 해당 메소드가 self(메소드를 호출하고 있는 현재의 인스턴스)를 새로운 인스턴스로 대체 가능하다라고 컴파일러에게 허락함

  • 클래스에서는 mutating을 안쓸까?
    → 클래스는 참조 타입이기 때문!
class Animal {
	var name: String
    var age: Int
    
    init(name: String, age: Int) {
    	self.name = name
        self.age = age
}

var doggy = Animal(name: "Doggy", age: 3)
var doggy2 = doggy

doggy2.age = 4

print(doggy.age) // 4 출력
print(doggy2.age) // 4 출력

doggy가 할당된 doggy2doggy와 완전히 동일한 인스턴스를 가리키고 있음!
⇒ 그렇기 때문에 doggy2.age를 바꾸어도 doggy.agedoggy2.age는 동일하게 변경된 값인 4를 출력
➡︎ var doggy2 = doggyAnimal(name: "Doggy", age: 3) 인스턴스 자체를 할당하는 것이 아닌, 해당 인스턴스에 대한 참조를 할당하는 것이므로 두 변수 doggydoggy2가 가리키는 인스턴스는 동일

➡︎ 따라서, 애초에 mutating을 사용하는 조건 자체가 아니기때문에(어차피 동일한 인스턴스니까) mutating 없이 메소드를 작성함!

profile
안녕하세요, iOS 개발을 공부하고 있는 Bambu입니다. (프로필: Swifticons)

0개의 댓글