31일차 - 강의 주차
앱 개발 숙련 주차 - ViewController 생명주기
⎮ UIViewController
1. UIViewController
- UIKit에서 앱의 뷰 계층을 관리하는 개체
- 1개의 페이지는 반드시 1개의 UIViewController를 가지게 됨
- UIViewController 내부에 UIView, UIButton, UIScrollView 등 UIKit의 UI클래스들을 배치하며 화면을 구성
⎮ UIViewController 생명주기
1. IOS의 대표적인 생명주기 (2가지)
- 앱의 생명주기(App LifeCycle)
- ViewController의 생명주기(ViewController LifeCycle)
2. ViewController의 생명주기 세분화하기
- init
생성자
- loadView
UIViewController 생성 시 함께 만들어지는 View
따라서 View를 수동으로 생성하고 초기화할 수 있는 책임이 있는 생명주기
(ex: loadView 시점에 사전 세팅을 할 수 있음)
- viewDidLoad
View가 메모리에 load가 되어진 상태
View가 메모리 위에 올라온 순간부터는 User의 눈에 보이게 될 상태라고 생각하면 됨
viewDidLoad는 한번만 호출되는 특성이 있음.
- viewWillAppear
View가 이제 User 눈에 보이기 직전
viewWillAppear는 여러번 호출될 수 있음
- viewIsAppearing
View를 띄우는 중간 단계 (눈에 보이려고 나타나는 중)
- viewDidAppear
View가 정말로 User 눈에 나타난 상태 (눈에 보이는 시점)
- viewWillDisappear
View가 책임을 마치고 사라지려고 하는 단계
- viewDidDisappear
View가 사라진 상태
- deinit
소멸자
⎮ UIViewController 생명주기 확인을 위한 기본 세팅
1. ViewController의 이동을 위한 UINavigationController 만들기
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = scene as? UIWindowScene else { return }
let window = UIWindow(windowScene: windowScene)
// 여기서 UINavigationController 안에 ViewController를 rootVC로 설정
window.rootViewController = UINavigationController(rootViewController: ViewController())
window.makeKeyAndVisible()
self.window = window
}
2. ViewController가 메모리 위에 잘 올라왔는지 확인하기
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad")
}
}
⎮ UIViewController 생명주기 확인하기
1. ViewController 생명주기 확인을 위한 코드 작성하기
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad: 뷰가 메모리에 잘 올라왔어")
}
override func viewWillAppear(_ animated: Bool) {
print("viewWillAppear: 곧 뷰 띄울거야")
}
override func viewIsAppearing(_ animated: Bool) {
print("viewIsAppearing: 뷰 띄우는 중")
}
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear: 뷰 띄웠어")
}
override func viewWillDisappear(_ animated: Bool) {
print("viewWillDisappear: 뷰가 곧 사라질거야")
}
override func viewDidDisappear(_ animated: Bool) {
print("viewDidDisappear: 뷰 사라졌어")
}
}
2. 빌드하기
생명주기의 순서에 맞게 순차적으로 print 되었음을 확인할 수 있다.
* View가 아직 사라지지 않았기 때문에 viewWillDisappear과 viewDidDisappear는 호출되지 않음.
3. Disappear를 위한 버튼 만들기
private let button: UIButton = {
let button = UIButton()
button.setTitle("다음 페이지로 이동", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .red
return button
}()
다만, button을 누르면 다음 페이지로 이동해야하기 때문에 addTarget을 추가해줘야 한다.
이 상태에서 addTarget을 하면 self를 사용하면 경고가 하나 뜬다.
(button이 초기화되지 않은 상태에서 self를 사용하려고 하기 때문에 뜨는 경고)
따라서 이를 해결하기 위해 private let을 private lazy var로 수정하여 초기화 시점을 생성하는 시점에 맞춰준다.
(lazy var 키워드를 사용하여 초기화하는 시점을 미룸 - 버튼이 생성되는 시점으로)
이렇게 하면 button을 생성할 때는 초기화가 되었을 것이기 때문에, self를 사용할 수 있음.
private lazy var button: UIButton = {
let button = UIButton()
button.setTitle("다음 페이지로 이동", for: .normal)
button.setTitleColor(.white, for: .normal)
button.backgroundColor = .red
button.addTarget(self, action: #selector(buttonTapped), for: .touchDown)
return button
}()
이후 제약을 추가하여 autoLayout 설정을 해주면 버튼이 생성됨.
override func viewDidLoad() {
super.viewDidLoad()
configureUI()
print("viewDidLoad: 뷰가 메모리에 잘 올라왔어")
}
private func configureUI() {
[button].forEach { view.addSubview($0) }
view.backgroundColor = .white
button.snp.makeConstraints {
$0.center.equalToSuperview()
$0.width.equalTo(250)
$0.height.equalTo(100)
}
}
4. 다음 페이지를 담당할 ViewController 생성하기
// SecondView.swift
class SecondVC: UIViewController {
override func viewDidLoad() {
print("viewDidLoad: SecondVC 생성")
view.backgroundColor = .orange
}
}
5. 다음 페이지 ViewController 띄우기
@objc func buttonTapped() {
self.navigationController?.pushViewController(SecondVC(), animated: true)
}
6. 버튼을 누르면 disAppear도 확인이 가능함
* viewDidLoad는 view가 메모리에 올라가고 나면 다시 호출되지 않음
앱 개발 숙련 주차 - 메모리 관리 이해
⎮ 메모리와 디스크
* 메모리 / 디스크 모두 데이터의 저장 및 처리를 담당하지만 목적과 특성이 다름.
1. 메모리(일반적으로 RAM)
- RAM은 휘발성 메모리. 즉, 데이터를 영구적으로 저장하지 않고 일시적으로만 저장함
> 앱 실행 중에 메모리에 저장된 데이터는 앱을 종료하면 함께 삭제됨
(앱 자체도 데이터 덩어리이기 때문에, 앱을 실행시키면 데이터도 메모리에 올라가고 내려옴)
- RAM의 용량이 클수록, 동시에 실행시킬 수 있는 앱의 총량이 높아진다고 생각할 수 있음
- 메모리는 디스크보다 속도가 빠름(CPU가 디스크보다 메모리에 더 빨리 접근할 수 있기 때문)
- 단, 디스크에 비해 용량이 작음(보통 8, 16, 24GB 등)
- EEPROM과 같은 비휘발성 메모리도 있음.
* 아이폰의 경우 EEPROM에 장치의 일련번호 및 하드웨어 정보를 저장함
2. 디스크
- 영구적인 데이터를 저장하는 곳(비휘발성)
> 앱 실행 중에 디스크에 저장된 데이터는 앱을 종료해도 디스크에 남음
- 파일, 문서, 프로그램 등 상대적으로 용량이 큰 정보들을 담을 수 있음
- 메모리에 비해 속도가 느림
- UserDefaults, CoreData를 활용해서 디스크에 데이터를 저장할 수 있음
3. 메모리 vs 디스크 : 어떤게 더 효율적일까?
- 전화번호부 앱에서 친구의 이름, 전화번호 정보 데이터(디스크)
- 카운터 앱 개발할 때 사용하는 number 변수(메모리)
- 스테이지가 있는 게임 앱에서 유저가 몇 스테이지까지 클리어했는지에 대한 정보(디스크)
- 유튜브나 인스타그램 같은 SNS 앱 상에서 추천창에 뜬 이미지와 동영상 정보들(메모리)
⎮ 일반적인 메모리 관리 : Java의 GC(Garbage Collector)
Garbage Collector : 메모리 관리를 돕는 시스템 중 하나(특히 Java에서 GC를 사용함)
최강록 요리사가 열심히 요리를 하고 있다.
조리대의 크기가 클수록 한꺼번에 올려놓을 수 있는 식재료의 양이 많아진다.
오늘은 보조 쉐프가 출근을 하지 않아서 요리하는 중간에 식재료를 정리하면서 동시에 요리까지 하는 것이 버겁다.
이때 센스 있는 알바가 조리대를 보더니, 꺼내져 있는 재료 중 안쓰는 식재료를 알아서 정리해준다.
최강록 요리사는 알바의 시급을 올려주었고, 이제 조리대를 효율적으로 사용할 수 있다.
- 최강록 요리사 : CPU
- 조리대의 크기 : RAM의 크기(한꺼번에 처리할 수 있는 데이터의 양이 많아짐)
- 센스 있는 알바 : Garbage Collector(안쓰는 데이터는 알아서 정리해줌)
Garbage Collector는 메모리에서 필요 없는 것들을 정리해주는 역할을 함
사용하지 않는 메모리가 쌓이면서 메모리에 부담되는 상황을 메모리 누수(Memory Leak)라 하는데,
Java에서는 개발자가 직접 명시적으로 메모리를 관리하지 않더라도 기본적으로 메모리 관리를 돕는 GC가 있음.
(런타임에 메모리 영역을 슥 훑어보면서 사용 중인 것들을 Mark하고, 표시되지 않은 것을 정리하는 Mark And Sweep 방식을 사용)
⎮ Swift의 Reference Counting(RC)
Swift의 메모리 관리 시스템의 핵심이 되는 개념은 Reference Counting이다.
메모리를 할당 받은 객체 : 인스턴스
class MyClass { }
let myClass = MyClass()
여기서 myClass는 메모리를 할당받음
인스턴스는 하나 이상의 참조자(소유자 - owner)가 있어야 메모리에 유지가 됨.
소유자가 없다면 즉시 메모리에서 제거가 됨.
이때 인스턴스를 참조하고 있는 소유자의 개수를 RC(reference count)라 함
RC가 0이면 메모리에서 삭제됨
class MyClass {
init() {
print("MyClass 생성")
}
deinit {
print("MyClass 소멸")
}
}
// Reference Count = 1
var myClass: MyClass? = MyClass()
// Reference Count = 2
var myClass2 = myClass
// Reference Count = 2 - 1 = 1
myClass = nil
// Reference Count = 1 - 1 = 0
myClass2 = nil
⎮ ARC와 MRC
1. ARC (Automatic Reference Counting)
- Swift의 메모리 관리 시스템. Java에 GC가 있다면 Swift에는 ARC가 있음
- Reference Count를 자동으로 계산
> 객체가 생성될 때 RC가 1로 설정
> 객체가 다른 변수가 속성에 할당되어 참조될 때마다 RC가 1씩 증가
> 객체에 대한 참조가 해제될 때마다 RC가 감소
> RC가 0이 되면 더 이상 사용되지 않는 것으로 간주하여 메모리에서 해제함
2. MRC (Manual Reference Counting)
- Objective - C에서 사용하는 메모리 관리 시스템
- Reference Count를 개발자가 코드로 직접 계산(Menual)
> 객체가 생성될 떄 개발자가 명시적으로 메모리 할당
> 객체가 다른 변수나 속성에 할당되어 참조될 때마다 개발자가 명시적으로 RC를 증가시킴
> 객체에 대한 참조가 해제될떄마다 개발자가 명시적으로 RC 감소시킴
> RC가 0이 되면 개발자가 명시적으로 메모리에서 해제함
Q : "ARC는 자동으로 RC 카운트를 하니까 개발자는 메모리 관리에 대해 신경쓰지 않아도 되겠네?"
A : 아니. ARC로 잡아내지 못하는 누수 상황이 있어서, 개발자는 늘 신경써야 해
⎮ 약한참조와 강한참조
1. 약참조
- Reference Count를 증가시키지 않으면서 참조하는 것
- weak 키워드를 붙여서 약참조할 수 있음
2. 강참조
- Reference Count를 증가시키면서 참조하는 것
- 일반적인 참조 방식
⎮ Closure의 캡처링 개념
class Uddt {
let mbti = "ENTJ"
init() {
print("클래스 생성")
}
deinit {
print("클래스 소멸")
}
}
// uddt RC = 1
var uddt: Uddt? = Uddt()
// 클로저 캡처 : 여기서 uddt RC = 2가 됨
let printMbti: () -> () = { [uddt] in
guard let uddt else { return }
print("uddt's mbti = \(uddt.mbti)")
}
printMbti()
// 2 - 1 = 1
uddt = nil
위의 코드는 deinit이 호출이 될까 되지 않을까?
답은 "되지 않는다"이다.
왜냐하면 여기서 사용된 클로저 캡처 자체가 메모리 주소를 캡처하는 것이기 때문이다.
이를 해결하기 위해서 weak 키워드를 사용해줄 수 있다.
class Uddt {
let mbti = "ENTJ"
init() {
print("클래스 생성")
}
deinit {
print("클래스 소멸")
}
}
// uddt RC = 1
var uddt: Uddt? = Uddt()
// 클로저 캡처 : weak이라서 RC가 증가하지 않음
let printMbti: () -> () = { [weak uddt] in
guard let uddt else { return }
print("uddt's mbti = \(uddt.mbti)")
}
printMbti()
// RC 1 - 1 = 0
uddt = nil
⎮ 순환참조
- 순환 참조 : A가 B를 참조하고, B가 A를 참조하는 상황 (대표적인 메모리 누수의 원인)
class Person {
var pet: Dog?
init() {
print("Person 클래스 생성")
}
deinit {
print("Person 클래스 소멸")
}
}
class Dog {
var owner: Person?
init() {
print("Dog 클래스 생성")
}
deinit {
print("Dog 클래스 소멸")
}
}
// person RC = 1
var person: Person? = Person()
// dog RC = 1
var dog: Dog? = Dog()
// dog RC = 1 + 1 = 2
person?.pet = dog
// person RC = 1 + 1 = 2
dog?.owner = person
// person RC = 2 - 1 = 1
person = nil
// dog RC = 2 - 1 = 1
dog = nil
이러한 순환참조를 해결하기 위해
class Person {
var pet: Dog?
init() {
print("Person 클래스 생성")
}
deinit {
print("Person 클래스 소멸")
}
}
class Dog {
weak var owner: Person?
init() {
print("Dog 클래스 생성")
}
deinit {
print("Dog 클래스 소멸")
}
}
// person RC = 1
var person: Person? = Person()
// dog RC = 1
var dog: Dog? = Dog()
// dog RC = 1 + 1 = 2
person?.pet = dog
// person RC = 1 + 1 = 2
dog?.owner = person
// person RC = 2 - 1 = 1
person = nil
// dog RC = 2 - 1 = 1
dog = nil
다음과 같이 weak 키워드를 적어줄 수 있음.
'스파르타코딩 클럽 > 본 캠프' 카테고리의 다른 글
45. 스파르타 코딩클럽 [본캠프 - 온보딩 45일차] (0) | 2025.05.07 |
---|---|
33. 스파르타 코딩클럽 [본캠프 - 온보딩 33일차] (0) | 2025.04.15 |
29. 스파르타 코딩클럽 [본캠프 - 온보딩 29일차] (0) | 2025.04.13 |
28. 스파르타 코딩클럽 [본캠프 - 온보딩 28일차] (0) | 2025.04.09 |
27. 스파르타 코딩클럽 [본캠프 - 온보딩 27일차] (0) | 2025.04.08 |