
Ruchit Shah의 May 2025 iOS Interview Handbook
Advanced Swift Interview Topics for iOS Developers 을 한국어로 번역후 MD로 변환한 내용입니다!
원문링크
설명:
.completeFileProtection과 함께 File Protection API 사용예제 (KeychainAccess 라이브러리 사용):
import KeychainAccess
let keychain = Keychain(service: "com.yourapp.bundleid")
do {
try keychain.set("my-secret-token", key: "userToken")
let token = try keychain.get("userToken")
print("Retrieved token: \(token ?? "nil")")
} catch {
print("Keychain error: \(error)")
}
설명:
예제 (네이티브 API 사용):
import Security
func saveToKeychain(value: String, forKey key: String) {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemAdd(query as CFDictionary, nil)
}
func getFromKeychain(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
대칭 암호화 (예: AES):
비대칭 암호화 (예: RSA, ECC):
| 특징 | 대칭 (AES) | 비대칭 (RSA) |
|---|---|---|
| 키 | 하나 (공유) | 두 개 (공개/개인) |
| 속도 | 빠름 | 느림 |
| 사용 사례 | 대량 암호화 | 키 교환, 디지털 서명 |
설명:
항상 HTTPS(TLS) 사용. 추가 암호화 레이어가 필요한 경우:
1. AES로 페이로드 암호화
2. 서버의 공개 RSA 키로 AES 키 암호화
3. 서버가 개인 키로 AES 키 복호화 후 페이로드 복호화
예제 (CryptoKit을 사용한 AES):
import CryptoKit
func encryptWithAES(message: String, key: SymmetricKey) -> Data {
let data = Data(message.utf8)
let sealedBox = try! AES.GCM.seal(data, using: key)
return sealedBox.combined!
}
func decryptWithAES(ciphertext: Data, key: SymmetricKey) -> String? {
let sealedBox = try! AES.GCM.SealedBox(combined: ciphertext)
let decryptedData = try! AES.GCM.open(sealedBox, using: key)
return String(data: decryptedData, encoding: .utf8)
}
// 사용 예
let key = SymmetricKey(size: .bits256)
let encrypted = encryptWithAES(message: "Hello, server!", key: key)
let decrypted = decryptWithAES(ciphertext: encrypted, key: key)
print(decrypted ?? "decryption failed")
CryptoKit (iOS 13+, 현대적인 Swift API)
Security 프레임워크
CommonCrypto
예제 (CryptoKit으로 SHA256 해싱):
import CryptoKit
let input = "password123"
let inputData = Data(input.utf8)
let hash = SHA256.hash(data: inputData)
let hashString = hash.compactMap { String(format: "%02x", $0) }.joined()
print("SHA256 Hash: \(hashString)")
프로젝트 구조:
SecureDataDemo/
├── SecureStorage.swift ← Keychain 함수
├── EncryptionManager.swift ← AES 암호화/복호화
└── ViewController.swift ← UI + 통합
SecureStorage.swift:
import Foundation
import Security
class SecureStorage {
static func save(value: String, forKey key: String) {
let data = Data(value.utf8)
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecValueData as String: data
]
SecItemDelete(query as CFDictionary) // 기존 항목 제거
SecItemAdd(query as CFDictionary, nil)
}
static func load(forKey key: String) -> String? {
let query: [String: Any] = [
kSecClass as String: kSecClassGenericPassword,
kSecAttrAccount as String: key,
kSecReturnData as String: true,
kSecMatchLimit as String: kSecMatchLimitOne
]
var item: CFTypeRef?
let status = SecItemCopyMatching(query as CFDictionary, &item)
guard status == errSecSuccess, let data = item as? Data else {
return nil
}
return String(data: data, encoding: .utf8)
}
}
EncryptionManager.swift:
import Foundation
import CryptoKit
class EncryptionManager {
static let shared = EncryptionManager()
private let symmetricKey = SymmetricKey(size: .bits256)
func encrypt(message: String) -> Data? {
let data = Data(message.utf8)
guard let sealedBox = try? AES.GCM.seal(data, using: symmetricKey) else {
return nil
}
return sealedBox.combined
}
func decrypt(ciphertext: Data) -> String? {
guard let sealedBox = try? AES.GCM.SealedBox(combined: ciphertext),
let decryptedData = try? AES.GCM.open(sealedBox, using: symmetricKey) else {
return nil
}
return String(data: decryptedData, encoding: .utf8)
}
}
ViewController.swift:
import UIKit
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
simulateSecureDataFlow()
}
func simulateSecureDataFlow() {
let token = "super_secure_token_9876"
// Keychain에 저장
SecureStorage.save(value: token, forKey: "authToken")
if let storedToken = SecureStorage.load(forKey: "authToken") {
print("✅ Token from Keychain: \(storedToken)")
// API로 전송하기 전 토큰 암호화
if let encrypted = EncryptionManager.shared.encrypt(message: storedToken) {
print("🔐 Encrypted Data: \(encrypted.base64EncodedString())")
// 서버에서 받은 데이터라고 가정
let serverReceived = encrypted
// 서버 측에서 복호화
if let decrypted = EncryptionManager.shared.decrypt(ciphertext: serverReceived) {
print("🔓 Decrypted on server: \(decrypted)")
}
}
}
}
}
요구사항:
정의:
클로저의 특징:
문법 예제:
let greet = { (name: String) -> String in
return "Hello, \(name)!"
}
print(greet("Ruchit")) // 출력: Hello, Ruchit!
실제 사용 예제:
let names = ["Zara", "Ruchit", "Amit", "Bhavik"]
let sorted = names.sorted(by: { $0 < $1 })
print(sorted)
정의:
예제 (일반 vs Trailing):
// Trailing closure 없이
UIView.animate(withDuration: 0.3, animations: {
view.alpha = 0
})
// Trailing closure 사용
UIView.animate(withDuration: 0.3) {
view.alpha = 0
}
정의:
예제:
func makeCounter() -> () -> Int {
var count = 0
return {
count += 1
return count
}
}
let counter = makeCounter()
print(counter()) // 1
print(counter()) // 2
정의:
문제 예제:
class ViewModel {
var message = "Hi"
var handler: (() -> Void)?
func setup() {
handler = {
print(self.message) // ❌ 강한 참조 사이클 발생
}
}
}
해결 방법: [weak self] 또는 [unowned self] 사용
func setup() {
handler = { [weak self] in
print(self?.message ?? "")
}
}
차이점:
weak: self를 옵셔널로 만들고 강한 참조 방지unowned: self가 절대 nil이 되지 않을 것이 확실할 때 사용 (권장하지 않음)요약 표:
| 개념 | 설명 | 예제 |
|---|---|---|
| Closure | 재사용 가능한 코드 블록 | { name in "Hello, \(name)" } |
| Trailing Closure | 함수 괄호 밖에 전달된 클로저 | someFunction { ... } |
| 값 캡처 | 클로저가 컨텍스트 변수를 기억 | 카운터에서 count 캡처 |
| Retain Cycle | 클로저와 클래스 간 강한 참조로 인한 메모리 누수 | 클로저 내부의 self |
| 회피 방법 | [weak self] 또는 [unowned self] 사용 | handler = { [weak self] in ... } |
정의:
작동 방식:
예제:
class Person {
var name: String
init(name: String) {
self.name = name
print("\(name) is initialized")
}
deinit {
print("\(name) is deallocated")
}
}
var person1: Person? = Person(name: "Ruchit")
person1 = nil // 참조 카운트가 0으로 떨어져 인스턴스가 해제됨
| 참조 타입 | 객체 유지? | 옵셔널? | 사용 사례 |
|---|---|---|---|
| strong | ✅ Yes | ❌ No | 기본값, 메모리 유지 |
| weak | ❌ No | ✅ Yes | Retain cycle 방지, 객체가 nil이 될 수 있을 때 |
| unowned | ❌ No | ❌ No | Retain cycle 방지, 객체가 항상 존재할 것이 보장될 때 |
Strong 예제 (기본값):
class Dog {
var name: String
init(name: String) { self.name = name }
}
class Owner {
var dog: Dog // 기본적으로 강한 참조
}
Weak 예제:
class Dog {
var name: String
init(name: String) { self.name = name }
}
class Owner {
weak var dog: Dog? // 참조 카운트를 증가시키지 않음
}
Retain Cycle (순환 참조):
클래스 간 Retain Cycle:
class A {
var b: B?
}
class B {
var a: A?
}
var objA: A? = A()
var objB: B? = B()
objA?.b = objB
objB?.a = objA
objA = nil
objB = nil // ❌ 둘 다 해제되지 않음 — 메모리 누수!
해결 방법 (weak 또는 unowned 사용):
class B {
weak var a: A? // Retain cycle 방지
}
클로저 Retain Cycle 예제:
class ViewModel {
var name = "Ruchit"
var printName: (() -> Void)?
func setup() {
printName = {
print(self.name) // ❌ self가 강하게 캡처됨 → retain cycle
}
}
}
[weak self]로 해결:
func setup() {
printName = { [weak self] in
print(self?.name ?? "")
}
}
| 사용 시점 | weak 사용 | unowned 사용 |
|---|---|---|
| 객체가 nil이 될 수 있음 | ✅ Yes | ❌ No (해제되면 크래시) |
| 객체가 항상 존재함이 보장됨 | ❌ Optional | 더 안전, 옵셔널 언래핑 불필요 |
실제 예제: 클로저에서 unowned 사용
class ViewController {
var title = "Main"
func doSomething() {
performAction {
print(self.title) // 주의하지 않으면 Retain cycle
}
}
func performAction(_ completion: @escaping () -> Void) {
completion()
}
}
// 개선된 방법:
performAction { [unowned self] in
print(title) // self가 유지되지 않으며 옵셔널이 아님
}
⚠️ 주의: self가 실행 시점에 100% 존재할 것이 확실할 때만 unowned를 사용하세요. 해제될 가능성이 있다면 weak를 사용하세요.
요약:
| 용어 | 설명 |
|---|---|
| ARC | 강한 참조를 추적하여 자동으로 메모리 관리 |
| strong | 기본 참조, 객체를 유지함 |
| weak | 유지하지 않으며, 옵셔널 값과 함께 retain cycle 방지 |
| unowned | 유지하지 않으며, 옵셔널이 아니고 객체가 항상 존재한다고 가정 |
| Retain Cycle | 두 객체가 서로 강하게 참조 (또는 클로저가 self 캡처) |
SOLID는 소프트웨어를 더 유지보수 가능하고, 확장 가능하며, 테스트 가능하게 만드는 5가지 설계 원칙의 약어입니다. Robert C. Martin (Uncle Bob)이 소개했습니다.
| 원칙 | 목적 | 키워드 |
|---|---|---|
| S - Single Responsibility | 클래스당 하나의 역할 | 단일 책임 |
| O - Open/Closed | 확장 가능, 수정 불가 | 개방-폐쇄 |
| L - Liskov Substitution | 하위 타입이 상위 타입처럼 동작 | 리스코프 치환 |
| I - Interface Segregation | 작고 역할별 프로토콜 선호 | 인터페이스 분리 |
| D - Dependency Inversion | 추상화에 의존, 구체적 타입 아님 | 의존성 역전 |
정의:
클래스는 변경해야 할 이유가 하나만 있어야 합니다. 즉, 한 가지 일만 해야 합니다.
나쁜 예 - ViewController:
class UserProfileViewController: UIViewController {
func fetchUserData() { /* API 로직 */ }
func parseJSON() { /* JSON 파싱 */ }
func updateUI() { /* UI 로직 */ }
}
❌ 이 ViewController는 네트워킹, 파싱, UI를 처리 = 너무 많은 책임
SRP를 적용한 리팩토링:
class UserService {
func fetchUser(completion: @escaping (User) -> Void) {
/* API 호출 */
}
}
class UserProfileViewModel {
var user: User?
let service = UserService()
}
class UserProfileViewController: UIViewController {
var viewModel = UserProfileViewModel()
func updateUI() { /* UI 렌더링만 */ }
}
✅ 각 클래스가 하나의 책임을 처리: 네트워킹, 데이터 보유, UI
정의:
소프트웨어 엔티티(클래스, 함수)는 확장에는 열려 있어야 하지만 수정에는 닫혀 있어야 합니다.
Strategy 패턴 사용 예제:
protocol PaymentMethod {
func pay(amount: Double)
}
class CreditCardPayment: PaymentMethod {
func pay(amount: Double) {
print("Paid with credit card")
}
}
class PayPalPayment: PaymentMethod {
func pay(amount: Double) {
print("Paid with PayPal")
}
}
class PaymentProcessor {
var method: PaymentMethod
init(method: PaymentMethod) {
self.method = method
}
func processPayment(amount: Double) {
method.pay(amount: amount)
}
}
✅ 기존 로직을 수정하지 않고 새 결제 방식(예: ApplePay) 추가 가능
정의:
하위 클래스의 객체는 동작을 깨뜨리지 않고 상위 클래스의 객체로 대체 가능해야 합니다.
위반 예제:
class Bird {
func fly() {}
}
class Penguin: Bird {
override func fly() {
// 펭귄은 날 수 없음 — 잘못된 동작
}
}
❌ Penguin은 Bird처럼 동작할 수 없습니다. 계약을 위반합니다.
리팩토링:
protocol Bird { }
protocol FlyingBird: Bird {
func fly()
}
class Eagle: FlyingBird {
func fly() { print("Eagle flies") }
}
class Penguin: Bird { }
✅ 이제 FlyingBird만 fly()를 준수하여 LSP를 존중합니다
정의:
하나의 크고 범용적인 프로토콜보다 여러 개의 작고 구체적인 프로토콜을 선호합니다.
나쁜 예제:
protocol Vehicle {
func drive()
func fly()
}
class Car: Vehicle {
func drive() {}
func fly() {} // ❌ Car와 관련 없음
}
더 나은 설계:
protocol Drivable {
func drive()
}
protocol Flyable {
func fly()
}
class Car: Drivable {
func drive() {}
}
✅ 클래스는 필요한 것만 구현
정의:
고수준 모듈은 저수준 모듈에 의존해서는 안 됩니다. 둘 다 추상화에 의존해야 합니다.
강하게 결합된 코드:
class UserManager {
let db = FirebaseManager() // 하드코딩된 의존성
}
추상화를 통한 분리:
protocol DatabaseManager {
func save(data: String)
}
class FirebaseManager: DatabaseManager {
func save(data: String) { /* Firebase 코드 */ }
}
class UserManager {
let db: DatabaseManager
init(db: DatabaseManager) {
self.db = db
}
func saveUser() {
db.save(data: "User")
}
}
✅ 이제 테스트나 런타임 시 어떤 DatabaseManager든 주입 가능 (예: CoreData, REST)
| 원칙 | iOS에서 사용처 |
|---|---|
| SRP | ViewController에서 네트워킹, 파싱, UI 분리 |
| OCP | 프로토콜/확장으로 기능 추가 (예: 결제, 테마) |
| LSP | 재사용 가능한 컴포넌트를 위한 프로토콜 지향 프로그래밍 |
| ISP | 모듈식 프로토콜 설계 (예: UITableViewDataSource, Delegate) |
| DIP | 서비스, 코디네이터, 매니저를 위한 프로토콜 기반 아키텍처 |
// MARK: - 1. Single Responsibility Principle (SRP)
class UserService {
func fetchUser(completion: @escaping (String) -> Void) {
completion("Ruchit")
}
}
class UserProfileViewModel {
var userName: String?
let service = UserService()
func loadUser() {
service.fetchUser { [weak self] name in
self?.userName = name
}
}
}
class UserProfileViewController {
let viewModel = UserProfileViewModel()
func displayUser() {
viewModel.loadUser()
print("User: \(viewModel.userName ?? "")")
}
}
// MARK: - 2. Open/Closed Principle (OCP)
protocol PaymentMethod {
func pay(amount: Double)
}
class CreditCardPayment: PaymentMethod {
func pay(amount: Double) {
print("Paid \(amount) with Credit Card")
}
}
class PayPalPayment: PaymentMethod {
func pay(amount: Double) {
print("Paid \(amount) with PayPal")
}
}
class PaymentProcessor {
var method: PaymentMethod
init(method: PaymentMethod) {
self.method = method
}
func process(amount: Double) {
method.pay(amount: amount)
}
}
// MARK: - 3. Liskov Substitution Principle (LSP)
protocol Bird {}
protocol FlyingBird: Bird {
func fly()
}
class Eagle: FlyingBird {
func fly() {
print("Eagle soars high")
}
}
class Penguin: Bird {} // 날 수 없으므로 fly() 미구현
// MARK: - 4. Interface Segregation Principle (ISP)
protocol Drivable {
func drive()
}
protocol Flyable {
func fly()
}
class Car: Drivable {
func drive() {
print("Driving a car")
}
}
class Plane: Flyable {
func fly() {
print("Flying a plane")
}
}
// MARK: - 5. Dependency Inversion Principle (DIP)
protocol DatabaseManager {
func save(data: String)
}
class FirebaseManager: DatabaseManager {
func save(data: String) {
print("Saved to Firebase: \(data)")
}
}
class UserManager {
let db: DatabaseManager
init(db: DatabaseManager) {
self.db = db
}
func saveUser(name: String) {
db.save(data: name)
}
}
SwiftUI에서는 property wrapper를 사용하여 뷰 간 데이터를 전달하고 상태를 관리합니다.
뷰 내에서 로컬 상태를 저장하는 데 사용됩니다. 불린 토글이나 사용자 입력처럼 단순하고 변경 가능한 데이터에 주로 사용됩니다.
struct ContentView: View {
@State private var isOn = false
var body: some View {
Toggle("Switch", isOn: $isOn)
}
}
isOn은 로컬 상태 변수이고, $isOn은 해당 상태에 대한 바인딩입니다다른 뷰의 상태에 대한 양방향 연결을 생성합니다. 일반적으로 부모 뷰에서 자식 뷰로 전달되며, 자식 뷰가 부모 뷰의 상태를 수정할 수 있게 합니다.
struct ParentView: View {
@State private var isToggled = false
var body: some View {
ChildView(isOn: $isToggled)
}
}
struct ChildView: View {
@Binding var isOn: Bool
var body: some View {
Toggle("Child Switch", isOn: $isOn)
}
}
ObservableObject 프로토콜을 준수하는 외부 상태 객체의 변경사항을 관찰하는 데 사용됩니다. 여러 뷰에서 데이터를 공유하고 데이터가 변경될 때 자동으로 업데이트하는 데 유용합니다.
class UserData: ObservableObject {
@Published var name: String = "John Doe"
}
struct ProfileView: View {
@ObservedObject var userData: UserData
var body: some View {
Text(userData.name)
}
}
전역적으로 공유되는 데이터에 사용됩니다. 뷰 계층 구조의 최상위에서 설정되며, 계층 구조의 모든 자식 뷰에서 접근할 수 있습니다.
class ThemeData: ObservableObject {
@Published var themeColor: Color = .blue
}
struct ContentView: View {
@EnvironmentObject var themeData: ThemeData
var body: some View {
Text("The theme color is \(themeData.themeColor.description)")
.foregroundColor(themeData.themeColor)
}
}
@EnvironmentObject 주입:
@main
struct MyApp: App {
@StateObject var themeData = ThemeData()
var body: some Scene {
WindowGroup {
ContentView()
.environmentObject(themeData)
}
}
}
SwiftUI에서는 뷰의 body에서 if, else, switch 문을 직접 사용하여 조건부 렌더링을 처리합니다.
struct ContentView: View {
@State private var isLoggedIn = false
var body: some View {
VStack {
if isLoggedIn {
Text("Welcome back!")
} else {
Text("Please log in.")
}
Button(action: {
isLoggedIn.toggle()
}) {
Text(isLoggedIn ? "Log Out" : "Log In")
}
}
}
}
SwiftUI:
@State 또는 @ObservedObject가 변경되면 SwiftUI가 자동으로 UI를 업데이트UIKit:
setNeedsLayout() 또는 reloadData() 호출 필요tableView.reloadData() 호출ViewModifier는 스타일링이나 변환을 재사용 가능한 방식으로 뷰에 적용하는 데 사용됩니다.
struct RedBorderModifier: ViewModifier {
func body(content: Content) -> some View {
content
.border(Color.red, width: 5)
.padding()
}
}
extension View {
func redBorder() -> some View {
self.modifier(RedBorderModifier())
}
}
struct ContentView: View {
var body: some View {
Text("Hello, World!")
.redBorder()
}
}
정의:
Property wrapper는 프로퍼티의 동작을 캡슐화하는 제네릭 구조체 또는 클래스입니다. 프로퍼티 선언을 깔끔하고 재사용 가능하게 유지하면서 프로퍼티 접근(getter와 setter) 주변에 로직을 추가하는 방법을 제공합니다.
@ 기호를 사용하여 정의되며 모든 프로퍼티에 적용할 수 있습니다.
기본 예제:
@propertyWrapper
struct Uppercase {
private var value: String
init(wrappedValue: String) {
self.value = wrappedValue
}
var wrappedValue: String {
get { value }
set { value = newValue.uppercased() }
}
}
struct User {
@Uppercase var name: String
}
let user = User(name: "john doe")
print(user.name) // 출력: "JOHN DOE"
둘 다 SwiftUI에서 제공하는 데이터 바인딩, 지속성 및 UI 업데이트를 처리하는 특수 property wrapper입니다.
뷰의 변경 가능한 상태를 관리합니다. 상태가 변경되면 SwiftUI가 뷰를 다시 렌더링합니다.
내부 동작:
struct ContentView: View {
@State private var counter = 0
var body: some View {
VStack {
Text("Counter: \(counter)")
Button("Increment") {
counter += 1
}
}
}
}
UserDefaults 시스템에서 데이터를 저장하고 검색하는 방법을 제공하며 자동 동기화됩니다.
내부 동작:
struct ContentView: View {
@AppStorage("userName") private var userName: String = "Guest"
var body: some View {
Text("Hello, \(userName)!")
}
}
사용 사례: 이메일 형식 검증
@propertyWrapper
struct ValidEmail {
private var email: String
init(wrappedValue: String) {
self.email = wrappedValue
}
var wrappedValue: String {
get { email }
set {
// 간단한 이메일 검증
if newValue.contains("@") {
email = newValue
} else {
email = "Invalid email"
}
}
}
}
struct User {
@ValidEmail var email: String
}
let user = User(email: "john.doe@example.com")
print(user.email) // 출력: "john.doe@example.com"
Property wrapper는 Codable과 함께 작동할 수 있지만 신중하게 관리해야 합니다. Codable은 프로퍼티가 인코딩 및 디코딩되어야 하므로 wrapper가 인코딩 및 디코딩 프로세스와 어떻게 상호작용하는지 정의해야 합니다.
Custom Codable Property Wrapper 예제:
@propertyWrapper
struct CodableString: Codable {
var value: String
init(wrappedValue: String) {
self.value = wrappedValue
}
var wrappedValue: String {
get { value }
set { value = newValue }
}
}
struct User: Codable {
@CodableString var name: String
}
let user = User(name: "John Doe")
// 인코딩
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
print(String(data: encoded, encoding: .utf8)!) // 출력: {"name":"John Doe"}
}
// 디코딩
let decoder = JSONDecoder()
if let decoded = try? decoder.decode(User.self, from: encoded) {
print(decoded.name) // 출력: "John Doe"
}
Swift에서 async/await는 비동기 코드를 더 읽기 쉽고 구조화되게 만드는 언어 기능입니다. 비동기 코드를 동기 코드처럼 보이게 작성할 수 있습니다.
예제:
import Foundation
// 네트워크 요청을 시뮬레이션하는 async 함수
func fetchData() async -> String {
// 네트워크 지연 시뮬레이션
await Task.sleep(2 * 1_000_000_000) // 2초
return "Data fetched"
}
// async 함수 호출
func loadData() async {
let data = await fetchData()
print(data)
}
Task {
await loadData()
}
정의:
Structured concurrency는 동시 작업의 실행을 구조화된 방식으로 조직하는 모델입니다. 작업에 명확한 범위와 수명이 부여되고 실행이 명시적으로 관리됩니다.
Swift 5.5는 Task와 TaskGroup을 통해 structured concurrency를 도입했습니다. 이 모델은 작업이 잘 정의된 범위 내에서 생성되도록 보장하여 수명 주기를 관리하고 의도하지 않은 작업 누수와 같은 문제를 방지합니다.
예제:
func fetchMultipleData() async {
async let data1 = fetchData()
async let data2 = fetchData()
// 둘 다 동시에 대기
let results = await [data1, data2]
print(results)
}
기존 API (Completion Handler):
func fetchDataWithCompletion(completion: @escaping (String?, Error?) -> Void) {
// 네트워크 요청 시뮬레이션
DispatchQueue.global().async {
// 지연 시뮬레이션
sleep(2)
completion("Data fetched", nil)
}
}
async/await로 변환:
func fetchData() async throws -> String {
return try await withCheckedThrowingContinuation { continuation in
fetchDataWithCompletion { data, error in
if let error = error {
continuation.resume(throwing: error)
} else if let data = data {
continuation.resume(returning: data)
}
}
}
}
withCheckedThrowingContinuation 함수가 completion handler 기반 API를 async/await로 연결합니다.
| 타입 | 설명 | 사용 사례 |
|---|---|---|
| Task | 현재 컨텍스트에 연결된 비동기 작업 단위. 생성된 범위가 끝나면 자동으로 취소됨 | 범위의 수명 주기를 따라야 하는 작업 |
| DetachedTask | 현재 컨텍스트나 범위에 연결되지 않은 비동기 작업. 독립적으로 실행되며 자동 취소되지 않음 | 자동으로 취소되지 않아야 하는 장기 실행 작업 |
| TaskGroup | 여러 비동기 작업을 동시에 실행하고 모두 완료될 때까지 기다리는 방법 | 여러 동시 작업 관리 |
Task 예제:
Task {
let result = await fetchData()
print(result)
}
DetachedTask 예제:
DetachedTask {
let result = await fetchData()
print(result)
}
TaskGroup 예제:
func fetchMultipleData() async {
await withTaskGroup(of: String.self) { group in
group.async {
return await fetchData()
}
group.async {
return await fetchData()
}
for await result in group {
print(result)
}
}
}
정의:
Copy-on-Write (COW)는 실제로 수정될 때까지 불필요한 데이터 복사를 피하는 메모리 최적화 기법입니다. Swift에서 Array, String, Dictionary 같은 데이터 구조는 COW를 구현합니다.
작동 방식:
Array에서 Copy-on-Write 시연:
var array1 = [1, 2, 3]
var array2 = array1 // 아직 복사 안 됨, array1과 array2가 같은 메모리 공유
array2.append(4) // array2를 수정하므로 복사가 트리거됨
print(array1) // [1, 2, 3]
print(array2) // [1, 2, 3, 4]
class DataStorage {
var data: [Int]
init(data: [Int]) {
self.data = data
}
}
struct CopyOnWriteStruct {
private var storage: DataStorage
init(data: [Int]) {
self.storage = DataStorage(data: data)
}
// 수정 시에만 고유한 복사본 보장
private mutating func copyIfNeeded() {
if !isKnownUniquelyReferenced(&storage) {
storage = DataStorage(data: storage.data)
}
}
var data: [Int] {
get { storage.data }
set {
copyIfNeeded()
storage.data = newValue
}
}
}
var struct1 = CopyOnWriteStruct(data: [1, 2, 3])
var struct2 = struct1 // 아직 복사 안 됨, struct1과 struct2가 같은 데이터 공유
struct2.data.append(4) // struct2가 수정될 때 복사 트리거
print(struct1.data) // [1, 2, 3]
print(struct2.data) // [1, 2, 3, 4]
설명:
DataStorage는 실제 데이터를 보유하는 클래스입니다copyIfNeeded() 메서드는 구조체가 고유하게 참조되지 않을 때만 데이터 복사본을 만듭니다data 프로퍼티는 데이터를 읽고 쓰며, 필요할 때만(즉, 변경이 발생할 때) 데이터가 복사되도록 보장합니다DispatchQueue.main.async:
DispatchQueue.main.async {
print("This is async on the main queue")
}
// 제어가 즉시 반환됨
DispatchQueue.main.sync:
.sync를 호출하면 데드락 발생 (자신이 완료되기를 기다리며 차단됨)DispatchQueue.main.sync {
print("This is sync on the main queue")
}
// 작업이 완료된 후 제어가 반환됨
핵심 차이:
.async: 논블로킹, 현재 스레드를 차단하지 않고 미래에 코드를 실행.sync: 블로킹, 호출자에게 반환되기 전에 코드가 완료될 때까지 대기Serial Queue:
let serialQueue = DispatchQueue(label: "com.example.serialQueue")
serialQueue.async {
print("Task 1")
}
serialQueue.async {
print("Task 2")
}
// "Task 1"이 완료된 후 "Task 2"가 시작됨
Concurrent Queue:
let concurrentQueue = DispatchQueue(label: "com.example.concurrentQueue",
attributes: .concurrent)
concurrentQueue.async {
print("Task 1")
}
concurrentQueue.async {
print("Task 2")
}
// "Task 1"과 "Task 2"가 동시에 실행될 수 있음
데드락은 두 스레드가 서로 완료되기를 기다릴 때 발생하여 무한 대기 상태가 됩니다.
데드락의 일반적인 원인:
.sync 호출데드락 피하기:
DispatchQueue.main.sync 호출하지 않기.sync 호출하지 않기데드락 예제:
DispatchQueue.main.sync {
DispatchQueue.main.sync {
// ❌ 데드락: 메인 스레드가 자신을 기다리며 차단됨
}
}
OperationQueue란?
더 구조화된 방식으로 작업(태스크)을 관리하고 조직할 수 있는 GCD의 상위 수준 추상화입니다.
| 기능 | 설명 |
|---|---|
| 작업 관리 | 우선순위, 의존성, 취소를 포함한 더 나은 작업 관리 |
| 실행 제어 | maxConcurrentOperationCount 프로퍼티로 최대 동시 작업 수 제어 |
| 취소 | 작업을 취소할 수 있음 (GCD는 불가능) |
| 의존성 | 작업 간 의존성 정의 가능 (작업 A가 완료된 후 작업 B 시작) |
예제:
let operationQueue = OperationQueue()
let op1 = BlockOperation {
print("Task 1")
}
let op2 = BlockOperation {
print("Task 2")
}
op2.addDependency(op1) // op1 완료 후 op2 시작
operationQueue.addOperations([op1, op2], waitUntilFinished: false)
cancel() 메서드를 호출하여 작업을 취소할 수 있습니다. 그러나 효과를 보려면 작업의 실행 코드 내에서 정기적으로 취소를 확인하고 취소되면 중지해야 합니다.
let operationQueue = OperationQueue()
let op = BlockOperation {
for i in 0..<10 {
if op.isCancelled {
print("Operation was cancelled")
return
}
print(i)
}
}
operationQueue.addOperation(op)
DispatchQueue.global().asyncAfter(deadline: .now() + 2) {
op.cancel() // 2초 후 작업 취소
}
addDependency() 메서드를 사용하여 작업 간 의존성을 정의할 수 있습니다.
let operationQueue = OperationQueue()
let op1 = BlockOperation {
print("Task 1")
}
let op2 = BlockOperation {
print("Task 2")
}
let op3 = BlockOperation {
print("Task 3")
}
op2.addDependency(op1) // op1 완료 후 op2 실행
op3.addDependency(op2) // op2 완료 후 op3 실행
operationQueue.addOperations([op1, op2, op3], waitUntilFinished: false)
실행 순서: op1 → op2 → op3
요약:
Class (클래스):
deinit) 지원class Dog {
var name: String
init(name: String) {
self.name = name
}
}
let dog1 = Dog(name: "Buddy")
let dog2 = dog1
dog2.name = "Max"
print(dog1.name) // 출력: Max (dog1과 dog2가 같은 객체 참조)
Struct (구조체):
init, ==(동등 비교) 등 자동 합성struct Dog {
var name: String
}
var dog1 = Dog(name: "Buddy")
var dog2 = dog1
dog2.name = "Max"
print(dog1.name) // 출력: Buddy (dog1과 dog2는 독립적)
Enum (열거형):
enum DogBreed {
case bulldog, poodle, labrador
}
var myDogBreed = DogBreed.bulldog
myDogBreed = .poodle
정의:
Swift가 메모리의 객체에 대한 참조를 추적하는 데 사용하는 메모리 관리 시스템입니다. ARC는 더 이상 사용되지 않는 객체의 메모리가 해제되도록 보장하여 메모리 누수를 방지합니다.
작동 방식:
class Car {
var brand: String
init(brand: String) {
self.brand = brand
}
deinit {
print("\(brand) is being deallocated")
}
}
var car1: Car? = Car(brand: "Tesla")
var car2 = car1 // 참조 카운트 = 2
car1 = nil // 참조 카운트 = 1
car2 = nil // 참조 카운트 = 0, "Tesla is being deallocated" 출력
Strong 참조:
var dog = Dog(name: "Buddy")
// 'dog'는 Dog 인스턴스에 대한 강한 참조
Weak 참조:
nil이 됨class Owner {
var name: String
init(name: String) {
self.name = name
}
}
class Dog {
var owner: Owner?
}
var owner1: Owner? = Owner(name: "John")
var dog1 = Dog()
dog1.owner = owner1
owner1 = nil // owner1이 해제되고 dog1.owner는 nil이 됨 (weak 참조)
Unowned 참조:
class Teacher {
var name: String
var student: Student?
init(name: String) {
self.name = name
}
}
class Student {
var name: String
var teacher: Teacher?
init(name: String) {
self.name = name
}
}
var teacher = Teacher(name: "Mr. Smith")
var student = Student(name: "Alice")
teacher.student = student
student.teacher = teacher // 강한 참조 사이클을 피하기 위한 unowned 참조
값 타입:
struct, enum, 기본 타입(Int, Double, Array, Dictionary 등)특징:
struct Point {
var x: Int
var y: Int
}
var point1 = Point(x: 1, y: 2)
var point2 = point1
point2.x = 10
print(point1.x) // 출력: 1
print(point2.x) // 출력: 10
참조 타입:
class, closure특징:
class Person {
var name: String
init(name: String) {
self.name = name
}
}
var person1 = Person(name: "John")
var person2 = person1
person2.name = "Steve"
print(person1.name) // 출력: Steve (두 변수가 같은 인스턴스 참조)
현대 Swift(Swift 5.5+)에서는 URLSession과 async/await를 사용하는 것이 선호됩니다. 이전에는 클로저나 Alamofire 같은 서드파티 라이브러리를 사용했습니다.
async/await 사용 예제 (Swift 5.5+):
struct Post: Codable {
let id: Int
let title: String
}
func fetchPosts() async throws -> [Post] {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
let (data, _) = try await URLSession.shared.data(from: url)
return try JSONDecoder().decode([Post].self, from: data)
}
Completion Handler 사용 (이전 방식):
func fetchPosts(completion: @escaping ([Post]?) -> Void) {
let url = URL(string: "https://jsonplaceholder.typicode.com/posts")!
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data else {
completion(nil)
return
}
let posts = try? JSONDecoder().decode([Post].self, from: data)
completion(posts)
}.resume()
}
Swift의 Codable 디코딩은 여러 이유로 실패할 수 있습니다(키 누락, 타입 불일치, 잘못된 형식 등). 이를 처리하려면 do-catch로 디코딩을 감싸고 선택적으로 에러를 검사합니다.
do {
let decoded = try JSONDecoder().decode([Post].self, from: data)
return decoded
} catch let DecodingError.keyNotFound(key, context) {
print("Missing key: \(key), context: \(context)")
} catch let DecodingError.typeMismatch(type, context) {
print("Type mismatch: \(type), context: \(context)")
} catch {
print("Decoding failed: \(error.localizedDescription)")
}
팁: 자세한 에러를 로그하려면 debugDescription 또는 localizedDescription 사용
Alamofire를 사용하는 이유:
때때로 피하는 이유:
실무 경험:
iOS에는 여러 일반적인 지속성 방법이 있으며 각각 다른 사용 사례에 적합합니다:
// 데이터 저장
UserDefaults.standard.set("dark", forKey: "theme")
// 데이터 검색
let theme = UserDefaults.standard.string(forKey: "theme")
// 엔티티 생성 및 저장
let context = persistentContainer.viewContext
let newEntity = Entity(context: context)
newEntity.attribute = "value"
do {
try context.save()
} catch {
print("Failed to save context: \(error)")
}
// 엔티티 가져오기
let request: NSFetchRequest<Entity> = Entity.fetchRequest()
do {
let entities = try context.fetch(request)
} catch {
print("Failed to fetch entities: \(error)")
}
import RealmSwift
class Cocktail: Object {
@objc dynamic var name = ""
@objc dynamic var isFavorite = false
}
let realm = try! Realm()
// 즐겨찾기 칵테일 저장
let cocktail = Cocktail()
cocktail.name = "Margarita"
cocktail.isFavorite = true
try! realm.write {
realm.add(cocktail)
}
// 모든 즐겨찾기 칵테일 가져오기
let favoriteCocktails = realm.objects(Cocktail.self).filter("isFavorite == true")
칵테일의 이름, 재료, 즐겨찾기 여부 같은 속성을 저장하는 앱을 만든다고 가정해봅시다.
CoreData 예제:
1. CoreData 모델 정의:
name: Stringingredients: String (또는 별도 엔티티로 저장하려면 관계 사용)isFavorite: Booleanimport CoreData
func addCocktail(name: String, ingredients: String, isFavorite: Bool) {
let context = persistentContainer.viewContext
let newCocktail = Cocktail(context: context)
newCocktail.name = name
newCocktail.ingredients = ingredients
newCocktail.isFavorite = isFavorite
do {
try context.save()
} catch {
print("Failed to save cocktail: \(error)")
}
}
func fetchFavoriteCocktails() -> [Cocktail] {
let context = persistentContainer.viewContext
let fetchRequest: NSFetchRequest<Cocktail> = Cocktail.fetchRequest()
fetchRequest.predicate = NSPredicate(format: "isFavorite == true")
do {
let favoriteCocktails = try context.fetch(fetchRequest)
return favoriteCocktails
} catch {
print("Failed to fetch favorite cocktails: \(error)")
return []
}
}
Realm 예제:
import RealmSwift
class Cocktail: Object {
@objc dynamic var name = ""
@objc dynamic var ingredients = ""
@objc dynamic var isFavorite = false
}
func addCocktailToRealm(name: String, ingredients: String, isFavorite: Bool) {
let cocktail = Cocktail()
cocktail.name = name
cocktail.ingredients = ingredients
cocktail.isFavorite = isFavorite
let realm = try! Realm()
try! realm.write {
realm.add(cocktail)
}
}
func fetchFavoriteCocktailsFromRealm() -> Results<Cocktail> {
let realm = try! Realm()
return realm.objects(Cocktail.self).filter("isFavorite == true")
}
Swift에서 단위 테스트는 Xcode에 통합된 XCTest 프레임워크를 사용하여 수행됩니다. XCTest를 통해 코드의 정확성을 검증하는 테스트를 작성하여 각 컴포넌트가 예상대로 작동하는지 확인할 수 있습니다.
기본 단위 테스트 예제:
테스트할 코드:
class Calculator {
func add(_ a: Int, _ b: Int) -> Int {
return a + b
}
}
테스트 작성:
import XCTest
@testable import YourApp
class CalculatorTests: XCTestCase {
var calculator: Calculator!
override func setUp() {
super.setUp()
calculator = Calculator()
}
override func tearDown() {
calculator = nil
super.tearDown()
}
func testAdd() {
let result = calculator.add(2, 3)
XCTAssertEqual(result, 5, "Add function should return the sum of two numbers")
}
}
주요 요소:
setUp(): 각 테스트 메서드 전에 리소스 설정tearDown(): 각 테스트 후 리소스 정리XCTAssertEqual: 두 값이 같은지 확인하는 내장 assertion테스트 실행:
Xcode에서 Command + U를 누르거나 Test Navigator에서 실행
iOS에서 UI 테스팅은 주로 XCTest 프레임워크의 일부인 XCUITest를 사용합니다. 사용자 상호작용을 시뮬레이션하고 UI가 예상대로 동작하는지 검증하는 데 도움을 줍니다.
UI 테스트 예제:
버튼 탭이 레이블 텍스트를 변경하는지 확인하는 간단한 UI 테스트:
import XCTest
class MyAppUITests: XCTestCase {
func testButtonTapChangesLabelText() {
let app = XCUIApplication()
app.launch()
let button = app.buttons["MyButton"]
let label = app.staticTexts["MyLabel"]
// 초기 레이블 텍스트 확인
XCTAssertEqual(label.label, "Initial Text")
// 버튼 탭
button.tap()
// 버튼 탭 후 레이블 텍스트 변경 확인
XCTAssertEqual(label.label, "Text After Tap")
}
}
주요 요소:
XCUIApplication: 테스트 중인 앱을 나타냄app.buttons["MyButton"]: accessibility identifier로 버튼 접근button.tap(): 버튼 탭 동작 시뮬레이션XCTAssertEqual: 탭 후 레이블 텍스트 검증Accessibility Identifier:
UI 테스트 중 쉽게 식별할 수 있도록 UI 요소에 항상 accessibility identifier 설정:
button.accessibilityIdentifier = "MyButton"
label.accessibilityIdentifier = "MyLabel"
비동기 네트워크 호출 테스트는 네트워크 응답을 시뮬레이션하고 expectation을 사용하여 호출이 완료될 때까지 기다려야 합니다.
네트워크 호출 예제 (async/await 사용):
class NetworkService {
func fetchData() async throws -> String {
let url = URL(string: "https://api.example.com/data")!
let (data, _) = try await URLSession.shared.data(from: url)
return String(data: data, encoding: .utf8) ?? ""
}
}
비동기 네트워크 호출 단위 테스트:
import XCTest
@testable import YourApp
class NetworkServiceTests: XCTestCase {
var networkService: NetworkService!
override func setUp() {
super.setUp()
networkService = NetworkService()
}
override func tearDown() {
networkService = nil
super.tearDown()
}
func testFetchData() async {
// Expectation 생성
let expectation = XCTestExpectation(description: "Async network call should complete successfully")
do {
let result = try await networkService.fetchData()
XCTAssertEqual(result, "Expected Result")
expectation.fulfill() // 비동기 호출 완료 시 expectation 충족
} catch {
XCTFail("Network call failed with error: \(error)")
}
// timeout 내에 expectation이 충족될 때까지 대기 (비동기 테스트에 중요)
await fulfillment(of: [expectation], timeout: 5.0)
}
}
주요 요소:
XCTestExpectation: 비동기 코드를 위한 expectation 생성expectation.fulfill(): expectation을 완료로 표시await fulfillment(of:timeout:): 주어진 timeout 내에 expectation이 충족될 때까지 대기네트워크 호출 모킹:
단위 테스트의 경우 실제 API 호출을 피하고 OHHTTPStubs 같은 라이브러리를 사용하여 네트워크 응답을 모킹할 수 있습니다.
면접이나 과제에서 제공될 수 있는 코드 예제와 함께 리팩토링하거나 문제를 찾는 작업:
문제가 있는 코드:
func calculateSquareOfEvenNumbers(numbers: [Int]) -> [Int] {
var result = [Int]()
for number in numbers {
if number % 2 == 0 {
result.append(number * number)
}
}
return result
}
func calculateSquareOfOddNumbers(numbers: [Int]) -> [Int] {
var result = [Int]()
for number in numbers {
if number % 2 != 0 {
result.append(number * number)
}
}
return result
}
문제: 중복된 루프 로직
리팩토링:
func calculateSquare(numbers: [Int], condition: (Int) -> Bool) -> [Int] {
return numbers.filter(condition).map { $0 * $0 }
}
let evenSquares = calculateSquare(numbers: [1,2,3,4]) { $0 % 2 == 0 }
let oddSquares = calculateSquare(numbers: [1,2,3,4]) { $0 % 2 != 0 }
문제가 있는 코드:
func concatenateStrings(strings: [String]) -> String {
var result = ""
for string in strings {
result += string
}
return result
}
문제: 루프에서 비효율적인 문자열 연결
리팩토링:
func concatenateStrings(strings: [String]) -> String {
return strings.joined()
}
문제가 있는 코드:
func checkForDuplicates(numbers: [Int]) -> Bool {
for i in 0..<numbers.count {
for j in i+1..<numbers.count {
if numbers[i] == numbers[j] {
return true
}
}
}
return false
}
문제: 중복 확인을 위한 중첩 루프는 피할 수 있음
리팩토링:
func checkForDuplicates(numbers: [Int]) -> Bool {
return Set(numbers).count != numbers.count
}
문제가 있는 코드:
func findMaxNumber(numbers: [Int]) -> Int? {
var maxNumber: Int? = nil
for number in numbers {
if maxNumber == nil || number > maxNumber! {
maxNumber = number
}
}
return maxNumber
}
문제: 최대값을 위해 수동으로 반복
리팩토링:
func findMaxNumber(numbers: [Int]) -> Int? {
return numbers.max()
}
문제가 있는 코드:
func isAdult(age: Int) -> Bool {
if age >= 18 {
return true
} else {
return false
}
}
문제: 중복된 코드
리팩토링:
func isAdult(age: Int) -> Bool {
return age >= 18
}
문제가 있는 코드:
func getDescription(age: Int) -> String {
if age >= 18 {
return "Adult"
} else {
return 18 // ❌ 다른 타입 반환
}
}
문제: 다른 타입(String과 Int) 반환
리팩토링:
func getDescription(age: Int) -> String {
return age >= 18 ? "Adult" : "Minor"
}
문제가 있는 코드:
func findName(forId id: Int, in dictionary: [Int: String]) -> String? {
for key in dictionary.keys {
if key == id {
return dictionary[key]
}
}
return nil
}
문제: 비효율적인 딕셔너리 검색
리팩토링:
func findName(forId id: Int, in dictionary: [Int: String]) -> String? {
return dictionary[id]
}
문제가 있는 코드:
func divideNumbers(_ a: Int, _ b: Int) -> Int? {
if b == 0 {
return nil
}
return a / b
}
let result = divideNumbers(10, 2)! // ❌ 강제 언래핑
문제: 강제 언래핑으로 크래시 가능성
리팩토링:
if let result = divideNumbers(10, 2) {
print("Result: \(result)")
} else {
print("Cannot divide by zero")
}
문제가 있는 코드:
func sortNumbers(numbers: [Int]) -> [Int] {
var sortedNumbers = numbers
for i in 0..<numbers.count {
for j in 0..<numbers.count-i-1 {
if sortedNumbers[j] > sortedNumbers[j+1] {
let temp = sortedNumbers[j]
sortedNumbers[j] = sortedNumbers[j+1]
sortedNumbers[j+1] = temp
}
}
}
return sortedNumbers
}
문제: sorted()가 있을 때 버블 정렬 사용
리팩토링:
func sortNumbers(numbers: [Int]) -> [Int] {
return numbers.sorted()
}
문제가 있는 코드:
func getFirstElement(numbers: [Int]) -> Int? {
if numbers.count > 0 {
return numbers[0]
} else {
return nil
}
}
문제: 불필요한 배열 길이 확인
리팩토링:
func getFirstElement(numbers: [Int]) -> Int? {
return numbers.first
}
문제가 있는 코드:
func parseUserData(data: Data) -> User? {
let json = try? JSONSerialization.jsonObject(with: data, options: []) as? [String: Any]
if let jsonDict = json {
if let name = jsonDict["name"] as? String,
let age = jsonDict["age"] as? Int {
return User(name: name, age: age)
}
}
return nil
}
문제: 수동 파싱은 오류가 발생하기 쉬움
리팩토링:
struct User: Codable {
let name: String
let age: Int
}
func parseUserData(data: Data) -> User? {
return try? JSONDecoder().decode(User.self, from: data)
}
문제가 있는 코드:
func filterEvenNumbers(numbers: [Int]) -> [Int] {
var result = [Int]()
for number in numbers {
if number % 2 == 0 {
result.append(number)
}
}
return result
}
문제: 더 나은 성능을 위해 filter 메서드 사용 가능
리팩토링:
func filterEvenNumbers(numbers: [Int]) -> [Int] {
return numbers.filter { $0 % 2 == 0 }
}
문제가 있는 코드:
func hasSubstring(text: String, substring: String) -> Bool {
if text.contains(substring) {
return true
} else {
return false
}
}
문제: 중복된 조건 확인
리팩토링:
func hasSubstring(text: String, substring: String) -> Bool {
return text.contains(substring)
}
문제가 있는 코드:
func addNumbers(a: Any, b: Any) -> Int? {
if let num1 = a as? Int, let num2 = b as? Int {
return num1 + num2
}
return nil
}
문제: 올바른 함수 매개변수로 타입 캐스팅을 피할 수 있음
리팩토링:
func addNumbers(a: Int, b: Int) -> Int {
return a + b
}
문제가 있는 코드:
func checkAge(age: Int) -> String {
return age < 18 ? "Minor" : age < 21 ? "Young Adult" : "Adult"
}
문제: 너무 많은 중첩 삼항 연산자는 가독성 저하
리팩토링:
func checkAge(age: Int) -> String {
if age < 18 {
return "Minor"
} else if age < 21 {
return "Young Adult"
} else {
return "Adult"
}
}
이 핸드북은 iOS 개발자 면접을 위한 핵심 주제들을 다룹니다:
✅ 보안 및 암호화 - Keychain, AES, RSA
✅ 클로저 - Trailing closures, 값 캡처, Retain cycles
✅ 메모리 관리 - ARC, Strong/Weak/Unowned
✅ SOLID 원칙 - 깨끗한 아키텍처를 위한 5가지 원칙
✅ SwiftUI - @State, @Binding, @ObservedObject
✅ Property Wrappers - Custom wrappers, Codable
✅ 동시성 - async/await, Structured concurrency
✅ Copy-on-Write - 메모리 최적화
✅ GCD & OperationQueue - 동시성 관리
✅ 기본 개념 - Class vs Struct vs Enum
✅ 네트워킹 - URLSession, Codable, Alamofire
✅ 데이터 지속성 - UserDefaults, CoreData, Realm
✅ 테스팅 - XCTest, UI Testing, 비동기 테스트
✅ 코드 최적화 - 일반적인 안티패턴과 리팩토링
학습 팁:
1. 각 주제를 실제로 코딩해보세요
2. 프로젝트에서 실제 사용 사례를 찾아보세요
3. 면접 전에 핵심 개념을 복습하세요
4. 예제 코드를 자신의 프로젝트에 적용해보세요
행운을 빕니다! 🍀
이 핸드북은 2025년 5월 기준 iOS 면접 주제를 포함합니다.