Swift 시리즈 목차: 기초 문법부터 옵셔널, 프로토콜, 제네릭, SwiftUI, Combine까지 10편
이 글의 핵심
Swift는 옵셔널, ARC, Copy-on-Write, 프로토콜 지향 설계처럼 다른 언어에서 넘어온 개발자가 처음 보는 개념이 많습니다. 편마다 어떤 질문에 답하는지, 앱 개발이 목적이라면 어디까지 보고 SwiftUI로 넘어가도 되는지를 목적별 읽는 순서로 정리하고, 버전에 따라 문법이 달라지는 지점과 Objective-C와의 차이도 짚었습니다.
시리즈 소개
이 시리즈는 다른 언어를 한 가지 이상 써 본 사람이 Swift로 iOS 앱을 만들 수 있을 때까지 필요한 언어 기능을 순서대로 다룹니다. 문법 목록을 훑기보다, Swift에서만 유독 오래 헷갈리는 개념 몇 가지를 중심으로 편을 나눴습니다.
다른 언어를 먼저 배운 사람이 Swift에서 가장 오래 헷갈리는 것은 문법이 아니라 값 타입과 참조 타입의 구분입니다. Swift의 Array, String, Dictionary는 모두 구조체(값 타입)라서 대입하면 복사된 것처럼 동작하고, 실제 복사는 Copy-on-Write로 수정 시점까지 미뤄집니다. 반면 클래스는 참조가 공유되고 ARC로 수명이 관리되어, 클로저가 self를 강하게 붙잡으면 순환 참조로 메모리가 해제되지 않습니다. SwiftUI의 @State와 @StateObject가 왜 따로 있는지도 이 구분에서 나옵니다. 그래서 이 시리즈는 #02(옵셔널), #03(컬렉션과 COW), #04(클래스와 구조체, ARC)를 SwiftUI보다 앞에 두었습니다. 이 세 편을 건너뛰고 SwiftUI로 바로 가면 “값을 바꿨는데 화면이 안 바뀐다”, “뷰가 사라졌는데 객체가 해제되지 않는다” 같은 문제를 만났을 때 원인을 찾기 어렵습니다.
실습 환경과 Swift 버전
iOS 앱을 빌드하고 시뮬레이터로 돌리려면 macOS의 Xcode가 필요합니다. Windows·Linux용 공식 Swift 툴체인으로는 #01~#08의 언어 예제 대부분을 콘솔에서 돌려 볼 수 있지만, #09 SwiftUI와 #10 Combine은 Apple 플랫폼 프레임워크라 Xcode 없이는 실습하기 어렵습니다.
Swift는 버전마다 문법이 조금씩 바뀌어 온 언어라, 오래된 자료와 이 시리즈의 코드가 다르게 보일 수 있습니다. 편마다 버전이 걸린 곳을 표시해 두었는데, 자주 만나는 경계는 다음과 같습니다.
| 버전 | 바뀐 점 | 관련 편 |
|---|---|---|
| Swift 5.5 | async/await, Task, actor 도입 | #08 |
| Swift 5.6~5.7 | 존재 타입에 any 표기, if let name { } 축약 바인딩 | #02, #05 |
| Swift 6 언어 모드 | 데이터 경쟁 가능성이 경고가 아니라 컴파일 오류 | #08 |
편별 안내
번호는 읽는 순서의 기본값입니다. #03은 주제가 다른 두 편(컬렉션, 함수와 클로저)이 같은 번호를 쓰고 있습니다.
값과 참조: #01~#04
- #01 Swift 시작하기: Xcode와 Playground 설치, Hello World부터 변수·함수·제어문까지 한 번에 훑습니다. 뒤쪽에서 Swift의 메모리 안전성(배타적 접근 규칙)을 Objective-C, Rust의 소유권 모델과 비교합니다.
- #02 변수와 타입:
var/let, 타입 추론, 그리고 이 시리즈에서 가장 중요한 옵셔널을 다룹니다.if let,guard let,??, 옵셔널 체이닝을 언제 쓰는지와 강제 언래핑이 왜 크래시로 이어지는지를 봅니다. - #03 컬렉션:
Array·Dictionary·Set을 쓰는 법과 함께 Copy-on-Write가 내부에서 어떻게 동작하는지,isKnownUniquelyReferenced로 직접 구현해 봅니다. 시리즈에서 가장 깊게 들어가는 편이라 #02가 익숙해진 뒤 읽기를 권합니다. - #03 함수와 클로저: argument label,
inout, 고차 함수, trailing closure, 캡처 목록([weak self]),@escaping,@autoclosure까지 다룹니다. #04의 ARC와 짝을 이루는 편입니다. - #04 클래스와 구조체: class·struct·enum의 차이, 무엇을 고를지의 기준,
init/deinit, 프로퍼티 옵저버, ARC와weak/unowned를 다룹니다.
타입 시스템: #05~#07
- #05 프로토콜: 프로토콜 정의와 채택, 확장을 통한 기본 구현, associated type,
some과any의 차이,Codable, delegate 패턴, type erasure까지 폭넓게 다룹니다. 분량이 가장 많은 편이라 한 번에 다 읽기보다 SwiftUI 코드를 쓰면서 필요한 절로 돌아오는 편이 낫습니다. - #06 제네릭: 제네릭 함수와 타입,
where절로 거는 제약, 연관 타입을 다룹니다. #05의 associated type을 읽은 뒤 보면 연결이 잘 됩니다. - #07 에러 처리:
throws/try/do-catch,try?와try!의 차이,Result,defer,rethrows,LocalizedError, 그리고assert·precondition·fatalError를 언제 쓰는지를 다룹니다.
동시성과 UI: #08~#10
- #08 비동기 프로그래밍:
async/await,Task와withTaskGroup,actor와@MainActor를 다룹니다. Swift 6의 엄격한 동시성 검사를 이해하는 출발점입니다. - #09 SwiftUI: 선언적 UI,
@State·@Binding·ObservableObject의 차이, MVVM 구조,NavigationStack과List, 네트워크 로딩 화면까지 작은 앱 하나를 만드는 흐름으로 진행합니다. - #10 Combine: Publisher와 Subscriber, 연산자,
@Published를 다루고,debounce·combineLatest처럼 시간에 따라 흐르는 값을 조합하는 예제를 봅니다.
#08과 #10은 같은 문제(비동기 이벤트 처리)를 다른 방식으로 푸는 글이라 어느 쪽을 먼저 볼지 자주 질문을 받습니다. 새로 작성하는 코드라면 async/await와 AsyncSequence로 대부분의 경우를 처리할 수 있고, Apple의 최신 API와 예제도 이쪽을 기준으로 나옵니다. Combine은 iOS 13부터 쓸 수 있어 기존 코드베이스에 많이 남아 있고, 검색어 입력에 debounce를 걸거나 여러 입력 스트림을 combineLatest로 합치는 것처럼 시간에 따라 흐르는 값의 조합에서는 여전히 간결합니다. 그래서 #08을 먼저 읽고, 유지보수할 코드에 Combine이 있거나 스트림 연산이 필요할 때 #10을 보는 순서를 권합니다.
목적별 읽는 순서
| 목적 | 순서 | 메모 |
|---|---|---|
| 문법부터 차근차근 | #01 → #02 → #03 함수와 클로저 → #04 | 콘솔 예제로 충분합니다 |
| SwiftUI 앱을 빨리 만들기 | #02 → #03 함수와 클로저 → #04 → #09 | 막히면 #05와 #03 컬렉션으로 돌아옵니다 |
| 라이브러리·아키텍처 코드를 읽고 쓰기 | #05 → #06 → #07 | 프로토콜과 제네릭의 오류 메시지를 읽을 수 있게 됩니다 |
| 네트워크·동시성 | #07 → #08 → #10 | #08 뒤에 async/await 흔한 실수를 이어 보면 좋습니다 |
어느 경로든 #02의 옵셔널과 #04의 ARC는 건너뛰지 않는 것을 권합니다. 아래 “자주 막히는 지점”의 대부분이 이 두 편에서 시작합니다.
학습하면서 자주 막히는 지점
첫째는 옵셔널을 다루는 습관입니다. 컴파일 오류를 없애려고 !(강제 언래핑)를 붙이기 시작하면, 컴파일은 되지만 실행 중에 Fatal error: Unexpectedly found nil while unwrapping an Optional value로 앱이 종료됩니다. Swift가 nil 안전성을 준다는 말은 “nil 가능성을 타입에 드러내 준다”는 뜻이지 크래시를 막아 준다는 뜻이 아니므로, #02에서 if let, guard let, ??를 쓰는 연습을 충분히 하고 넘어가는 편이 좋습니다.
둘째는 클로저와 ARC입니다. 클래스 안의 클로저가 self를 캡처하면 서로를 강하게 참조해 해제되지 않는데, 에러도 경고도 나지 않고 메모리만 계속 늘어납니다. #03 함수와 클로저에서 @escaping과 캡처 목록([weak self])을, #04에서 강한·약한 참조를 함께 봐야 이 문제가 보입니다.
셋째는 프로토콜과 제네릭의 컴파일 오류 메시지입니다. associated type이 있는 프로토콜을 변수 타입으로 쓰려 하면 예전에는 “can only be used as a generic constraint because it has Self or associated type requirements” 같은 메시지가 나왔고, Swift 5.6~5.7을 거치며 any/some 키워드로 표현 방식이 바뀌었습니다. 오래된 자료와 최신 문법이 섞여 있는 영역이라, #05와 #06은 한 번 읽고 끝내기보다 SwiftUI 코드를 작성하면서 다시 돌아와 보는 편이 이해가 빠릅니다.
넷째는 Swift 6의 엄격한 동시성 검사입니다. Swift 6 언어 모드에서는 데이터 경쟁 가능성이 있는 코드가 경고가 아니라 컴파일 오류가 되어, Sendable이나 @MainActor 관련 오류가 한꺼번에 쏟아질 수 있습니다. #08의 Actor와 @MainActor 부분을 먼저 이해해 두면 이 오류들의 의미를 읽을 수 있습니다.
시리즈를 마친 뒤
- iOS·Swift 내부 구조 심화: ARC와 프로토콜 증인 테이블(witness table), UIKit과 SwiftUI의 내부 동작을 더 깊게 봅니다.
- Swift async/await 흔한 실수와 디버깅 팁: #08에서 다룬 구조적 동시성을 실제 코드에서 쓸 때 부딪히는 문제를 모았습니다.
- Kotlin 시리즈: 같은 앱을 Android로 만든다면 이어서 볼 시리즈입니다. 옵셔널과 Kotlin의 nullable 타입을 비교해 보면 두 언어가 같은 문제를 어떻게 다르게 풀었는지 보입니다.
- Rust 시리즈: #01에서 잠깐 비교한 소유권 모델을 제대로 보고 싶다면 이어서 읽을 만합니다.
자주 묻는 질문
Swift와 Objective-C의 차이는?
Objective-C는 C 위에 메시지 전송 방식의 객체 시스템을 얹은 언어라 nil에 메시지를 보내도 조용히 무시되고, 타입 검사가 느슨합니다. Swift는 옵셔널과 강한 정적 타입, 값 타입 중심의 표준 라이브러리로 이런 오류를 컴파일 시점에 드러냅니다. 두 언어는 브리징 헤더로 한 프로젝트 안에서 함께 쓸 수 있어서, 오래된 앱은 두 언어가 섞여 있는 경우가 많습니다.
옵셔널은 왜 필요한가요?
값이 없을 수 있다는 사실을 타입에 표시하기 위해서입니다. String과 String?은 서로 다른 타입이라, nil일 수 있는 값을 확인 없이 쓰는 코드는 컴파일되지 않습니다. 다만 !로 강제 언래핑하면 이 검사를 스스로 끄는 것이어서, nil일 때는 실행 중에 크래시가 납니다.
SwiftUI와 UIKit 중 무엇을 먼저 배워야 하나요?
새 프로젝트라면 SwiftUI로 시작하는 편이 이 시리즈의 흐름과 맞습니다. 다만 회사의 기존 앱은 UIKit으로 되어 있거나 둘이 섞여 있는 경우가 많아서, 유지보수 업무라면 UIKit의 뷰 컨트롤러 생명주기와 delegate 패턴(#05)을 따로 익혀야 합니다.
추천 학습 자료
- The Swift Programming Language: 공식 언어 가이드입니다. 이 시리즈의 각 편은 이 문서의 해당 장과 함께 보면 좋습니다.
- Swift.org: 툴체인 설치와 버전별 변경 사항(Swift Evolution 제안)을 확인할 수 있습니다.
- Apple Developer 문서: SwiftUI·Combine API 레퍼런스입니다.
- Swift Forums: 언어 기능의 설계 배경을 찾아볼 때 유용합니다.
시작하기: Swift 시작하기 (#01)부터 읽으면 됩니다.