목차
옵셔널 언래핑 방법
옵셔널 체이닝(Optional Chaining)
오류 처리(Error Handling)
제네릭(Generic)
컬렉션 타입(Collection Type)
배열(Array) 기초와 활용
배열의 추가, 삭제, 정렬, 최댓값/최솟값
들어가며
Swift를 공부하다 보면 초반에는 문법이 각각 따로 보이지만, 실제로는 전부 연결되어 있습니다.
이번 6주차 자료에서는 다음 네 가지가 핵심입니다.
옵셔널 체이닝, 오류 처리, 제네릭, 그리고 배열(Array) 입니다.
이 내용들은 iOS 앱을 만들 때 거의 매번 등장하는 기본 개념이라서, 단순히 예제만 따라가기보다
“왜 이렇게 쓰는지”, “어떤 상황에서 필요한지”를 함께 이해하는 것이 중요합니다.
1. 옵셔널을 언래핑하는 여러 가지 방법
Swift에서 옵셔널(Optional)은 값이 있을 수도 있고, 없을 수도 있는 타입입니다.
예를 들어 아래처럼 선언하면 x는 문자열이 들어있을 수도 있고 nil일 수도 있습니다.
var x: String? = "Hi"
이 옵셔널 값을 사용하는 대표적인 방법은 4가지입니다.
1-1. Forced Unwrapping
x!
강제로 값을 꺼내는 방법입니다.
값이 반드시 있다고 확신할 때는 쓸 수 있지만, nil이면 바로 크래시가 납니다.
그래서 자료에서도 unsafe라고 설명하고 있습니다.
예시:
var x: String? = "Hi"
print(x, x!)
1-2. Optional Binding
if let a = x {
print(a)
}
가장 안전한 방식입니다.
값이 있으면 꺼내서 사용하고, 없으면 실행하지 않습니다.
실무와 시험 모두에서 아주 중요합니다.
1-3. Nil Coalescing Operator
let c = x ?? ""
옵셔널 값이 있으면 그 값을 사용하고, nil이면 뒤의 기본값을 사용합니다.
즉, “없으면 이 값으로 대신하자”라는 의미입니다.
1-4. Optional Chaining
let b = x?.count
옵셔널 값 뒤에 ?를 붙여 안전하게 접근하는 방식입니다.
예를 들어 x가 "Hi"이면 글자 수를 얻을 수 있고, nil이면 에러 대신 nil이 반환됩니다.
예제 흐름:
var x: String? = "Hi"
if let a = x {
print(a)
}
let b = x!.count
print(type(of: b), b)
let b1 = x?.count
print(type(of: b1), b1, b1!)
let c = x ?? ""
print(c)
여기서 중요한 점은
x!.count는 일반 타입처럼 다룰 수 있지만 위험하고,
x?.count는 안전하지만 결과가 옵셔널 타입이 된다는 점입니다.
2. Optional Chaining이란?
옵셔널 체이닝은 옵셔널 타입의 프로퍼티나 메서드에 안전하게 접근하는 문법입니다.
대표 예시는 다음과 같습니다.
cell.textLabel?.text
textLabel은 UILabel? 타입이라서 바로 .text에 접근할 수 없습니다.
따라서 언래핑 후 접근해야 하는데, 이때 !보다 ?를 쓰는 것이 더 안전합니다.
실제로 Xcode도 이런 경우 ?.를 자동으로 붙여주는 경우가 많습니다.
2-1. 왜 필요한가?
예를 들어 다음 두 코드를 비교해보면 차이가 분명합니다.
cell.textLabel!.text
이 코드는 편하지만 textLabel이 nil이면 크래시가 납니다.
cell.textLabel?.text
이 코드는 접근에 실패하면 nil을 반환하므로 더 안전합니다.
즉, 성공하면 옵셔널 값이 나오고 실패하면 nil이 나옵니다.
2-2. Optional Chaining 예시
자료에는 다음과 같은 예시들이 나옵니다.
(pLocation?.coordinate.latitude)!
tabBarController?.selectedIndex = 1
cell.textLabel?.text = items[(indexPath as NSIndexPath).row]
rectangleAdView?.delegate = self
audioPlayer?.volume = volumeControl.value
audioRecorder?.recording
audioRecorder?.record()
locationManager?.requestWhenInUseAuthorization()
이처럼 프로퍼티 접근, 값 대입, 메서드 호출까지 모두 ?.를 통해 안전하게 처리할 수 있습니다.
3. Optional Chaining 예제로 이해하기
3-1. Person 예제
class Person {
var name: String
var age: Int
init(name: String, age: Int) {
self.name = name
self.age = age
}
}
let kim: Person = Person(name: "Kim", age: 20)
print(kim.age)
let han: Person? = Person(name: "Han", age: 25)
print(han.age) // error
kim은 일반 타입이라 바로 접근 가능하지만,
han은 Person? 이므로 han.age처럼 바로 접근하면 오류가 납니다.
이럴 때 가능한 방식은 다음과 같습니다.
print(han!.age)
print(han?.age)
print((han?.age)!)
또는 가장 안전하게:
if let hanAge = han?.age {
print(hanAge)
} else {
print("nil")
}
여기서 han?.age의 결과는 Optional(25) 형태가 됩니다.
즉, 옵셔널 체이닝의 결과는 다시 옵셔널입니다.
3-2. Company와 CEO 예제
class Company {
var ceo: Person?
}
class Person {
var name: String
init(name: String) {
self.name = name
}
}
let apple = Company()
apple.ceo = Person(name: "Kim")
이때 ceo는 옵셔널이므로 다음처럼 접근합니다.
if let ceo = apple.ceo {
print(ceo.name)
}
print(apple.ceo!.name)
print(apple.ceo?.name)
if let name = apple.ceo?.name {
print(name)
}
print(apple.ceo?.name ?? "CEO가 없습니다")
핵심은 다음입니다.
- apple.ceo!.name : 값이 없으면 위험
- apple.ceo?.name : 안전하지만 결과는 옵셔널
- ??와 함께 사용하면 기본값 설정 가능
3-3. 다중 옵셔널 체이닝 예제
class Person {
var name: String?
var age: Int?
var sns: SNS? = SNS()
}
class SNS {
var fb: FaceBook? = FaceBook()
var tt: X?
}
class FaceBook {
var account: String = "aaa@bbb.com"
}
class X {
var account: String = ""
}
이 구조에서 아래 코드는 다중 언래핑입니다.
let p = Person()
if let s = p.sns {
if let f = s.fb {
print("1: \(f.account)")
}
}
옵셔널 체이닝으로 줄이면:
if let account = p.sns?.fb?.account {
print("2: \(account)")
}
또한 다음 코드들도 자료에 나옵니다.
print("3: \((p.sns?.fb?.account)!)")
print("4: \(p.sns!.fb!.account)")
// print("5: \(p.sns?.tt?.account)") // nil
// print("6: \(p.sns!.tt!.account)") // 오류
여기서 정리할 핵심은 두 가지입니다.
첫째
옵셔널 체인으로 클래스나 구조체의 프로퍼티를 참조할 때,
중간에 nil이 있어도 오류가 발생하지 않고 nil을 반환합니다.
둘째
마지막 값이 일반 타입이어도,
옵셔널 체인을 거쳐 얻은 결과는 옵셔널 타입으로 반환됩니다.
자료에는 메서드도 언급되는데,
메서드의 경우 괄호 다음에 ?를 붙이는 형태로 사용할 수 있다고 설명합니다.
4. 옵셔널 체이닝을 쓰는 이유
옵셔널 타입이 많이 중첩되면 if let을 계속 써야 해서 코드가 길어집니다.
예를 들어:
if let s = p.sns {
if let f = s.fb {
print("\(f.account)")
}
}
이걸 한 줄로 줄이면:
print("\(p.sns?.fb?.account)")
물론 출력 결과는 옵셔널이 되므로, 실제로 사용할 때는 바인딩이나 ??를 함께 쓰는 것이 좋습니다.
자료에서는 다음처럼 비교합니다.
print("\(p.sns!.fb!.account)") // crash 가능성 있음
print("\(p.sns?.fb?.account)") // 옵셔널 체인
즉, 옵셔널 체이닝은
다중 if 없이 간결하게,
크래시 위험을 줄이며,
안전하게 프로퍼티와 메서드에 접근하기 위해 사용합니다.
5. 오류 처리(Error Handling)
이제 두 번째 주제인 오류 처리입니다.
Swift에서 오류 처리는 런타임 시 발생하는 문제를 발견하고, 응답하고, 복구하는 과정입니다.
예외 처리라고도 부르며, Swift 2.0 이후부터 본격적으로 도입되었습니다.
5-1. 왜 오류 처리가 필요한가?
예를 들어 파일을 읽는 작업을 생각해보면 다음 같은 문제가 생길 수 있습니다.
- 존재하지 않는 파일
- 읽기 권한 없음
- 호환되지 않는 형식
단순히 성공/실패만 아는 것으로는 부족할 때가 많습니다.
왜 실패했는지 정확히 알아야 상황에 맞는 대응이 가능하기 때문입니다.
Swift는 옵셔널로도 성공/실패를 표현할 수 있지만,
오류 처리 기능을 통해 실패 원인을 더 구체적으로 전달할 수 있습니다.
5-2. Swift 오류 제어 방법
자료에서는 Swift의 오류 제어를 크게 세 가지로 설명합니다.
1) do-catch 구문
가장 일반적이고 널리 사용되는 방식입니다.
throws로 전파된 에러를 do-catch에서 명시적으로 처리합니다.
2) try?
에러를 간단히 무시하거나, 실패 여부만 확인하고 싶을 때 씁니다.
3) try!
에러가 절대 발생하지 않을 것이라고 가정할 때 사용합니다.
하지만 에러가 발생하면 프로그램이 크래시 납니다.
5-3. try?
let result = try? throwingFunction()
if let value = result {
print("성공: \(value)")
} else {
print("실패")
}
이 방식은 에러를 자세히 처리하지 않고,
그냥 “성공했는가 실패했는가” 정도만 보고 싶을 때 적절합니다.
결과가 옵셔널이 된다는 점도 기억해야 합니다.
5-4. try!
let result = try! throwingFunction()
print("결과: \(result)")
에러가 절대 안 난다고 확신할 때만 사용하는 문법입니다.
하지만 실제로 에러가 나면 프로그램이 바로 종료되므로 조심해서 써야 합니다.
5-5. Throwing Function
에러를 던질 수 있는 함수는 throws 키워드를 사용합니다.
예시:
func can() throws
func canThrowErrors() throws -> String
func cannotThrowErrors() -> String
정리하면:
- throws가 붙은 함수는 에러를 던질 수 있음
- 이런 함수는 그냥 호출할 수 없음
- 반드시 try, try?, try!, 혹은 do-catch와 함께 사용해야 함
5-6. do-try-catch 호출 방식
자료의 대표 예시는 AVAudioPlayer(contentsOf:) 입니다.
이 함수는 throws가 붙어 있기 때문에 그냥 호출하면 안 됩니다.
잘못된 예:
AVAudioPlayer(contentsOf: audioFile)
이렇게 호출하면 다음과 같은 오류가 발생합니다.
Call can throw, but it is not marked with 'try' and the error is not handled
따라서 반드시 다음처럼 처리해야 합니다.
do {
audioPlayer = try AVAudioPlayer(contentsOf: audioFile)
} catch let error as NSError {
print("Error-initPlay : \(error)")
}
즉, 에러 가능성이 있는 함수는 반드시 try 계열 문법으로 감싸야 한다는 점이 중요합니다.
5-7. do-catch 기본 구조
do {
try 오류 발생 코드
오류가 발생하지 않으면 실행할 코드
} catch 오류패턴1 {
처리 코드
} catch 오류패턴2 where 조건 {
처리 코드
} catch {
처리 코드
}
이 문법은 시험에서도 자주 나오는 기본 골격이므로 그대로 기억해두는 것이 좋습니다.
6. Generic
세 번째 주제는 제네릭입니다.
자료에서는 제네릭을 Swift의 아주 강력한 기능이라고 설명합니다.
실제로 Swift 표준 라이브러리 상당수가 제네릭으로 만들어져 있습니다.
6-1. 제네릭이 필요한 이유
매개변수 타입만 다르고 기능은 같은 함수를 여러 개 만드는 것은 비효율적입니다.
예를 들어 아래 함수는 Int만 받을 수 있습니다.
func myPrint(a: Int, b: Int) {
print(b, a)
}
myPrint(a: 1, b: 2)
myPrint(a: 2.5, b: 3.5) // error
Double을 넣으면 오류가 납니다.
이 문제를 해결하기 위해 제네릭을 사용합니다.
6-2. 제네릭 함수
func myPrint<T>(a: T, b: T) {
print(b, a)
}
myPrint(a: 1, b: 2)
myPrint(a: 2.5, b: 3.5)
// myPrint(a: "Hi", b: "Hello")
<T>는 타입 자리표시자입니다.
즉, 같은 동작을 하되 타입은 상황에 맞게 바뀔 수 있게 한 것입니다.
이렇게 하면 Int, Double, String 등 다양한 타입에 대해 같은 함수를 재사용할 수 있습니다.
6-3. 일반 클래스 vs 제네릭 클래스
일반 클래스
class Box {
var item: Int
init(item: Int) {
self.item = item
}
func getItem() -> Int {
return item
}
}
let intBox = Box(item: 12)
print(intBox.getItem())
이 클래스는 Int만 저장할 수 있습니다.
제네릭 클래스
class Box<T> {
var item: T
init(item: T) {
self.item = item
}
func getItem() -> T {
return item
}
}
이렇게 만들면 Int도 되고 String도 됩니다.
자료에서는 타입 추론 덕분에 <Int>를 생략할 수 있다고 설명합니다.
6-4. Swift의 Array도 제네릭 구조체
자료에서는 Array 자체가 제네릭 구조체라고 설명합니다.
var a: [Int] = [1,2,3,4]
var b: Array<Int> = [1,2,3,4]
그리고 내부 선언 형태는 다음처럼 소개됩니다.
@frozen struct Array<Element>
여기서 Element가 제네릭 타입입니다.
즉, 배열은 “어떤 타입의 원소를 담을지”를 나중에 결정할 수 있는 구조입니다.
7. Collection Type
컬렉션 타입은 여러 개의 값을 하나의 변수에 저장하고 관리할 수 있는 데이터 타입입니다.
자료에서는 Swift가 대표적으로 다음 세 가지 컬렉션을 제공한다고 보는 흐름입니다.
- Array
- Set
- Dictionary
이번 자료에서는 그중에서 Array를 중심으로 다룹니다.
8. Array
이제 배열을 자세히 정리해보겠습니다.
자료에서는 Array를 소개하면서 Swift의 배열이 제네릭 구조체임을 다시 강조합니다.
표현 방법은 여러 가지가 있습니다.
var x: [Int] = []
var y = [Int]()
var z: Array<Int> = []
var a: [Int] = [1,2,3,4]
var b: Array<Int> = [1,2,3,4]
var c: Array<Double> = [1.2,2.3,3.5,4.1]
즉, [Int]와 Array<Int>는 같은 의미입니다.
8-1. 배열의 자료형 확인
let number = [1,2,3,4]
let odd: [Int] = [1,3,5]
let even: Array<Int> = [2,4,6]
print(type(of: number))
print(number)
print(type(of: odd))
print(odd)
print(type(of: even))
print(even)
let animal = ["dog", "cat", "cow"]
print(type(of: animal))
print(animal)
여기서 중요한 점은 타입 추론입니다.
[1,2,3,4]는 자동으로 Array<Int>로 추론되고,
문자열 배열은 Array<String>으로 추론됩니다.
8-2. 빈 배열 만들기
var number: [Int] = []
var odd = [Int]()
var even: Array<Int> = Array()
print(number) // []
세 방식 모두 빈 배열 생성입니다.
8-3. 빈 배열 사용 시 주의사항
자료에서는 이 부분을 중요하게 다룹니다.
let number: [Int] = []
print(number[0]) // 오류
number.append(100) // 불가능
왜냐하면:
- 빈 배열인데 number[0]에 접근하면 오류
- let으로 선언한 배열은 불변이라 값 추가 불가
즉, 배열을 바꾸려면 var로 선언해야 합니다.
가변형과 불변형 예시:
var animal = ["dog", "cat", "cow"] // 가변형
let animal1 = ["dog", "cat", "cow"] // 불변형
8-4. 빈 배열에 값 넣고 수정하기
var number: [Int] = []
number.append(1)
print(number)
number[0] = 10
print(number)
배열은 먼저 공간을 만든 뒤 사용해야 하며,
없는 인덱스에 바로 대입하면 안 됩니다.
8-5. Array(repeating:count:)
특정 값을 여러 개 채운 배열을 만들 수 있습니다.
var x = [0,0,0,0,0]
print(x)
var x1 = Array(repeating: 0, count: 5)
print(x1)
var x2 = [Int](repeating: 1, count: 3)
print(x2)
var x3 = [String](repeating: "A", count: 4)
print(x3)
즉, 같은 값으로 초기화된 배열을 빠르게 만들 수 있습니다.
8-6. for-in으로 배열 접근
let colors = ["red", "green", "blue"]
print(colors)
for color in colors {
print(color)
}
배열의 각 원소를 순서대로 꺼낼 때 가장 기본적인 반복문입니다.
8-7. count와 isEmpty
배열이 비어 있는지, 원소가 몇 개인지 확인하는 속성입니다.
let num = [1, 2, 3, 4]
var x = [Int]()
print(num.isEmpty)
print(x.isEmpty)
if num.isEmpty {
print("비어 있습니다")
} else {
print(num.count)
}
이 속성들은 Array뿐 아니라 Dictionary, String 등에도 있다는 점도 자료에서 언급됩니다.
8-8. first와 last
let num = [1, 2, 3, 4]
let num1 = [Int]()
print(num.first, num.last)
print(num1.first, num1.last)
if let f = num.first, let l = num.last {
print(f, l)
}
핵심은 first와 last의 결과가 옵셔널이라는 점입니다.
왜냐하면 배열이 비어 있을 수도 있기 때문입니다.
그래서 빈 배열에서는 nil이 나옵니다.
8-9. 첨자(subscript)로 항목 접근
var num = [1, 2, 3, 4]
print(num[0], num[3])
print(num.first!)
for i in 0...num.count-1 {
print(num[i])
}
print(num[1...2])
num[0...2] = [10,20,30]
print(num)
배열은 인덱스를 통해 접근할 수 있습니다.
또한 범위 슬라이싱도 가능하고, 범위 치환도 가능합니다.
9. Array의 추가와 제거
자료에서는 배열의 다양한 변경 메서드를 한 번에 보여줍니다.
var num = [1, 2, 3]
print(num)
num.append(4)
print(num)
num.append(contentsOf: [6, 7, 8])
print(num)
num.insert(5, at: 4)
print(num)
num.remove(at: 3)
print(num)
num.removeLast()
print(num)
print(num.firstIndex(of: 2))
if let i = num.firstIndex(of: 2) {
num[i] = 20
}
print(num)
num = num + num
print(num)
num += [8, 9]
print(num)
num.removeAll()
print(num)
정리하면 다음 기능들이 있습니다.
- append() : 맨 뒤에 1개 추가
- append(contentsOf:) : 여러 개 추가
- insert(_:at:) : 원하는 위치에 삽입
- remove(at:) : 특정 인덱스 삭제
- removeLast() : 마지막 삭제
- firstIndex(of:) : 특정 값의 첫 번째 위치 찾기
- +, += : 배열 합치기
- removeAll() : 전체 삭제
10. Array는 값 타입
Swift의 배열은 구조체이므로 값 타입(value type) 입니다.
var num = [1,2,3]
var x = num
num[0] = 100
print(num)
print(x)
이 코드에서 x = num은 같은 배열을 가리키는 것이 아니라,
복사본이 만들어지는 것입니다.
따라서 num을 수정해도 x는 바뀌지 않습니다.
11. 배열의 최댓값과 최솟값
var num = [1,2,3,10,20]
print(num)
print(num.min())
print(num.max())
print(num.min()!)
print(num.max()!)
min()과 max()는 결과가 옵셔널입니다.
배열이 비어 있으면 최솟값과 최댓값이 존재하지 않기 때문입니다.
따라서 안전하게 쓰려면 옵셔널 바인딩을 함께 고려하는 것이 좋습니다.
12. 배열 정렬
자료 마지막에서는 배열 정렬을 다룹니다.
var num = [1,5,3,2,4]
num.sort()
print(num)
num[0...4] = [2,3,4,5,1]
num.sort(by: >)
print(num)
num[0...4] = [2,3,4,5,1]
num.reverse()
print(num)
print(num.sorted())
print(num)
print(num.sorted(by: >))
print(num)
여기서 차이를 잘 구분해야 합니다.
12-1. sort()
num.sort()
오름차순 정렬을 하며 원본이 변경됩니다.
12-2. sort(by: >)
num.sort(by: >)
내림차순 정렬이며 원본이 변경됩니다.
12-3. reverse()
num.reverse()
현재 순서를 반대로 뒤집습니다.
정렬이라기보다 순서를 뒤집는 개념에 가깝습니다.
12-4. sorted()
num.sorted()
num.sorted(by: >)
정렬된 새 결과를 반환하고,
원본 배열은 그대로 유지됩니다.
13. 시험 포인트 정리
이번 자료에서 특히 시험에 자주 나올 만한 핵심은 아래입니다.
옵셔널 체이닝
- ?.는 옵셔널 프로퍼티/메서드에 안전하게 접근할 때 사용
- 실패하면 에러가 아니라 nil 반환
- 결과는 마지막 값이 일반 타입이어도 옵셔널 타입
오류 처리
- throws 함수는 그냥 호출 불가
- try, try?, try!, do-catch 사용 필요
- try?는 실패 시 nil
- try!는 실패 시 크래시
제네릭
- 같은 기능을 여러 타입에서 재사용할 수 있게 해줌
- func myPrint<T>(a: T, b: T)
- Array<Element>도 제네릭 구조체
Array
- [Int]와 Array<Int>는 동일
- 빈 배열 접근 시 인덱스 오류 주의
- let 배열은 변경 불가
- first, last, min, max는 옵셔널 반환
- sort()는 원본 변경, sorted()는 원본 유지
마무리
이번 6주차 내용은 Swift 문법 중에서도 실전에서 정말 자주 쓰이는 핵심들입니다.
특히 옵셔널 체이닝, 에러 처리, 제네릭, 배열은 각각 따로 외우는 것이 아니라
“안전하게 값을 다루는 방식”이라는 공통 흐름으로 이해하면 훨씬 잘 정리됩니다.
간단히 정리하면,
- 옵셔널 체이닝은 nil 때문에 크래시 나지 않도록 안전하게 접근하는 문법
- 오류 처리는 에러 가능성이 있는 코드를 명확히 처리하는 문법
- 제네릭은 타입에 구애받지 않고 재사용 가능한 코드를 만드는 기능
- 배열은 Swift 컬렉션의 가장 기본이 되는 자료구조
이 네 가지를 제대로 익혀두면 이후의 클래스, 구조체, 컬렉션, iOS UI 코드까지 훨씬 수월하게 이해할 수 있습니다.
'iOS' 카테고리의 다른 글
| [iOS] Open API 기반 iOS앱 개발 (0) | 2026.05.11 |
|---|---|
| [iOS] 7주차 열거형 & 구조체 정리 (0) | 2026.04.20 |
| [iOS] 프로그래밍 5주차 정리 (0) | 2026.04.06 |
| [iOS] iOS 프로그래밍 4주차 정리 (0) | 2026.03.30 |
| [iOS] Swift 데이터 타입 · Tuple · Optional · 연산자 · 제어문 정리 (0) | 2026.03.16 |