Stage 1.5 · 8 Topics · Stage 2 선수 조건

🔩 Swift 핵심 문법

Stage 2에서 쓸 핵심 패턴을 미리 익힌다

struct vs class mutating Computed Property Property Observer Access Control init 패턴 Type Casting static · lazy · inout
시작 전에 던져볼 질문
01
Swift에서 struct와 class는 언제 선택하는가?
검색 → Swift struct vs class when to use value reference type
02
computed property와 함수의 차이는 무엇인가?
검색 → Swift computed property vs function difference
03
access control은 왜 필요한가? private을 안 쓰면 어떤 문제가 생기는가?
검색 → Swift access control private internal public fileprivate
Topic 01

struct vs class — 값 타입 vs 참조 타입

Swift에서 가장 중요한 타입 선택의 기준
Swift
// struct — 값 타입 (복사)
struct PointS {
    var x: Int
    var y: Int
}

var a = PointS(x: 1, y: 2)
var b = a        // 복사본 생성
b.x = 99
print(a.x) // 1  ← a는 변하지 않는다

// class — 참조 타입 (같은 객체를 가리킴)
class PointC {
    var x: Int
    var y: Int
    init(x: Int, y: Int) { self.x = x; self.y = y }
}

var c = PointC(x: 1, y: 2)
var d = c        // 같은 객체를 가리킴
d.x = 99
print(c.x) // 99 ← c도 바뀐다!

// SwiftUI에서의 선택 기준
// struct: 데이터 모델, View (SwiftUI View는 항상 struct)
// class: ObservableObject, 네트워크 매니저, 싱글톤

// struct에서 자신을 수정하는 함수 — mutating
struct Counter {
    var count = 0

    mutating func increment() {
        count += 1  // mutating 없으면 컴파일 에러
    }

    mutating func reset() {
        count = 0
    }
}
생각해보기
  • SwiftUI View가 struct인 이유는 무엇인가? class로 만들면 어떤 문제가 생기는가?
  • let a = SomeClass()로 선언된 클래스 인스턴스의 프로퍼티를 바꿀 수 있는가?
  • Copy-on-Write란 무엇이며 Swift의 Array/Dictionary가 이를 어떻게 활용하는가?
struct = 값 타입
할당/전달 시 복사. 독립적인 상태. 스레드 안전. SwiftUI View, 데이터 모델에 적합
class = 참조 타입
할당/전달 시 같은 인스턴스를 공유. 상속 가능. ObservableObject, 공유 상태에 사용
mutating
struct의 메서드가 self(인스턴스)를 수정할 때 필수. enum의 메서드에도 적용
Topic 02

Computed Property

저장하지 않고 계산으로 만드는 프로퍼티
Swift
struct Rectangle {
    var width: Double
    var height: Double

    // Computed Property — get만 있으면 get 생략 가능
    var area: Double {
        width * height
    }

    // get + set
    var perimeter: Double {
        get { (width + height) * 2 }
        set { width = newValue / 2 - height }
    }
}

var rect = Rectangle(width: 10, height: 5)
print(rect.area)      // 50.0
print(rect.perimeter) // 30.0

// SwiftUI에서 자주 쓰는 패턴
struct UserProfile {
    var firstName: String
    var lastName: String

    // 저장하지 않고 조합
    var fullName: String { "\(firstName) \(lastName)" }

    // 조건 판단
    var isNameValid: Bool { !firstName.isEmpty && !lastName.isEmpty }

    // 포맷팅
    var initials: String {
        let f = firstName.first.map(String.init) ?? ""
        let l = lastName.first.map(String.init) ?? ""
        return f + l
    }
}
생각해보기
  • computed property와 함수 중 어느 것을 써야 하는가? 계산 비용이 클 때는?
  • read-only computed property에서 get { }을 생략할 수 있는 이유는?
  • setnewValue는 어떻게 이름을 바꿀 수 있는가?
read-only computed property
get { } 블록만 있으면 생략 가능. var area: Double { width * height }
get + set
set에서 newValue로 새 값 접근. 양방향 변환이 필요할 때 (온도: 섭씨 ↔ 화씨)
함수 vs computed property
파라미터가 없고 부작용이 없는 계산 → computed property. 부작용이 있거나 파라미터가 필요 → 함수
Topic 03

Property Observer — willSet / didSet

프로퍼티 값이 바뀌기 전/후에 코드를 실행하는 방법
Swift
class StepCounter {
    var steps: Int = 0 {
        willSet {
            // 바뀌기 직전 — newValue로 새 값 접근
            print("곧 \(newValue)로 변경됩니다")
        }
        didSet {
            // 바뀐 직후 — oldValue로 이전 값 접근
            print("\(oldValue)에서 \(steps)으로 변경됨")
            if steps >= 10000 {
                print("목표 달성!")
            }
        }
    }
}

// SwiftUI ViewModel에서 실용적인 패턴
class CartViewModel: ObservableObject {
    @Published var items: [CartItem] = [] {
        didSet {
            // 아이템이 바뀔 때마다 자동 저장
            save()
        }
    }

    @Published var searchText: String = "" {
        didSet {
            // 검색어 변경 시 결과 갱신 (단순 필터)
            filterItems()
        }
    }

    private func save() { /* UserDefaults 저장 */ }
    private func filterItems() { /* 필터링 */ }
}

// 주의: @State에는 didSet을 직접 붙일 수 없다
// → .onChange(of:) modifier 사용
struct ContentView: View {
    @State private var count = 0

    var body: some View {
        Stepper("\(count)", value: $count)
            .onChange(of: count) { old, new in
                print("\(old) → \(new)")
            }
    }
}
생각해보기
  • willSet에서 값을 바꿀 수 있는가? didSet에서는?
  • init 내에서 프로퍼티에 값을 할당할 때 didSet이 호출되는가?
  • SwiftUI의 @StatedidSet을 쓸 수 없는 이유는?
willSet
값이 바뀌기 직전 호출. newValue로 새 값 접근. 값을 바꿀 수는 없다
didSet
값이 바뀐 직후 호출. oldValue로 이전 값 접근. didSet 안에서 같은 프로퍼티를 수정하면 재귀 호출 없이 한 번만 실행
init에서는 호출 안 됨
이니셜라이저 내부 할당에서는 observer가 호출되지 않는다. 의도적 설계
Topic 04

Access Control — 접근 제어

외부에 무엇을 보여주고 무엇을 숨길지 결정하는 방법
Swift
// 접근 제어 수준 (넓은 순서)
// open > public > internal(기본값) > fileprivate > private

class BankAccount {
    // private: 이 클래스 안에서만 접근 가능
    private var balance: Double = 0

    // internal(기본값): 같은 모듈에서 접근 가능
    var owner: String

    // public: 외부 모듈에서도 읽기 가능
    public var accountNumber: String

    init(owner: String, number: String) {
        self.owner = owner
        self.accountNumber = number
    }

    // private(set): 읽기는 내부/외부 모두, 쓰기는 내부만
    private(set) var transactionCount = 0

    func deposit(_ amount: Double) {
        guard amount > 0 else { return }
        balance += amount
        transactionCount += 1
    }

    func withdraw(_ amount: Double) -> Bool {
        guard amount <= balance else { return false }
        balance -= amount
        transactionCount += 1
        return true
    }
}

// SwiftUI ViewModel에서 실용적인 패턴
class CounterViewModel: ObservableObject {
    @Published private(set) var count = 0  // 외부에서 읽기만 가능

    func increment() { count += 1 }   // 내부에서만 수정
    func decrement() { count -= 1 }
    func reset()     { count = 0  }
}
생각해보기
  • privatefileprivate의 차이는? 같은 파일의 extension에서 private 멤버에 접근할 수 있는가?
  • @Published private(set) var count처럼 쓰는 이유는 무엇인가?
  • 접근 제어를 안 쓰면 어떤 문제가 생기는가? 팀 프로젝트에서의 의미는?
private
선언된 타입 + 같은 파일의 extension 안에서만 접근. 가장 자주 쓰는 수준
internal (기본값)
키워드를 안 쓰면 내부(internal). 같은 모듈(앱 타깃) 전체에서 접근 가능
private(set)
읽기는 누구나, 쓰기는 내부만. ViewModel의 @Published 프로퍼티 보호에 자주 사용
Topic 05

init — 이니셜라이저 패턴

인스턴스를 안전하게 초기화하는 방법
Swift
// struct: 자동으로 memberwise init 생성
struct Point {
    var x: Double
    var y: Double
    // Swift가 자동으로 init(x:y:) 생성
}
let p = Point(x: 1, y: 2)

// 커스텀 init을 추가하면 memberwise init이 사라짐
// extension으로 분리하면 두 가지 모두 유지 가능
extension Point {
    init(value: Double) {
        self.x = value
        self.y = value
    }
}

// class: 직접 init을 작성해야 함
class Person {
    let name: String
    var age: Int
    var email: String?   // Optional이면 init에서 안 써도 됨

    // 지정 이니셜라이저 (Designated)
    init(name: String, age: Int) {
        self.name = name
        self.age = age
    }

    // 편의 이니셜라이저 (Convenience) — 다른 init 재사용
    convenience init(name: String) {
        self.init(name: name, age: 0)
    }
}

// Failable init — 실패할 수 있는 초기화
struct Color {
    let hex: String

    init?(hex: String) {  // ? 주의
        guard hex.hasPrefix("#"), hex.count == 7 else {
            return nil  // 초기화 실패 → nil 반환
        }
        self.hex = hex
    }
}

let valid = Color(hex: "#FF6B2B")   // Optional
let invalid = Color(hex: "bad")     // nil
생각해보기
  • struct에 커스텀 init을 추가했더니 기존 코드가 컴파일 에러가 났다. 왜인가? 어떻게 해결하는가?
  • convenience init은 왜 반드시 self.init(...)을 호출해야 하는가?
  • init?(failable)을 쓰는 것과 throws로 에러를 던지는 것의 차이는?
memberwise init (struct 자동 생성)
커스텀 init이 없으면 모든 stored property를 파라미터로 받는 init이 자동 생성. extension으로 추가하면 두 가지 모두 유지
convenience init
class에서 다른 designated init을 재사용하는 편의 이니셜라이저. 반드시 같은 클래스의 init을 호출해야 함
failable init — init?
초기화 조건이 충족되지 않으면 nil 반환. 결과가 Optional. URL, 형변환 등에서 자주 사용
Topic 06

Type Casting — as? / as! / is

런타임에 타입을 확인하고 변환하는 방법
Swift
class Animal { var name: String; init(_ n: String) { name = n } }
class Dog: Animal { func bark() { print("멍!") } }
class Cat: Animal { func meow() { print("야옹") } }

let animals: [Animal] = [Dog("바둑이"), Cat("나비"), Dog("흰둥이")]

// is — 타입 확인
for animal in animals {
    if animal is Dog {
        print("\(animal.name)은 강아지")
    }
}

// as? — 안전한 다운캐스팅 (Optional 반환)
for animal in animals {
    if let dog = animal as? Dog {
        dog.bark()  // Dog 전용 메서드 호출 가능
    }
}

// as! — 강제 다운캐스팅 (실패 시 크래시)
let firstAnimal = animals[0]
let dog = firstAnimal as! Dog  // 확실할 때만 사용

// 업캐스팅 as (항상 성공, 옵셔널 불필요)
let d = Dog("강아지")
let a: Animal = d as Animal

// 실전: Codable + Any 타입 처리
let json: [String: Any] = ["count": 5, "name": "test"]
if let count = json["count"] as? Int {
    print(count) // 5
}

// switch + pattern matching
for animal in animals {
    switch animal {
    case let dog as Dog:
        dog.bark()
    case let cat as Cat:
        cat.meow()
    default:
        break
    }
}
생각해보기
  • as?를 쓰지 않고 as!를 남용하면 어떤 문제가 생기는가?
  • 업캐스팅(자식 → 부모)과 다운캐스팅(부모 → 자식) 중 어느 것이 안전하고 왜인가?
  • SwiftUI에서 [any View] 대신 AnyView를 써야 하는 이유는?
as? — 조건부 캐스팅
실패 시 nil 반환. if let과 함께 안전하게 사용. 실무에서 가장 많이 쓰는 형태
as! — 강제 캐스팅
타입이 확실할 때만 사용. 실패 시 런타임 크래시. 확실하지 않으면 항상 as?를 쓸 것
is — 타입 체크
Bool 반환. switch문의 case에서 case let x as Type 패턴으로도 활용
Topic 07

static · lazy · inout

코드를 더 유연하게 만드는 세 가지 키워드
Swift
// static — 인스턴스가 아닌 타입에 속하는 멤버
struct AppConfig {
    static let baseURL = "https://api.example.com"
    static let timeout: TimeInterval = 30

    static func makeURL(_ path: String) -> URL? {
        URL(string: baseURL + path)
    }
}

AppConfig.baseURL  // 인스턴스 없이 접근
AppConfig.makeURL("/posts")

// 싱글톤 패턴
class NetworkManager {
    static let shared = NetworkManager()
    private init() {}  // 외부에서 생성 불가
}

// lazy — 처음 접근할 때 초기화
class DataManager {
    // 비용이 큰 초기화를 처음 쓸 때까지 미룸
    lazy var cache: [String: Data] = [:]

    lazy var formatter: DateFormatter = {
        let f = DateFormatter()
        f.dateStyle = .medium
        f.locale = Locale(identifier: "ko_KR")
        return f
    }()
}

// inout — 함수 밖 변수를 직접 수정
func doubleValue(_ value: inout Int) {
    value *= 2
}

var number = 5
doubleValue(&number)  // & 필수
print(number) // 10

// swap 구현
func swap(_ a: inout T, _ b: inout T) {
    let temp = a
    a = b
    b = temp
}
생각해보기
  • staticclass 키워드의 차이는? struct에서 class를 쓸 수 있는가?
  • lazy var를 struct에서 쓰면 mutating이 필요한 이유는?
  • inout과 반환값으로 수정된 값을 돌려주는 방식의 차이는? 어느 것이 더 Swift스러운가?
static
타입 자체에 속하는 프로퍼티/메서드. 인스턴스 없이 접근. 상수, 유틸리티 함수, 싱글톤에 활용
lazy var
처음 접근 시 초기화. 비용이 큰 객체(DateFormatter, 큰 배열)에 적합. let에는 사용 불가
inout
함수가 파라미터를 직접 수정. 호출 시 & 필요. 함수형 프로그래밍 관점에서는 반환값 선호
Topic 08

실전에서 자주 만나는 패턴

앞에서 배운 문법이 실제 코드에서 어떻게 결합되는가
Swift
// 실전 ViewModel — 지금까지 배운 문법 총동원
@MainActor
class ProductViewModel: ObservableObject {

    // private(set): 외부 읽기 O, 쓰기 X
    @Published private(set) var products: [Product] = []
    @Published private(set) var isLoading = false

    // computed property: 파생 데이터
    var discountedProducts: [Product] {
        products.filter { $0.discount > 0 }
    }

    var totalCount: Int { products.count }

    // static: 공유 상수
    static let maxItemsPerPage = 20

    // lazy: 처음 쓸 때만 생성
    private lazy var cache: [Int: Product] = [:]

    // didSet: 캐시 동기화
    // (내부적으로 products 업데이트 시 활용)

    func load() async {
        isLoading = true
        defer { isLoading = false }  // defer: 블록 종료 시 항상 실행
        do {
            products = try await ProductService.fetchAll()
        } catch {
            print(error.localizedDescription)
        }
    }
}

// struct 모델 — 값 타입
struct Product: Identifiable, Codable {
    let id: Int
    let name: String
    let price: Double
    var discount: Double = 0

    // computed property
    var finalPrice: Double { price * (1 - discount) }
    var isOnSale: Bool { discount > 0 }

    // static factory method
    static func mock() -> Product {
        Product(id: 0, name: "테스트 상품", price: 9900)
    }
}
생각해보기
  • 이 코드에서 defer가 하는 역할은? isLoading = false를 함수 끝에 쓰는 것과 어떤 차이가 있는가?
  • Product를 struct로, ProductViewModel을 class로 만든 이유는?
  • @Published private(set)를 쓰는 것과 @Published private를 쓰는 것의 차이는?
defer
블록이 종료될 때(return/throw/정상 종료 모두) 실행. 정리 코드(isLoading = false, 파일 닫기)에 유용
Static factory method
static func mock() 패턴. Preview나 테스트에서 샘플 데이터 생성에 유용
모든 문법의 연결
각 문법은 독립적으로 배우지만, 실제 코드에서는 조합해서 사용. 읽는 훈련이 쓰는 것보다 먼저다
struct vs class
STRUCT (복사)
a = {x:1} → b = {x:1}
b.x = 99
a.x = 1 ← 변하지 않음
CLASS (참조)
c → d (같은 객체)
d.x = 99
c.x = 99 ← 같이 변함!
Computed Property
var area: Double {
  width * height
}
// 저장 ✗, 계산 ✓
RESULT
10 × 5 = 50.0
willSet / didSet
6,500
willSet → 7,000
didSet ← 6,500
목표까지 3,500보
Access Control
BANK ACCOUNT
balance private 🔒
owner internal
deposit() public
init 패턴
MEMBERWISE (자동)
Point(x: 1, y: 2)
CONVENIENCE
Person(name: "홍길동")
FAILABLE (init?)
Color(hex: "bad") → nil
Type Casting
[Animal] 배열
🐕 as? Dog → bark() ✓
🐈 as? Dog → nil
as? 실패 시 nil, 크래시 없음
static · lazy
static let shared
  = Manager()
lazy var cache
  = [String: Data]()
lazy: 처음 쓸 때만 초기화
종합 패턴
ProductViewModel
private(set) products
computed: discounted
lazy cache
defer: isLoading
Homework

Swift 핵심 문법을 익혔다면

필수
BankAccount 클래스 리팩터링
balance를 private으로, 잔액을 computed property로 외부 노출, 입출금 기록을 didSet으로 자동 저장, failable init으로 초기 잔액 유효성 검사 추가
필수
Stage 1 Todo 앱 리팩터링
기존 Todo 앱에 access control 적용, computed property로 완료 개수/미완료 개수 추출, static으로 샘플 데이터 생성 메서드 추가
도전
Generic Stack 자료구조 구현
push / pop / peek / isEmpty를 가진 Generic Stack struct 구현. pop()은 failable(Optional 반환), Equatable을 준수하는 타입에서만 contains() 사용 가능하도록 extension + where 절 추가
완료 체크리스트
computed property를 언제 쓰는지 알고, SwiftUI View에서 직접 활용할 수 있다
struct와 class의 차이를 설명하고, 각각 언제 써야 하는지 이유를 말할 수 있다
private / private(set) / internal을 적절히 사용할 수 있다
as? 와 as! 의 차이를 알고, 실제 코드에서 안전하게 사용할 수 있다
← Stage 1 마지막 1-4 막히는 Swift 문법
코멘트
후원하기
콘텐츠가 도움이 됐다면
커피 한 잔의 후원을 부탁드려요 :)
카카오페이 QR
이현호
카카오페이