본문 바로가기
iOS

Writing Safe Swift Code: Tuples, Optionals, and Type Inference

by Hwangminseo 2025. 9. 16.

 무엇을 배우나? 

  • Swift의 튜플을 활용해 여러 값을 묶어 처리하는 방법을 살펴볼 것이다.
  • 또한 옵셔널을 통해 값이 없을 수도 있는 상황을 안전하게 다루는 방식을 정리해 볼 것이다.
  • 마지막으로 Swift의 타입 안전성 개념과 함께 어노테이션·추론 활용법을 다뤄볼 것 이다.

튜플(Tuple)

  • 여러 값을 하나의 객체로 일시적으로 묶어 다루는 Swift의 강력한 기능이다.
  • 목적: 함수의 다중 반환이나 임시 데이터 전달에 적합하며, 구조체를 만들기엔 과한 소규모 이질 데이터 묶음에 유용하다.
  • 다언어 지원: Python·TypeScript·C#·Rust 등에서도 개념을 공유하여 학습 전이성이 높다.
  • 구문과 타입: 괄호와 콤마로 작성하며 각 항목은 서로 다른 타입이어도 된다(예: let myTuple = (10, 12.1, "Hi"))이다.
  • 배열과 차이: 배열은 동일 타입 컬렉션이고, 튜플은 서로 다른 값들의 묶음을 간단히 표현한다(let oddNumbers = [1,3,5,7,9])다.
  • 접근 방법: 인덱스는 0부터 시작하고 점 표기법으로 접근한다(예: myTuple.2)다.
  • 실행 예: var myString = myTuple.2; print(myTuple, myString)의 출력은 (10, 12.1, "Hi") Hi이다.
  • 분해 할당: let (a, b, c) = myTuple처럼 한 번에 요소를 꺼내어 변수에 배치할 수 있다.
  • 가독성 팁: 필요한 경우 레이블을 붙여 let p = (x: 10, y: 20)처럼 의미를 드러내면 코드가 더 읽기 쉬워진다.
let myTuple = (10, 12.1, "Hi")
print(myTuple, myTuple.2)   // (10, 12.1, "Hi") Hi

let (a, b, c) = myTuple
print(a, b, c)              // 10 12.1 Hi

let (num, _, text) = myTuple
print(num, text)            // 10 Hi

let point = (x: 10, y: 20)
print(point.x, point.y)     // 10 20

튜플의 응용

  • 일괄 분해: let (myInt, myFloat, myString) = myTuple로 한 줄에 모두 추출하며 type(of: myTuple)는 (Int, Double, String)이다.
  • 선택적 추출: 불필요한 항목은 밑줄 _로 무시한다(예: var (myInt, _, myString) = myTuple)이다.
  • 이름 있는 튜플: let myTuple = (count: 10, length: 12.1, message: "Hi")처럼 레이블을 부여하며 타입은 (count: Int, length: Double, message: String)이다.
  • 접근 방식: 레이블과 인덱스를 모두 지원하며 myTuple.message와 myTuple.2는 같은 요소를 가리킨다.
  • 실행 결과: print(myTuple.message, myTuple.2)의 출력은 Hi Hi이다.
  • 활용 & Void: 튜플은 함수의 다중 반환에 유용하며, Void는 빈 튜플 ()의 별칭으로 반환값이 없음을 뜻한다.
let myTuple = (10, 12.1, "Hi")
let (myInt, myFloat, myString) = myTuple
var (myInt2, _, myString2) = myTuple

let myTuple2 = (count: 10, length: 12.1, message: "Hi")
print(myTuple2.message, myTuple2.2)   // Hi Hi

func doNothing() -> Void { }
print(doNothing())                    // ()

옵셔널 타입

  • 정의: 오류 가능성이 있는 반환값은 옵셔널로 감싸서 돌려준다. Int("123")는 Optional(123), Int("Hi")는 nil을 반환한다.
  • nil과 기본형: Swift 기본형(Int, Double, String 등)은 nil을 저장하지 못한다. nil을 담으려면 옵셔널로 선언해야 한다.
  • 선언 표기: 타입 뒤에 ? 또는 !를 붙여 표기한다. 예) var x: Int?, var y: Int!이다.
  • 의미 차이: Int?는 값 또는 nil을 가질 수 있어 안전한 언래핑이 필요하다. Int!는 암시적 추출로 바로 쓰지만 nil이면 런타임 오류가 난다.
  • 출력 예: print(Int("123"))은 Optional(123)로 보이며, Int("Hi") 결과는 nil이라 언래핑 없이 직접 사용하면 안 된다.
  • 권장 사용: 값 존재가 불확실하면 Int?를 기본으로 쓰고, Int!는 초기화 후 항상 값이 보장되는 특수한 경우에만 최소 사용한다.
print(Int("123"))   // Optional(123)
print(Int("Hi"))    // nil

var x: Int? = Int("123")
var y: Int! = Int("123")

print(x)            // Optional(123)
print(y)            // 123

타입 어노테이션과 타입 추론

  • 타입 안전성: Swift는 type safe 언어이다. 한 번 타입이 정해진 변수에는 다른 타입 값을 저장할 수 없으며, 느슨한 타이핑 언어와 대조된다.
  • 식별 방법: 상수·변수의 타입을 정하는 방법은 타입 어노테이션타입 추론 두 가지가 있다.
  • 타입 어노테이션: 선언 시 이름 뒤에 : 타입을 명시한다. 예) var userCount: Int = 10이다.
  • 타입 추론: 어노테이션이 없으면 컴파일러가 대입된 값으로 타입을 결정한다. 예) var x = 2.231은 Double, let companyName = "Apple"은 String이다.
  • 기본 규칙: 부동소수점 리터럴은 기본적으로 Double로 추론된다.
  • 정리: 명시가 필요한 곳은 어노테이션으로 가독성을 높이고, 단순한 곳은 추론으로 코드를 간결하게 유지한다.
var userCount: Int = 10
var x = 2.231
let companyName = "Apple"

print(type(of: userCount))    // Int
print(type(of: x))            // Double
print(type(of: companyName))  // String

강제 언래핑

  • 정의: 강제 언래핑은 옵셔널 값 뒤에 !를 붙여 즉시 실제 값을 꺼내는 방식이다.
  • 사용 예: if x != nil { print(x!) } else { print("nil") }처럼 nil 검사 후 사용한다.
  • 위험성: 값이 nil인 상태에서 x!를 호출하면 런타임 오류가 발생한다.
  • 잘못된 비교: if x! = nil은 대입으로 해석되어 컴파일 에러가 나며, !가 =보다 우선순위가 높다.
  • 표기 주의: 비교는 항상 x != nil 형태로 쓰고 연산자 양쪽에 공백을 두어 가독성을 높인다.
  • 권장 대안: 가능하면 옵셔널 바인딩(if let, guard let)이나 nil 병합 연산자(??)로 안전하게 처리한다.
var x: Int?
x = 10
if x != nil {
    print(x!)          // 10
} else {
    print("nil")
}

var x1: Int?
if x1 != nil {
    print(x1!)
} else {
    print("nil")       // nil
}

옵셔널 바인딩

  • 정의: 옵셔널에 값이 있을 때만 언래핑하여 안전하게 사용하는 방법이다.
  • 기본 문법: if let xx = x { ... }처럼 쓰며, 값이 있으면 xx에 언래핑되어 본문이 실행된다.
  • 단축 문법(Swift 5.7+): 같은 이름일 때 if let x처럼 = x를 생략할 수 있다(= if let x = x).
  • 값이 없을 때: 옵셔널이 nil이면 else 블록이 실행된다(예: var x1: Int?; if let xx = x1 { ... } else { print("nil") }).
  • 출력 예시: var x: Int? = 10; if let xx = x { print(x, xx) }는 **Optional(10) 10**을 출력한다.
  • 스코프 주의: 바인딩된 상수/변수는 if 블록 내부에서만 유효하다.
var x: Int? = 10
if let xx = x {
    print(x, xx)          // Optional(10) 10
} else {
    print("nil")
}

var x1: Int?
if let xx = x1 {
    print(xx)
} else {
    print("nil")          // nil
}

옵셔널 언래핑

  • 조건문에서 콤마로 나열해 한 번에 언래핑한다.
  • 기본 문법: if let a = aOpt, let b = bOpt { ... }처럼 콤마마다 let을 반복한다.
  • 단축 문법(Swift 5.7+): 이름이 같다면 if let aOpt, let bOpt { ... }로 = 값을 생략할 수 있다.
  • 실행 조건: 나열한 옵셔널이 모두 값이 있을 때만 본문이 실행되며 하나라도 nil이면 else로 간다.
  • 예시 출력: pet1="cat", pet2="dog"; if let first = pet1, let second = pet2 { print(first, second) } → cat dog이 출력된다.
  • 스코프 주의: 바인딩된 상수는 해당 if 블록 내부에서만 유효하다.
var pet1: String? = "cat"
var pet2: String? = "dog"

if let first = pet1, let second = pet2 {
    print(first, second)      // cat dog
} else {
    print("nil")
}

var pet3: String?
var pet4: String? = "bird"

if let third = pet3, let fourth = pet4 {
    print(third, fourth)
} else {
    print("nil")              // nil
}

두 가지 옵셔널

  • Int?는 값 또는 nil을 담는 일반 옵셔널이고, Int!는 값이 항상 있다고 가정하는 암시적 추출 옵셔널이다.
  • 접근 방식: Int?는 사용 전 옵셔널 바인딩/언래핑이 필요하고, Int!는 자동 언래핑되어 일반 값처럼 접근한다.
  • 사용 예: var x: Int?, var y: Int!처럼 선언하며, y는 바로 사용 가능하나 x는 if let/guard let 또는 ??로 처리한다.
  • 주요 쓰임: 스토리보드 IBOutlet 등 지연 초기화되지만 화면 로드 후에는 항상 값 보장되는 경우에 Int!가 사용된다.
  • 위험성: Int!가 실제로 nil이면 런타임 크래시가 발생한다; Int?는 안전하게 실패를 표현한다.
  • 가이드: 기본은 Int?를 사용하고, 값 존재가 전 lifecycle에서 엄격히 보장될 때만 Int!를 제한적으로 사용한다.
let x: Int? = 1
let y: Int = x!
let z = x
print(x, y, z) 
// Optional(1) 1 Optional(1)
print(type(of: x), type(of: y), type(of: z)) 
// Optional<Int> Int Optional<Int>

let a: Int! = 1
let b: Int = a
let c: Int = a!
let d = a
let e = a + 1
print(a, b, c, d, e) 
// Optional(1) 1 1 Optional(1) 2
print(type(of: a), type(of: b), type(of: c), type(of: d), type(of: e)) 
// Optional<Int> Int Int Optional<Int> Int

Implicitly Unwrapped Optional

  • 정의: Int!, String!처럼 타입 뒤에 **!**를 붙이는 **암시적 언래핑 옵셔널(IUO)**이다.
  • 의도: 초기에는 nil일 수 있으나, 이후에는 항상 값이 있다고 가정하는 변수·상수에 사용한다.
  • 동작: 값을 쓸 때마다 자동으로 언래핑되어 일반 변수처럼 접근된다.
  • 예시/용도: 스토리보드 IBOutlet 등 화면 로드 후 값이 반드시 존재하는 상황에서 쓰인다.
  • 위험: 실제로 nil이면 런타임 오류가 발생하므로 검증 없이 남용하지 않는다.
  • 가이드: 기본은 **일반 타입 또는 Int?**를 쓰고, 수명주기 전반에 값이 보장될 때만 Int!를 제한적으로 사용한다.
let x: Int? = 1
let y: Int = x!
let z = x
print(x, y, z) 
// Optional(1) 1 Optional(1)
print(type(of: x), type(of: y), type(of: z)) 
// Optional<Int> Int Optional<Int>

let a: Int! = 1
let b: Int = a
let c: Int = a!
let d = a
let e = a + 1
print(a, b, c, d, e) 
// Optional(1) 1 1 Optional(1) 2
print(type(of: a), type(of: b), type(of: c), type(of: d), type(of: e)) 
// Optional<Int> Int Int Optional<Int> Int

옵셔널을 사용하는 이유

  • 옵셔널은 nil을 표현해 값 부재를 타입 수준에서 모델링한다.
  • 값 없음 상태: 옵셔널 변수에 nil을 대입하면 valueless state가 되며 정상 동작한다.
  • 비옵셔널 제한: 옵셔널이 아닌 변수·상수에는 nil을 저장할 수 없어 컴파일 에러가 난다.
  • 선언 규칙: nil 가능성을 표현하려면 타입 뒤에 ?를 붙여 선언한다(var myInt: Int? = nil, var myInt: Int?).
  • 자동 초기화: 옵셔널 변수는 초기화하지 않으면 자동으로 nil로 초기화된다(예: var myString: String = nil은 비옵셔널이라 에러).
  • 안전성 이점: 옵셔널은 “없을 수도 있음”을 명시적으로 처리하게 만들어 런타임 오류를 줄인다.

Any, AnyObject

  • AnyObject: 클래스(객체) 타입만 담을 수 있다. 구조체·열거형·기본 타입(Int, String 등)은 불가하다.
  • Any: 모든 타입의 값을 담을 수 있다. 클래스·구조체·열거형은 물론 함수 타입까지 가능하다.
  • 예시: var o: AnyObject = Dog()처럼 클래스 인스턴스는 AnyObject에, var x: Any = "Hi"; x = 10; x = 3.5처럼 Any에는 다양한 타입을 담는다.
  • 타입 확인: 담긴 값은 type(of:)로 확인하고, 사용할 때는 as?/as!로 다운캐스팅한다.
  • 호환성 맥락: Cocoa/Cocoa Touch API는 컬렉션 등에서 AnyObject를 요구하는 경우가 많다.
  • 주의: Any/AnyObject 남용은 타입 안전성을 떨어뜨리므로, 가능하면 구체 타입을 직접 사용하는 편이 좋다.
class Dog {}
class Cat {}
let dog = Dog()
let cat = Cat()
var o1: AnyObject = dog
var o2: AnyObject = cat

var x: Any = "Hi"
print(x, type(of: x))   // Hi String
x = 10
print(x, type(of: x))   // 10 Int
x = 3.5
print(x, type(of: x))   // 3.5 Double

Swift에서 자주 사용하는 연산자들

  • 대입(=): 변수에 값을 저장하는 기본 연산자이다.
  • 산술(+, −, *, /, %): 덧셈·뺄셈·곱셈·나눗셈·나머지 계산을 수행한다.
  • 복합 대입(+=, −=, *=, /=, %=): 기존 값을 읽어 계산 후 다시 대입한다.
  • 비교(==, !=, <, <=, >, >=): 두 값을 비교해 Bool 결과를 반환한다.
  • 논리(&&, ||, !): 조건을 결합하거나 부정하여 흐름을 제어한다.
  • 옵셔널·범위: ?는 옵셔널 선언, !는 강제 언래핑, ??는 nil 병합이며 ...는 닫힌 범위, ..<는 반열린 범위이다.
var a = 10
var b = 3

print(a + b)    // 13
print(a - b)    // 7
print(a * b)    // 30
print(a / b)    // 3
print(a % b)    // 1

a += 5
print(a)        // 15
a -= 2
print(a)        // 13

print(a == b)   // false
print(a != b)   // true
print(a > b)    // true
print(a <= b)   // false

let cond1 = true
let cond2 = false
print(cond1 && cond2)   // false
print(cond1 || cond2)   // true
print(!cond2)           // true

var x: Int? = nil
print(x ?? 100)         // 100

for i in 1...3 {
    print(i)            // 1 2 3
}
for i in 1..<3 {
    print(i)            // 1 2
}

Swift의 증가 · 감소 연산자

  • Swift는 ++/-- 연산자를 지원하지 않는다.
  • 증가 방법: x += 1 또는 x = x + 1로 값을 1 증가시킨다.
  • 감소 방법: x -= 1 또는 x = x - 1로 값을 1 감소시킨다.
  • 가독성: 명확성과 일관성을 위해 복합 대입 연산자 사용을 권장한다.
  • 에러 예시: x++, x--는 cannot find operator '++' in scope 오류가 난다.
  • 정리: 숫자 증감은 항상 대입식 형태로 작성한다.

옵셔널 언래핑 방법들

  • 옵셔널(String? 등)은 사용 전 언래핑이 필요하다.
  • 강제 언래핑 !: x!는 간단하나 nil이면 크래시가 나므로 지양한다.
  • 옵셔널 바인딩: if let a = x { ... }처럼 값이 있을 때만 열어 안전하게 사용한다.
  • nil 병합 ??: let c = x ?? ""로 기본값을 주어 흐름을 간결하게 처리한다.
  • 옵셔널 체이닝 ?.: let b = x?.count처럼 안전하게 접근하되 결과가 다시 옵셔널임을 염두에 둔다.
  • 정리: 기본은 바인딩/??/체이닝을 우선하고, !은 값이 확실히 보장되는 경우에만 제한적으로 사용한다.
var x: String? = "Hi"

print(x, x!)
if let a = x {
    print(a)
}

let c = x ?? ""
print(c)

let b = x!.count
print(type(of: b), b)

let b1 = x?.count
print(type(of: b1), b1, b1!)

느낀 점

Swift의 튜플과 옵셔널을 배우면서 단순히 문법을 외우는 것이 아니라, 왜 필요한지와 안전성 측면을 깊이 이해하게 되었다.
특히 옵셔널 바인딩과 nil 병합 연산자를 활용하면 런타임 오류를 줄이고 깔끔한 코드를 작성할 수 있다는 점이 인상적이었다.
앞으로 실제 프로젝트에서 타입 안전성과 옵셔널 처리 방식을 잘 활용해 신뢰성 있는 코드를 작성해야겠다는 다짐을 하게 되었다.