iOS 완벽 마스터
iOS의 핵심 개념과 실전 활용법
학습 항목
이미지 로딩 중...
iOS 실무 개발 필수 팁 완벽 가이드
iOS 개발 현장에서 바로 활용할 수 있는 실무 팁들을 소개합니다. UIKit의 핵심 기능부터 성능 최적화, 디버깅 기법까지 초급 개발자가 알아야 할 필수 지식을 담았습니다.
목차
- Safe_Area_활용
- Auto_Layout_우선순위
- Memory_Leak_방지
- UserDefaults_안전하게_사용
- Delegate_패턴_구현
- Optional_Binding_활용
- Extension으로_코드_정리
1. Safe_Area_활용
시작하며
여러분이 iPhone X 이후 기기에서 앱을 실행했을 때 상단 노치나 하단 홈 인디케이터에 UI가 가려진 경험이 있나요? 버튼이 잘리거나 텍스트가 읽히지 않는 문제가 발생하곤 합니다.
이런 문제는 Safe Area를 제대로 활용하지 않아서 발생합니다. 특히 다양한 기기를 지원해야 하는 앱에서는 치명적인 UX 문제로 이어질 수 있습니다.
바로 이럴 때 필요한 것이 Safe Area Layout Guide입니다. 이를 활용하면 모든 기기에서 안전하게 콘텐츠를 표시할 수 있습니다.
개요
간단히 말해서, Safe Area는 시스템 UI에 가려지지 않는 안전한 영역을 의미합니다. iOS 11부터 도입된 이 개념은 노치, 상태바, 홈 인디케이터 등 시스템 요소를 피해 콘텐츠를 배치할 수 있게 해줍니다.
예를 들어, iPhone 14 Pro의 Dynamic Island나 iPad의 멀티태스킹 영역 같은 경우에 매우 유용합니다. 기존에는 topLayoutGuide, bottomLayoutGuide를 사용했다면, 이제는 safeAreaLayoutGuide 하나로 모든 edge를 처리할 수 있습니다.
Safe Area의 핵심 특징은 자동으로 기기별 inset을 계산해준다는 점입니다. 개발자는 각 기기의 물리적 특성을 외울 필요 없이 Safe Area에만 맞추면 되므로, 유지보수성이 크게 향상됩니다.
코드 예제
// UIView를 Safe Area에 맞춰 배치하는 예제
let contentView = UIView()
contentView.backgroundColor = .systemBlue
contentView.translatesAutoresizingMaskIntoConstraints = false
view.addSubview(contentView)
// Safe Area에 맞춰 제약 조건 설정
NSLayoutConstraint.activate([
contentView.topAnchor.constraint(equalTo: view.safeAreaLayoutGuide.topAnchor),
contentView.leadingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.leadingAnchor),
contentView.trailingAnchor.constraint(equalTo: view.safeAreaLayoutGuide.trailingAnchor),
contentView.bottomAnchor.constraint(equalTo: view.safeAreaLayoutGuide.bottomAnchor)
])
설명
이것이 하는 일: Safe Area Layout Guide를 사용하여 콘텐츠가 시스템 UI에 가려지지 않도록 안전한 영역에 배치합니다. 첫 번째로, translatesAutoresizingMaskIntoConstraints를 false로 설정합니다.
이는 Auto Layout을 사용하기 위해 반드시 필요한 설정입니다. 이렇게 하지 않으면 기존의 autoresizing mask와 충돌이 발생합니다.
그 다음으로, view.safeAreaLayoutGuide를 참조하여 제약 조건을 설정합니다. topAnchor는 노치나 상태바 아래에, bottomAnchor는 홈 인디케이터 위에 자동으로 위치합니다.
leadingAnchor와 trailingAnchor는 화면 회전 시에도 안전한 영역을 보장합니다. 마지막으로, NSLayoutConstraint.activate()를 통해 모든 제약 조건을 한 번에 활성화합니다.
이 방식은 개별적으로 isActive = true를 설정하는 것보다 성능이 좋습니다. 여러분이 이 코드를 사용하면 iPhone SE부터 iPhone 14 Pro Max까지 모든 기기에서 완벽한 레이아웃을 얻을 수 있습니다.
기기별 분기 처리가 필요 없고, 새로운 기기가 출시되어도 코드 수정 없이 대응 가능하며, 가로/세로 모드 전환 시에도 자동으로 조정됩니다.
실전 팁
💡 UITableView나 UICollectionView는 기본적으로 Safe Area를 고려하므로 별도 설정이 필요 없습니다. 단, contentInsetAdjustmentBehavior를 변경한 경우 주의하세요.
💡 풀스크린 이미지나 비디오를 표시할 때는 의도적으로 Safe Area를 무시하고 view의 anchor를 직접 사용해야 합니다.
💡 safeAreaInsets 값을 직접 읽어서 커스텀 계산에 활용할 수 있습니다. 예: view.safeAreaInsets.top
💡 viewSafeAreaInsetsDidChange() 메서드를 오버라이드하면 Safe Area 변경 시점을 감지하여 동적으로 대응할 수 있습니다.
💡 Storyboard에서는 "Safe Area Relative Margins" 옵션을 체크하여 Safe Area 기반 레이아웃을 설정할 수 있습니다.
2. Auto_Layout_우선순위
시작하며
여러분이 레이아웃을 구현하다가 "Unable to simultaneously satisfy constraints" 에러를 본 적 있나요? 여러 제약 조건이 서로 충돌하면서 앱이 의도대로 동작하지 않는 상황이 발생합니다.
이런 문제는 제약 조건의 우선순위를 제대로 설정하지 않아서 발생합니다. 특히 동적으로 변하는 콘텐츠나 다양한 화면 크기를 지원할 때 자주 마주치게 됩니다.
바로 이럴 때 필요한 것이 Constraint Priority입니다. 이를 활용하면 유연하면서도 견고한 레이아웃을 설계할 수 있습니다.
개요
간단히 말해서, Constraint Priority는 제약 조건 간의 중요도를 설정하는 메커니즘입니다. 우선순위는 1부터 1000까지의 값으로 설정되며, 1000은 필수(required), 그 이하는 선택적(optional)으로 처리됩니다.
예를 들어, 텍스트 라벨의 너비와 버튼의 최소 너비가 충돌할 때, 우선순위를 통해 어느 쪽을 우선할지 결정할 수 있습니다. 기존에는 조건문으로 제약 조건을 추가/제거했다면, 이제는 우선순위만 조정하여 더 우아하게 해결할 수 있습니다.
우선순위의 핵심 특징은 시스템이 자동으로 최적의 레이아웃을 계산한다는 점입니다. 개발자는 원하는 결과의 우선순위만 지정하면 되고, Content Hugging과 Compression Resistance를 통해 콘텐츠 크기도 조절할 수 있습니다.
코드 예제
// 라벨과 버튼이 가로로 배치된 상황에서 우선순위 설정
let label = UILabel()
label.text = "이것은 매우 긴 텍스트입니다"
label.translatesAutoresizingMaskIntoConstraints = false
let button = UIButton()
button.setTitle("확인", for: .normal)
button.translatesAutoresizingMaskIntoConstraints = false
// 버튼의 최소 너비 제약 (우선순위 높음)
let buttonWidthConstraint = button.widthAnchor.constraint(greaterThanOrEqualToConstant: 80)
buttonWidthConstraint.priority = .required // 1000
// 라벨이 내용을 모두 표시하려는 제약 (우선순위 낮음)
label.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) // 250
설명
이것이 하는 일: 제약 조건의 우선순위를 설정하여 공간이 부족할 때 어떤 요소를 우선할지 결정합니다. 첫 번째로, 버튼의 최소 너비 제약을 required(1000)로 설정합니다.
이는 어떤 상황에서도 버튼이 최소 80pt의 너비를 유지해야 함을 의미합니다. 사용자가 버튼을 탭할 수 있는 충분한 영역을 보장하기 위해 이렇게 설정합니다.
그 다음으로, 라벨의 Content Compression Resistance를 낮춥니다. 이는 공간이 부족할 때 라벨의 텍스트가 잘릴 수 있음을 의미합니다.
defaultLow(250)는 시스템 기본값보다 낮아서, 다른 요소들에게 우선권을 넘깁니다. 마지막으로, Auto Layout 엔진이 이 우선순위를 바탕으로 최적의 레이아웃을 계산합니다.
화면이 넓으면 모든 제약을 만족시키고, 좁으면 우선순위가 낮은 제약부터 무시하면서 레이아웃을 조정합니다. 여러분이 이 코드를 사용하면 다양한 화면 크기에서 자동으로 적응하는 UI를 만들 수 있습니다.
iPhone SE처럼 작은 화면에서는 라벨이 잘리더라도 버튼은 항상 탭 가능한 크기를 유지하고, iPad처럼 큰 화면에서는 모든 콘텐츠가 완벽하게 표시되며, 제약 충돌 에러가 발생하지 않습니다.
실전 팁
💡 UILayoutPriority에는 .required(1000), .defaultHigh(750), .defaultLow(250) 등 미리 정의된 값들이 있으니 활용하세요.
💡 Content Hugging Priority는 뷰가 콘텐츠보다 커지는 것을 방지하고, Compression Resistance는 콘텐츠가 잘리는 것을 방지합니다.
💡 우선순위를 런타임에 변경할 때는 constraint.priority를 직접 수정하면 됩니다. 단, required 제약은 변경 전에 비활성화해야 합니다.
💡 디버깅 시 po constraint 명령으로 제약의 우선순위를 확인할 수 있습니다.
💡 여러 제약이 동일한 우선순위를 가질 때는 시스템이 임의로 선택하므로, 명확한 우선순위 차이를 두는 것이 좋습니다.
3. Memory_Leak_방지
시작하며
여러분이 클로저를 사용하다가 앱의 메모리가 계속 증가하는 현상을 경험한 적 있나요? 화면을 닫았는데도 메모리가 해제되지 않아 결국 앱이 크래시되는 문제가 발생합니다.
이런 문제는 강한 순환 참조(Strong Reference Cycle)가 발생해서 객체들이 서로를 붙잡고 있기 때문입니다. 특히 네트워크 요청, 애니메이션, 타이머 등 비동기 작업에서 자주 발생하며, 발견하기 어려운 버그의 원인이 됩니다.
바로 이럴 때 필요한 것이 weak self 패턴입니다. 이를 활용하면 메모리 누수를 예방하고 안정적인 앱을 만들 수 있습니다.
개요
간단히 말해서, weak self는 클로저 내부에서 self를 약한 참조로 캡처하여 순환 참조를 방지하는 패턴입니다. Swift의 클로저는 기본적으로 외부 변수를 강하게 캡처합니다.
이는 클로저가 실행되는 동안 객체가 유지되도록 보장하지만, self가 클로저를 소유하고 클로저가 self를 캡처하면 순환 참조가 발생합니다. 예를 들어, ViewController가 네트워크 완료 클로저를 가지고 있고, 그 클로저 내에서 self.updateUI()를 호출하는 경우에 문제가 됩니다.
기존에는 메모리 누수를 발견하기 위해 Instruments를 사용해야 했다면, 이제는 처음부터 weak self를 사용하여 예방할 수 있습니다. weak self의 핵심 특징은 참조 카운트를 증가시키지 않는다는 점과, 객체가 해제되면 자동으로 nil이 된다는 점입니다.
이를 통해 안전하게 메모리를 관리할 수 있습니다.
코드 예제
class ProfileViewController: UIViewController {
var imageURL: URL?
func loadProfileImage() {
guard let url = imageURL else { return }
// weak self를 사용하여 순환 참조 방지
URLSession.shared.dataTask(with: url) { [weak self] data, response, error in
// self가 이미 해제되었을 수 있으므로 guard let으로 안전하게 언래핑
guard let self = self else { return }
guard let data = data, error == nil else { return }
// 메인 스레드에서 UI 업데이트
DispatchQueue.main.async {
self.updateImageView(with: data)
}
}.resume()
}
func updateImageView(with data: Data) {
// 이미지 업데이트 로직
}
}
설명
이것이 하는 일: 네트워크 요청 완료 클로저에서 weak self를 사용하여 ViewController가 해제될 때 메모리 누수가 발생하지 않도록 합니다. 첫 번째로, [weak self]를 캡처 리스트에 추가합니다.
이는 클로저가 self를 약한 참조로 캡처하도록 지시하는 것으로, 이렇게 하면 클로저가 self의 참조 카운트를 증가시키지 않습니다. 사용자가 화면을 벗어나면 ViewController가 정상적으로 해제될 수 있습니다.
그 다음으로, guard let self = self로 언래핑합니다. weak 참조는 옵셔널이므로 반드시 언래핑해야 하며, 이 시점에서 self가 nil이면 클로저를 조기 종료합니다.
이는 이미 해제된 객체의 메서드를 호출하는 것을 방지합니다. 마지막으로, 언래핑된 self를 사용하여 안전하게 인스턴스 메서드를 호출합니다.
네트워크 작업은 백그라운드 스레드에서 실행되므로, UI 업데이트는 DispatchQueue.main.async로 메인 스레드에서 수행합니다. 여러분이 이 패턴을 사용하면 다음과 같은 이점을 얻을 수 있습니다.
사용자가 화면을 빠르게 전환해도 메모리가 정상적으로 해제되고, 오래 걸리는 네트워크 작업 중에도 앱이 안정적으로 동작하며, Instruments에서 메모리 누수를 찾느라 시간을 낭비하지 않아도 됩니다.
실전 팁
💡 unowned self는 weak과 달리 옵셔널이 아니지만, 객체가 해제된 후 접근하면 크래시가 발생하므로 확실한 경우에만 사용하세요.
💡 GCD의 DispatchQueue.main.async에서도 weak self를 사용하는 것이 안전합니다. 특히 딜레이가 있는 경우 필수입니다.
💡 Combine 프레임워크의 sink 클로저에서도 [weak self]를 잊지 마세요. 많은 개발자가 놓치는 부분입니다.
💡 Xcode의 Debug Memory Graph 기능으로 순환 참조를 시각적으로 확인할 수 있습니다. 주기적으로 점검하세요.
💡 guard let self = self 대신 Swift 5.3 이상에서는 단순히 self를 사용할 수 있지만, 명시적으로 guard를 사용하는 것이 의도를 명확히 합니다.
4. UserDefaults_안전하게_사용
시작하며
여러분이 앱의 설정 값이나 사용자 데이터를 저장하다가 nil이 반환되거나 잘못된 타입으로 인해 크래시가 발생한 경험이 있나요? UserDefaults는 간편하지만, 잘못 사용하면 예상치 못한 버그의 원인이 됩니다.
이런 문제는 타입 안정성이 없고 키를 문자열로 관리하면서 발생합니다. 오타가 나거나 다른 타입으로 캐스팅하려다 실패하는 경우가 빈번합니다.
바로 이럴 때 필요한 것이 타입 안전한 UserDefaults 래퍼입니다. 이를 활용하면 컴파일 타임에 오류를 잡고 안전하게 데이터를 관리할 수 있습니다.
개요
간단히 말해서, 타입 안전한 UserDefaults 래퍼는 문자열 키와 타입 캐스팅의 위험을 제거하는 패턴입니다. UserDefaults는 키-값 저장소로 간단한 데이터를 저장하기에 적합하지만, Any 타입을 반환하므로 매번 타입 캐스팅이 필요합니다.
예를 들어, 사용자의 알림 설정, 테마 선택, 마지막 로그인 날짜 같은 앱 설정을 저장할 때 유용합니다. 기존에는 "isNotificationEnabled" 같은 문자열 키를 직접 사용했다면, 이제는 타입이 정의된 열거형이나 구조체를 사용하여 안전하게 접근할 수 있습니다.
이 패턴의 핵심 특징은 컴파일 타임 안정성과 기본값 지원입니다. 키 오타는 컴파일 에러로 즉시 발견되고, 값이 없을 때의 기본값을 명확히 정의할 수 있어 nil 처리 로직이 간결해집니다.
코드 예제
// UserDefaults 키를 안전하게 관리하는 래퍼
enum UserDefaultsKey: String {
case isNotificationEnabled
case userName
case lastLoginDate
}
extension UserDefaults {
// Bool 값 저장/읽기
func set(_ value: Bool, forKey key: UserDefaultsKey) {
set(value, forKey: key.rawValue)
}
func bool(forKey key: UserDefaultsKey, defaultValue: Bool = false) -> Bool {
return object(forKey: key.rawValue) != nil ? bool(forKey: key.rawValue) : defaultValue
}
// String 값 저장/읽기
func set(_ value: String?, forKey key: UserDefaultsKey) {
set(value, forKey: key.rawValue)
}
func string(forKey key: UserDefaultsKey) -> String? {
return string(forKey: key.rawValue)
}
}
// 사용 예시
UserDefaults.standard.set(true, forKey: .isNotificationEnabled)
let isEnabled = UserDefaults.standard.bool(forKey: .isNotificationEnabled, defaultValue: false)
설명
이것이 하는 일: UserDefaults의 키를 열거형으로 관리하고, 타입별 안전한 접근 메서드를 제공하여 런타임 에러를 방지합니다. 첫 번째로, UserDefaultsKey 열거형을 정의합니다.
이는 모든 UserDefaults 키를 한 곳에서 관리하게 해주며, rawValue를 String으로 지정하여 실제 저장 키로 사용합니다. 이렇게 하면 키 이름을 변경할 때 한 곳만 수정하면 되고, 오타가 발생할 가능성이 완전히 사라집니다.
그 다음으로, UserDefaults Extension을 추가하여 타입별 메서드를 구현합니다. bool(forKey:defaultValue:) 메서드는 값이 없을 때 기본값을 반환하도록 개선되었습니다.
object(forKey:)로 값의 존재 여부를 먼저 확인하는 것이 핵심입니다. 마지막으로, 이 래퍼를 사용하면 코드가 매우 간결해집니다.
.isNotificationEnabled처럼 점 표기법으로 자동완성이 지원되고, 타입이 명확하여 캐스팅이 불필요하며, 기본값 처리가 메서드 레벨에서 이루어집니다. 여러분이 이 패턴을 사용하면 다음과 같은 실무 이점을 얻을 수 있습니다.
리팩토링 시 키 이름 변경이 안전하고(Xcode의 Rename 기능 활용 가능), 새로운 설정을 추가할 때 열거형에만 케이스를 추가하면 되며, 팀원들이 어떤 설정이 있는지 열거형만 보면 바로 알 수 있고, 타입 불일치로 인한 크래시가 원천적으로 차단됩니다.
실전 팁
💡 복잡한 객체를 저장할 때는 Codable을 사용하세요. JSONEncoder/JSONDecoder로 Data로 변환하여 저장하면 됩니다.
💡 민감한 정보(비밀번호, 토큰)는 UserDefaults가 아닌 Keychain을 사용해야 합니다. UserDefaults는 암호화되지 않습니다.
💡 대용량 데이터는 UserDefaults에 저장하지 마세요. 파일 시스템이나 Core Data가 더 적합합니다. UserDefaults는 작고 자주 접근하는 설정에 최적화되어 있습니다.
💡 앱 그룹을 사용하면 앱과 Extension 간에 UserDefaults를 공유할 수 있습니다. UserDefaults(suiteName: "group.com.yourcompany.app")로 생성하세요.
💡 값이 변경될 때 알림을 받으려면 NotificationCenter에서 .didChangeNotification을 관찰하세요. 다만, 모든 변경에 대해 알림이 오므로 키 필터링이 필요합니다.
5. Delegate_패턴_구현
시작하며
여러분이 화면 간 데이터를 전달하거나 사용자 액션에 반응해야 할 때 어떻게 구현하시나요? NotificationCenter를 남발하거나 싱글톤에 의존하면 코드가 복잡해지고 디버깅이 어려워집니다.
이런 문제는 컴포넌트 간 결합도가 높아서 발생합니다. 특히 재사용 가능한 커스텀 뷰나 컴포넌트를 만들 때, 명확한 통신 방법이 없으면 코드가 스파게티처럼 얽히게 됩니다.
바로 이럴 때 필요한 것이 Delegate 패턴입니다. 이를 활용하면 느슨한 결합으로 깔끔하고 재사용 가능한 코드를 작성할 수 있습니다.
개요
간단히 말해서, Delegate 패턴은 한 객체가 다른 객체를 대신하여 작업을 수행하거나 이벤트를 전달하는 디자인 패턴입니다. iOS에서 가장 널리 사용되는 패턴으로, UITableView, UITextField 등 대부분의 UIKit 컴포넌트가 이 패턴을 사용합니다.
Protocol로 인터페이스를 정의하고, weak 참조로 delegate를 보유하며, 이벤트 발생 시 delegate의 메서드를 호출합니다. 예를 들어, 커스텀 로그인 폼에서 "로그인 성공" 이벤트를 상위 ViewController에 전달할 때 매우 유용합니다.
기존에는 클로저나 NotificationCenter를 사용했다면, Delegate는 더 명확한 인터페이스와 컴파일 타임 체크를 제공합니다. Delegate 패턴의 핵심 특징은 1:1 통신, 타입 안정성, 그리고 명확한 책임 분리입니다.
누가 누구에게 무엇을 전달하는지가 코드에서 명확히 드러나므로 유지보수가 쉬워집니다.
코드 예제
// 1. Protocol 정의
protocol LoginViewDelegate: AnyObject {
func loginViewDidTapLoginButton(_ loginView: LoginView, email: String, password: String)
func loginViewDidTapForgotPassword(_ loginView: LoginView)
}
// 2. Delegate를 사용하는 View
class LoginView: UIView {
// weak로 선언하여 순환 참조 방지
weak var delegate: LoginViewDelegate?
private let emailTextField = UITextField()
private let passwordTextField = UITextField()
private let loginButton = UIButton()
override init(frame: CGRect) {
super.init(frame: frame)
setupUI()
}
required init?(coder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
private func setupUI() {
loginButton.addTarget(self, action: #selector(loginButtonTapped), for: .touchUpInside)
}
@objc private func loginButtonTapped() {
// Delegate 메서드 호출
guard let email = emailTextField.text, let password = passwordTextField.text else { return }
delegate?.loginViewDidTapLoginButton(self, email: email, password: password)
}
}
// 3. Delegate를 채택하는 ViewController
class LoginViewController: UIViewController, LoginViewDelegate {
private let loginView = LoginView()
override func viewDidLoad() {
super.viewDidLoad()
loginView.delegate = self // delegate 설정
}
// Delegate 메서드 구현
func loginViewDidTapLoginButton(_ loginView: LoginView, email: String, password: String) {
// 로그인 로직 수행
print("로그인 시도: \(email)")
}
func loginViewDidTapForgotPassword(_ loginView: LoginView) {
// 비밀번호 찾기 화면으로 이동
print("비밀번호 찾기")
}
}
설명
이것이 하는 일: 커스텀 LoginView가 사용자 액션을 감지하고, Delegate를 통해 상위 ViewController에 이벤트를 전달합니다. 첫 번째로, LoginViewDelegate Protocol을 정의합니다.
AnyObject를 상속하여 class 전용 프로토콜로 만드는 것이 중요합니다. 이렇게 해야 weak 참조를 사용할 수 있습니다.
메서드 이름에는 관례적으로 delegate를 호출하는 객체를 첫 번째 파라미터로 전달하여, 여러 객체가 같은 delegate를 공유할 때 구분할 수 있게 합니다. 그 다음으로, LoginView 클래스에서 delegate 프로퍼티를 weak var로 선언합니다.
weak는 순환 참조를 방지하기 위해 필수이며, 옵셔널 체이닝으로 안전하게 호출합니다. loginButtonTapped() 메서드에서 필요한 데이터를 수집한 후 delegate 메서드를 호출합니다.
마지막으로, LoginViewController가 프로토콜을 채택하고 메서드를 구현합니다. viewDidLoad()에서 loginView.delegate = self로 설정하면 연결이 완료됩니다.
이제 LoginView는 ViewController에 대해 전혀 알지 못하지만, 이벤트는 완벽하게 전달됩니다. 여러분이 이 패턴을 사용하면 다음과 같은 실무 이점을 얻습니다.
LoginView를 다른 프로젝트에서 재사용할 수 있고(ViewController에 의존하지 않음), 테스트 시 Mock Delegate를 쉽게 주입할 수 있으며, 이벤트 흐름이 명확하여 디버깅이 쉽고, Xcode의 자동완성으로 어떤 메서드를 구현해야 하는지 바로 알 수 있습니다.
실전 팁
💡 Optional 메서드를 만들려면 @objc optional을 사용하세요. 단, protocol도 @objc로 선언해야 합니다. Swift 네이티브 방식으로는 기본 구현을 제공하는 extension을 사용하세요.
💡 여러 delegate를 가져야 한다면 MulticastDelegate 패턴을 고려하세요. 하지만 대부분의 경우 NotificationCenter나 Combine이 더 적합할 수 있습니다.
💡 Delegate 메서드 이름은 과거형(didTap)이나 미래형(willAppear)으로 짓는 것이 관례입니다. 이는 이벤트의 시점을 명확히 합니다.
💡 데이터를 delegate로 전달할 때는 원시 타입보다 의미 있는 모델 객체를 사용하는 것이 유지보수에 좋습니다.
💡 delegate와 closure의 선택 기준: 1:1 지속적 통신은 delegate, 일회성 콜백은 closure가 적합합니다.
6. Optional_Binding_활용
시작하며
여러분이 API 응답이나 사용자 입력을 처리할 때 nil 때문에 앱이 크래시된 경험이 있나요? Optional을 강제 언래핑(!)하거나 제대로 처리하지 않으면 런타임 에러가 발생합니다.
이런 문제는 Swift의 Optional 타입을 제대로 이해하지 못해서 발생합니다. nil이 올 수 있는 값을 안전하게 다루지 않으면, 예상치 못한 시점에 앱이 종료되어 사용자 경험을 크게 해칩니다.
바로 이럴 때 필요한 것이 Optional Binding입니다. 이를 활용하면 nil을 안전하게 처리하고 견고한 코드를 작성할 수 있습니다.
개요
간단히 말해서, Optional Binding은 Optional 값이 nil이 아닐 때만 안전하게 값을 추출하는 방법입니다. Swift는 nil의 위험을 컴파일 타임에 잡기 위해 Optional 타입을 도입했습니다.
if let, guard let, nil coalescing(??) 등 다양한 방법으로 Optional을 안전하게 다룰 수 있습니다. 예를 들어, 네트워크 응답에서 선택적 필드를 파싱하거나, UITextField의 텍스트를 읽을 때 매우 유용합니다.
기존에는 강제 언래핑(!)으로 편하게 사용했다가 크래시가 발생했다면, 이제는 Optional Binding으로 안전하게 처리할 수 있습니다. Optional Binding의 핵심 특징은 컴파일러의 도움을 받아 nil 처리를 강제한다는 점입니다.
guard let은 조기 종료 패턴에 적합하고, if let은 조건부 실행에 적합하며, nil coalescing은 기본값 제공에 적합합니다.
코드 예제
struct User {
let id: Int
let name: String
let email: String?
let age: Int?
}
func processUser(_ user: User?) {
// 1. guard let으로 조기 종료 - 메서드 전체에서 user 사용 가능
guard let user = user else {
print("User is nil")
return
}
print("Processing user: \(user.name)")
// 2. Optional Chaining - 중간 값이 nil이면 전체가 nil
let emailUppercased = user.email?.uppercased()
print("Email: \(emailUppercased ?? "No email")")
// 3. if let으로 조건부 처리
if let age = user.age {
print("User age: \(age)")
} else {
print("Age not provided")
}
// 4. 여러 Optional을 한 번에 바인딩
if let email = user.email, let age = user.age, age >= 18 {
print("Adult user with email: \(email)")
}
// 5. Nil Coalescing으로 기본값 제공
let displayAge = user.age ?? 0
print("Display age: \(displayAge)")
}
// 사용 예시
let user = User(id: 1, name: "홍길동", email: "hong@example.com", age: 25)
processUser(user)
설명
이것이 하는 일: 다양한 Optional Binding 기법을 사용하여 nil 값을 안전하게 처리하고 크래시를 방지합니다. 첫 번째로, guard let으로 user 파라미터를 검사합니다.
guard는 "이 조건이 만족되지 않으면 여기서 끝낸다"는 의미로, else 블록에서 반드시 함수를 종료해야 합니다. guard let의 장점은 언래핑된 변수가 이후 스코프 전체에서 사용 가능하다는 점입니다.
그 다음으로, Optional Chaining(?.)을 사용합니다. user.email이 nil이면 uppercased()가 호출되지 않고 전체 표현식이 nil이 됩니다.
이는 여러 단계의 Optional을 연결할 때 매우 편리합니다. ??
연산자로 nil일 때 기본값을 제공하여 항상 String 타입을 보장합니다. 마지막으로, 여러 Optional을 동시에 바인딩하고 추가 조건도 함께 검사합니다.
쉼표로 구분된 모든 조건이 true일 때만 if 블록이 실행됩니다. 이는 코드를 중첩된 if문으로 작성하는 것보다 훨씬 읽기 쉽습니다.
여러분이 이러한 기법들을 적절히 조합하면 다음과 같은 실무 이점을 얻습니다. 강제 언래핑으로 인한 크래시가 완전히 사라지고, 코드 리뷰에서 "!"를 사용한 이유를 설명할 필요가 없으며, nil 처리 로직이 명확하여 버그를 조기에 발견할 수 있고, API 응답의 선택적 필드를 안전하게 다룰 수 있습니다.
실전 팁
💡 guard let 대신 guard를 단독으로 사용하여 Bool 조건도 검사할 수 있습니다. 예: guard isValid else { return }
💡 try?는 에러를 Optional로 변환합니다. do-catch가 과할 때 유용하지만, 에러 정보는 잃게 됩니다.
💡 map과 flatMap으로 Optional을 함수형으로 변환할 수 있습니다. 예: user.email.map { $0.uppercased() }
💡 Swift 5.7부터 if let user = user를 if let user로 단축할 수 있습니다. 변수명이 같을 때 편리합니다.
💡 assert와 precondition으로 "절대 nil이면 안 되는" 경우를 명시적으로 체크하세요. 디버그 빌드에서 조기 발견할 수 있습니다.
7. Extension으로_코드_정리
시작하며
여러분이 프로젝트가 커지면서 ViewController가 수백 줄로 늘어나고 어디에 무엇이 있는지 찾기 어려운 경험을 해보셨나요? 모든 코드가 한 파일에 뭉쳐있으면 가독성이 떨어지고 협업이 어려워집니다.
이런 문제는 코드 구조화와 관심사 분리가 제대로 되지 않아서 발생합니다. 특히 UITableViewDelegate, UITextFieldDelegate 등 여러 프로토콜을 채택하면 코드가 섞여서 유지보수가 힘들어집니다.
바로 이럴 때 필요한 것이 Extension입니다. 이를 활용하면 논리적으로 관련된 코드를 그룹화하여 깔끔하게 정리할 수 있습니다.
개요
간단히 말해서, Extension은 기존 타입에 새로운 기능을 추가하거나 코드를 논리적으로 그룹화하는 Swift 기능입니다. 클래스, 구조체, 열거형 등 모든 타입에 메서드, 계산 프로퍼티, 이니셜라이저를 추가할 수 있습니다.
특히 프로토콜 채택을 Extension에서 하면 관련 메서드들을 함께 묶을 수 있어 가독성이 크게 향상됩니다. 예를 들어, UIViewController의 setup 메서드들, delegate 구현, 액션 메서드들을 각각 다른 extension으로 분리할 수 있습니다.
기존에는 // MARK: 주석으로 구분했다면, 이제는 Extension으로 물리적으로 분리하여 더 명확한 구조를 만들 수 있습니다. Extension의 핵심 특징은 원본 코드 수정 없이 기능을 확장한다는 점입니다.
시스템 타입(String, Int 등)에도 커스텀 메서드를 추가할 수 있고, 프로토콜 구현을 논리적으로 그룹화하며, 별도 파일로 분리하여 Git 충돌도 줄일 수 있습니다.
코드 예제
// MARK: - Main ViewController
class ProfileViewController: UIViewController {
private let tableView = UITableView()
private let profileImageView = UIImageView()
private var userData: [String] = []
override func viewDidLoad() {
super.viewDidLoad()
setupUI()
setupTableView()
}
}
// MARK: - UI Setup
private extension ProfileViewController {
func setupUI() {
view.backgroundColor = .systemBackground
view.addSubview(profileImageView)
view.addSubview(tableView)
setupConstraints()
}
func setupConstraints() {
// Auto Layout 코드
profileImageView.translatesAutoresizingMaskIntoConstraints = false
// ... 제약 조건 설정
}
}
// MARK: - UITableViewDataSource
extension ProfileViewController: UITableViewDataSource {
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return userData.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let cell = tableView.dequeueReusableCell(withIdentifier: "Cell", for: indexPath)
cell.textLabel?.text = userData[indexPath.row]
return cell
}
}
// MARK: - UITableViewDelegate
extension ProfileViewController: UITableViewDelegate {
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
tableView.deselectRow(at: indexPath, animated: true)
print("Selected: \(userData[indexPath.row])")
}
}
// MARK: - Helper Methods
extension ProfileViewController {
func loadUserData() {
// 네트워크 요청이나 데이터 로딩 로직
userData = ["Profile", "Settings", "Logout"]
tableView.reloadData()
}
}
설명
이것이 하는 일: ProfileViewController를 여러 Extension으로 나누어 각각의 책임을 명확히 분리하고 코드 구조를 개선합니다. 첫 번째로, 메인 클래스에는 프로퍼티와 viewDidLoad()만 남깁니다.
이는 파일을 열었을 때 ViewController의 전체 구조를 한눈에 파악할 수 있게 합니다. 어떤 UI 요소가 있고 어떤 데이터를 관리하는지 즉시 알 수 있습니다.
그 다음으로, private extension으로 UI 설정 관련 메서드를 그룹화합니다. private을 사용하면 이 메서드들이 파일 내부에서만 사용됨을 명확히 하여 API 표면적을 줄입니다.
setupUI(), setupConstraints() 같은 초기화 메서드들이 한곳에 모여있어 찾기 쉽습니다. 마지막으로, 각 프로토콜을 별도 Extension으로 구현합니다.
UITableViewDataSource와 UITableViewDelegate가 분리되어 있어, 테이블뷰 데이터 로직과 인터랙션 로직이 섞이지 않습니다. Xcode의 Minimap이나 Jump Bar에서 // MARK: 주석이 보여 원하는 섹션으로 빠르게 이동할 수 있습니다.
여러분이 이 구조를 사용하면 다음과 같은 실무 이점을 얻습니다. 새로운 팀원이 코드를 이해하기 쉽고, 특정 기능을 수정할 때 관련 코드만 찾아서 작업할 수 있으며, Git에서 다른 섹션을 동시에 수정해도 충돌이 적고, 테스트할 때 특정 프로토콜 구현만 Mock으로 교체하기 쉬우며, 프로토콜 요구사항을 빠뜨리면 해당 Extension에서 컴파일 에러가 발생하여 즉시 알 수 있습니다.
실전 팁
💡 Extension을 별도 파일로 분리할 수도 있습니다. 예: ProfileViewController+TableView.swift. 대규모 프로젝트에서 유용합니다.
💡 시스템 타입에 Extension을 추가할 때는 prefix를 붙이는 것이 좋습니다. 예: String의 isEmpty가 아니라 isNotEmpty 같은 명확한 이름 사용.
💡 Extension에는 저장 프로퍼티를 추가할 수 없지만, 계산 프로퍼티는 추가할 수 있습니다. 상태를 저장하려면 Objective-C Runtime의 associated objects를 사용해야 합니다.
💡 프로토콜에 기본 구현을 제공하는 Extension을 만들면, 채택한 타입에서 선택적으로 오버라이드할 수 있습니다. Swift의 강력한 기능입니다.
💡 여러 타입이 공통으로 사용하는 기능은 Protocol + Extension 조합으로 만들면 코드 재사용성이 극대화됩니다.