Swift 시작하기 — 기본 문법과 옵셔널

Swift란?

Swift는 Apple이 개발한 현대적 프로그래밍 언어입니다. iOS, macOS, watchOS, tvOS 앱 개발은 물론 서버 사이드 개발에도 사용됩니다. 강력한 타입 시스템, 옵셔널을 통한 null 안전성, 간결한 문법이 특징입니다.

이 글에서는 Swift의 기본 문법부터 옵셔널 처리까지 핵심 내용을 정리합니다.

변수와 상수

Swift에서는 let으로 상수(불변), var로 변수(가변)를 선언합니다. 타입 추론이 강력하여 대부분의 경우 타입을 명시하지 않아도 됩니다.

import Foundation

// let — 상수 (불변, 권장)
let name = "홍길동"             // String으로 추론
let age: Int = 25               // 명시적 타입 선언
let pi = 3.14159                // Double로 추론
// name = "김영희"              // 컴파일 에러! let은 재할당 불가

// var — 변수 (가변)
var score = 85
score = 92                      // 재할당 가능

// 문자열 보간 (String Interpolation)
print("\(name) (\(age)세), 점수: \(score)")
// 실행 결과: 홍길동 (25세), 점수: 92

// 여러 줄 문자열
let message = """
    안녕하세요,
    Swift를 시작합니다.
    현재 점수는 \(score)점입니다.
    """
print(message)
// 실행 결과:
// 안녕하세요,
// Swift를 시작합니다.
// 현재 점수는 92점입니다.

// 컬렉션 타입
let fruits = ["사과", "바나나", "포도"]          // Array
let scoreMap = ["국어": 95, "영어": 88]          // Dictionary
let uniqueNums: Set = [1, 2, 3, 2, 1]           // Set (중복 제거)

print("과일: \(fruits)")
print("국어 점수: \(scoreMap["국어"] ?? 0)")
print("고유 숫자: \(uniqueNums)")
// 실행 결과:
// 과일: ["사과", "바나나", "포도"]
// 국어 점수: 95
// 고유 숫자: [1, 2, 3]

Swift에서는 let을 기본으로 사용하고, 값을 변경해야 할 때만 var을 사용하는 것이 권장됩니다.

옵셔널 (Optional)

옵셔널은 Swift의 핵심 개념입니다. 값이 있을 수도 있고 없을 수도 있는(nil) 상황을 타입으로 표현합니다. 옵셔널이 아닌 타입에는 nil을 대입할 수 없으므로, 컴파일 타임에 null 참조 오류를 방지합니다.

import Foundation

// 옵셔널 선언 — 타입 뒤에 ? 붙임
var nickname: String? = "길동이"
var phone: String? = nil           // 값 없음

// if let — 옵셔널 바인딩 (안전한 언래핑)
if let unwrapped = nickname {
    print("닉네임: \(unwrapped)")  // nil이 아닐 때만 실행
} else {
    print("닉네임 없음")
}
// 실행 결과: 닉네임: 길동이

// guard let — 조기 반환 패턴
func greet(name: String?) {
    guard let validName = name else {
        print("이름이 없습니다.")
        return  // nil이면 여기서 반환
    }
    // 이 이후로 validName은 String (옵셔널 아님)
    print("안녕하세요, \(validName)님!")
}

greet(name: "김영희")
// 실행 결과: 안녕하세요, 김영희님!
greet(name: nil)
// 실행 결과: 이름이 없습니다.

// nil 병합 연산자 (??) — 기본값 제공
let displayName = nickname ?? "이름 없음"
let displayPhone = phone ?? "번호 없음"
print("표시명: \(displayName), 전화: \(displayPhone)")
// 실행 결과: 표시명: 길동이, 전화: 번호 없음

// 옵셔널 체이닝 (?.) — 안전한 프로퍼티/메서드 접근
let uppercased = nickname?.uppercased()
print("대문자: \(uppercased ?? "nil")")
// 실행 결과: 대문자: 길동이

// map으로 옵셔널 변환
let length: Int? = nickname.map { $0.count }
print("닉네임 길이: \(length ?? 0)")
// 실행 결과: 닉네임 길이: 3

!(강제 언래핑)는 nil일 때 런타임 크래시를 일으키므로 가능한 사용을 피합니다. if let, guard let, ??를 조합하여 안전하게 처리합니다.

함수와 클로저

Swift의 함수는 매개변수 레이블(argument label)을 지원하여 호출 시 가독성이 높습니다.

import Foundation

// 함수 — 매개변수 레이블 사용
func calculateBMI(weight: Double, height: Double) -> Double {
    return weight / (height * height)
}

// 외부/내부 매개변수 이름 분리
func greetUser(to name: String, with title: String = "님") -> String {
    return "안녕하세요, \(name)\(title)!"
}

// 가변 매개변수
func average(of numbers: Double...) -> Double {
    guard !numbers.isEmpty else { return 0 }
    return numbers.reduce(0, +) / Double(numbers.count)
}

print(calculateBMI(weight: 70, height: 1.75))
// 실행 결과: 22.857142857142858

print(greetUser(to: "홍길동"))
// 실행 결과: 안녕하세요, 홍길동님!

print(greetUser(to: "홍길동", with: " 선생님"))
// 실행 결과: 안녕하세요, 홍길동 선생님!

print(average(of: 85, 92, 78, 90))
// 실행 결과: 86.25

// 클로저 — 일급 함수
let numbers = [5, 3, 8, 1, 9, 2, 7]

// 정렬 — 클로저 사용
let sorted = numbers.sorted { $0 < $1 }  // 약식 인자 이름
print("정렬: \(sorted)")
// 실행 결과: 정렬: [1, 2, 3, 5, 7, 8, 9]

// map, filter, reduce 체이닝
let result = numbers
    .filter { $0 > 3 }           // 3 초과
    .map { $0 * 2 }              // 2배
    .reduce(0, +)                // 합계
print("결과: \(result)")
// 실행 결과: 결과: 62

// 고차 함수 정의
func transform(_ items: [Int], using operation: (Int) -> Int) -> [Int] {
    return items.map(operation)
}

let doubled = transform(numbers, using: { $0 * 2 })
print("2배: \(doubled)")
// 실행 결과: 2배: [10, 6, 16, 2, 18, 4, 14]

열거형과 패턴 매칭

Swift의 열거형(enum)은 연관 값(Associated Values)을 가질 수 있어 매우 강력합니다.

import Foundation

// 연관 값이 있는 열거형
enum NetworkResult {
    case success(data: String, statusCode: Int)
    case failure(error: String, statusCode: Int)
    case loading
}

// 패턴 매칭 — switch
func handleResult(_ result: NetworkResult) {
    switch result {
    case .success(let data, let code):
        print("성공 [\(code)]: \(data)")
    case .failure(let error, let code) where code >= 500:
        print("서버 에러 [\(code)]: \(error)")
    case .failure(let error, let code):
        print("클라이언트 에러 [\(code)]: \(error)")
    case .loading:
        print("로딩 중...")
    }
}

handleResult(.success(data: "사용자 데이터", statusCode: 200))
// 실행 결과: 성공 [200]: 사용자 데이터

handleResult(.failure(error: "서버 점검", statusCode: 503))
// 실행 결과: 서버 에러 [503]: 서버 점검

handleResult(.failure(error: "인증 실패", statusCode: 401))
// 실행 결과: 클라이언트 에러 [401]: 인증 실패

handleResult(.loading)
// 실행 결과: 로딩 중...

// if case let — 특정 케이스만 추출
let results: [NetworkResult] = [
    .success(data: "A", statusCode: 200),
    .failure(error: "B", statusCode: 404),
    .success(data: "C", statusCode: 201)
]

for case .success(let data, _) in results {
    print("성공 데이터: \(data)")
}
// 실행 결과:
// 성공 데이터: A
// 성공 데이터: C

정리

Swift의 핵심 특징을 요약하면 다음과 같습니다.

  • let/var: 불변/가변 구분으로 안전한 코드를 작성합니다. let을 기본으로 사용합니다.
  • 옵셔널: ?로 nil 가능성을 타입에 명시하고, if let, guard let, ??로 안전하게 처리합니다.
  • 매개변수 레이블: 함수 호출 시 자연어에 가까운 가독성을 제공합니다.
  • 클로저: 일급 함수와 약식 인자 이름($0, $1)으로 간결한 함수형 코드를 작성합니다.
  • 열거형: 연관 값과 패턴 매칭으로 상태를 안전하게 모델링합니다.
  • 강제 언래핑(!) 사용 자제: 런타임 크래시를 방지하기 위해 안전한 언래핑 방식을 사용합니다.

이 글이 도움이 되었나요?