Stage 2에서 쓸 핵심 패턴을 미리 익힌다
// 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
}
}
let a = SomeClass()로 선언된 클래스 인스턴스의 프로퍼티를 바꿀 수 있는가?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
}
}
get { }을 생략할 수 있는 이유는?set의 newValue는 어떻게 이름을 바꿀 수 있는가?get { } 블록만 있으면 생략 가능. var area: Double { width * height }set에서 newValue로 새 값 접근. 양방향 변환이 필요할 때 (온도: 섭씨 ↔ 화씨)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에서는?didSet이 호출되는가?@State에 didSet을 쓸 수 없는 이유는?newValue로 새 값 접근. 값을 바꿀 수는 없다oldValue로 이전 값 접근. didSet 안에서 같은 프로퍼티를 수정하면 재귀 호출 없이 한 번만 실행// 접근 제어 수준 (넓은 순서)
// 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 }
}
private와 fileprivate의 차이는? 같은 파일의 extension에서 private 멤버에 접근할 수 있는가?@Published private(set) var count처럼 쓰는 이유는 무엇인가?// 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
convenience init은 왜 반드시 self.init(...)을 호출해야 하는가?init?(failable)을 쓰는 것과 throws로 에러를 던지는 것의 차이는?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!를 남용하면 어떤 문제가 생기는가?[any View] 대신 AnyView를 써야 하는 이유는?if let과 함께 안전하게 사용. 실무에서 가장 많이 쓰는 형태as?를 쓸 것case let x as Type 패턴으로도 활용// 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
}
static과 class 키워드의 차이는? struct에서 class를 쓸 수 있는가?lazy var를 struct에서 쓰면 mutating이 필요한 이유는?inout과 반환값으로 수정된 값을 돌려주는 방식의 차이는? 어느 것이 더 Swift스러운가?let에는 사용 불가& 필요. 함수형 프로그래밍 관점에서는 반환값 선호// 실전 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를 쓰는 것의 차이는?static func mock() 패턴. Preview나 테스트에서 샘플 데이터 생성에 유용balance를 private으로, 잔액을 computed property로 외부 노출, 입출금 기록을 didSet으로 자동 저장, failable init으로 초기 잔액 유효성 검사 추가