Closed feldblume5263 closed 2 years ago
프레임워크는 무엇이고 라이브러리와 어떻게 다를까요?
우리는 이미 UIKit, SwiftUI이라는 프레임워크를 사용해보았습니다.
한번 비유를 들어볼게요.
우리가 살 집을 짓는다고 가정을 해봅시다. 저는 마당이 딸린 2층 집을 짓고 싶네요.
건축을 잘 알지 못하지만 건축과 프로그래밍은 닮은 점이 많아서 자주 예시로 들곤 하는 것 같습니다.
프레임워크는 "건설사" 로 비유하면 적합할 것 같습니다.
우리가 집을 땅을 직접 고르고 벽돌을 하나씩 만들어서 짓게 된다면 너무 힘들기 때문에 우리는 우리가 짓고 싶은 집을 OO건설이나 XX건설과 같은 건설사를 통해서 짓게 될 것입니다.
어떤 건설사에게 맡기느냐에 따라 집 완공까지의 경험은 크게 달라지겠지만,
덕분에 우리는 집을 짓는 방법을 다 알지 못해도 우리의 집을 지을 수 있습니다. 건설사는 우리에게 어떤 집을 어떤 색깔로 만들것인지, 방은 몇개인지, 방의 크기는 얼마를 원하는 지 정도만 물어볼테니까요.
건설사에게 다 맡기는 것보다는 우리가 할일이 더 있겠지만,
프레임워크는 이렇게 우리가 다 알지 못해도 쉽게 앱을 만들 수 있도록 어느정도 규칙을 정해놓고 틀을 만들어놓은 장치입니다.
즉, 프로그래밍에서 특정 운영 체제를 위한 응용 프로그램 표준 구조를 구현하는 클래스와 라이브러리 모임이라는 사전적 정의처럼 우리는 iOS라는 운영체제에서 iOS Application이라는 프로그램을 구현하기 위해 클래스와 라이브러리의 모임인 UIKit 혹은 SwiftUI라는 프레임워크를 사용하여 앱을 만드는 것이죠.
UIkit 과 SwiftUI는 상당히 고도화된 프레임워크이기 때문에 규칙이 상당히 많고 자유도가 상당히 제한되어 있는 편입니다.
예를들면 UIkit 프레임워크를 사용한다고 했을 때 우리가 알고 있어야 하고 지켜야 하는 ViewController의 라이프사이클은 우리가 임의대로 바꿀 수 없습니다.
또 SwiftUI 프레임워크를 사용했을 때 View 프로토콜을 따르는 Struct는 필수로 body 를 가져야하는 것처럼요.
그렇다면 라이브러리는 무엇이고 프레임워크와 어떻게 다를까요?
아까 예시로 들었던 우리의 집 만들기를 계속 해봅시다.
우리가 집의 화장실을 만든다고 했을 때 어떤 창문을 고를 것인지 어떤 타일을 고를 것인지 고민을 해야겠죠?
하지만 우리가 처음부터 타일을 굽거나 창문을 만들지는 않고 미리 만들어져 있는 기성품들을 골라서 사용하게 될 것입니다.
라이브러리는 이처럼 우리의 앱 만들 때의 편의를 위해서 미리 구현이 되어있는 Class와 Function의 집합으로서 우리는 이 라이브러리의 필요한 부분을 가져다가 자유롭게 이용하여 원하는 부분을 만들어 낼 수 있습니다.
iOS 개발에서는 Alamofire과 같은 Network를 쉽게 다루기 위한 네트워킹 라이브러리나, CoreData나 Realm 처럼 데이터베이스 라이브러리 등 수많은 라이브러리를 사용하고 있습니다.
결론적으로 라이브러리는 우리가 필요한 부분만 호출하여 활용 가능한, 즉 제어권이 개발자에게 있다는 점에서 프레임워크와 차이점을 지니고 있는거죠.
XCode를 통해 StoryBoard, 즉 UIKit 프레임워크를 사용하는 앱 프로젝트를 만들면 AppDelegate.swift와 SceneDelegate.swift, ViewController.swift, Main.storyBoard 가 함께 생성됩니다.
아마 UIKit에 익숙하지 않으신 분이라면 ViewController와 Main.storyBoard를 제외하고는 어떤 역할을 하고 있는지 잘 알지 못하는 경우가 대부분일 것이라고 생각해요.
저도 첫번째 앱을 만들어내는 동안 저 두 파일이 무엇을 의미하는지 전혀 알지 못한채로 완성했거든요.
오늘은 각각의 파일들이 어떤 역할을 하는지만 간략하게 소개하고,
실제로 SceneDelegate와 AppDelegate가 어떻게 동작하고 어떻게 쓰이는지에 대해서는 다음에 실제로 멀티 윈도우를 지원하는 앱을 만들어보면서 더 자세히 이해해보면 좋을 것 같아요!
"iOS 13 으로 오면서 iPad에서 멀티윈도우(멀티태스킹)가가능해졌다. 그래서 스크린에 하나의 Window만 띄울 수 있던 과거와 다르게 여러 앱을 한번에 실행할 수 있게 되었고, UIKit에서도 이를 Window -> Scene으로 새롭게 다루고있다"와 같은 멀티윈도우에 대한 이야기보다는 SceneDelegate와 AppDelegate를 통해서 가장 핵심 역할인 앱의 라이프사이클등 앱의 전반 사항을 어떻게 관리하는지를 중점을 살펴볼게요.
이 두 delegate을 이해하기 위해서는 먼저 SceneDelegate가 하는 역할을 이해해야 합니다.
앱의 라이프 사이클은 UISceneDelegate에서 다루고 있습니다.
다음 그림과 같은 앱 전체의 상태변화를 SceneDelegate에서 책임지는 것입니다.
이는 print를 찍어보면 더 직관적으로 알 수 있는데,
먼저 처음 앱을 실행하면,
먼저 AppDelegate에서 실행이 되고 Foreground로 들어오게 됩니다. (아직은 inactive)
그리고 BecomeActive 상태가 되는거죠.
그리고 홈 화면으로 나갔다가 들어오면,
먼저 inactive 상태로 들어가고
백그라운드 상태로 빠지게 됩니다.
다시 들어오면, 포그라운드 상태로 전환되게 되고,
다시 Active 상태가 되고요.
마지막으로 앱을 쓸어올려서 강제 종료하게 되면,
inactive상태로 빠지고 Suspend되게 됩니다.
만약에 백그라운드 상태에서 오래 있다가 메모리 부족이나 시간 초과 등으로 종료되는 경우에는 백그라운드에서 Suspended상태로 들어가게 됩니다.
각각의 상태에서 무슨 일이 일어나는지는 더 자세히 찾아보기를 권장합니다ㅎㅎ
학교에서 배우는 운영체제처럼 iOS에 대해서 잘 알고 있는것도 중요하니까요!
자, 그러면 AppDelegate에서 하는 역할은 무엇일까요?
App Delegate는 앱 전체에서 일어나는 이벤트에 대응하는 역할을 합니다.
앱은 Scene을 여러개 가질 수 있지만, 앱 전체에서 일어나는 이벤트를 처리해줄 필요도 있으니까요.
아까 앱을 실행시키니 맨 처음에 didFininshLaunchingWithOptions 이라는 함수가 실행되었죠?
이는 앱을 실행할 준비가 완료되었음을 알리는 역할을 합니다.
즉, 나 시작한다~~!!! 라고 알리는 것이죠.
사실 앱 실행 이전에 LaunchProcess라는 과정을 거치지만, 이 설명은 생략할게요.
아까 두번째에서 Configuration for connecting이라는 작업을 하였죠?
이는 새로운 Scene을 생성할 때, UIKit과의 연결을 위해 Configuration Data를 조회하는 과정이고, 이는 App Delegate에서 이루어집니다.
반대로 Scene이 종료되는 과정에서는, app switch는 유저가 한개 이상의 Scene을 종료하려고 한다는 사실을 app delegate에게 알리고 종료되게 됩니다.
즉 시스템 등에서 배터리가 부족하다, 다운로드 완료 노티피케이션 등을 처리하는 역할을 App Delegate에서 맡게 되는거죠.
이를 위해 다양한 함수들이 만들어져 있습니다.
이는 개발자들이 가장 자주 delegate 파일을 편집하는 이유가 될 수 도 있을 것 같아요.
예를 들면, Notification이나 카메라 사용 권한을 유저로부터 받는다던지,
Firebase를 사용한다면 해당 Configuration을 등록한다던지 등등
앱이 시작하기 이전에 필수적으로 실행되어야 하는 서비스들을 등록하는 단계가 아까 맨 처음의 didFininshLaunchingWithOptions 과 같은 함수에서 이루어집니다.
AppDelegate와 SceneDelegate를 거치고 앱이 실행된다고 하는데, iOS에서는 어떻게 AppDelegate를 실행해야 한다는 것을 알 수 있을까요?
C언어나 Python을 써보신 적이 있는 분들(혹은 다른 대부분의 언어도 마찬가지)은
int main(void) { }
와 같은 시작점이 있다는 것을 기억하실거에요.
도대체 뭐부터 실행되는지를 어떻게 알 수 있을까요?
결론부터 말하자면, UIKit은 AppDelegate가 Entry Point 타입으로 지정되어 있습니다.
AppDelegate 위에 @main이 붙어있는 것을 알 수 있을텐데,
@main이 붙으면 엔트리포인트 타입이 됩니다.
엔트리포인트 타입은 static func main()
을 필수적으로 가져야 하는데 AppDelegate는 그게 안보이죠?
UIApplicationDelegate 프로토콜을 채탁하고 있는 AppDelegate는 자체적으로 내부에
static func main()
함수가 존재하고 있습니다. 그래서 entryPoint 타입이 될 수 있는 것이죠.
@main을 AppDelegate에서 삭제하고 다른 클래스에 붙여서 테스트 해볼 수도 있습니다.
@main
class NewEntryPoint {
static func main() {
print("하나, 둘, 셋, 넷")
}
}
UIKit 프레임워크의 AppDelegate에서 앱의 initialize가 진행되면 가장 먼저 StoryBoard에서 진입점을 찾게 됩니다.
가장 먼저 Initial View Controller의 View Controller가 실행되는 것이죠.
앱들은 ViewController들로 이루어져 있는데, 이 각각의 ViewController들을 생명 주기를 가지고 있어요.
각각의 View Controller들은
loadView
viewDidLoad
viewWillAppear
(viewWillLayoutSubviews)
(viewDidLayoutSubviews)
viewDidAppear
viewWillDisappear
viewDidDisappear
과 같은 생명 주기를 가지는데, 이는 print를 찍어보면 쉽게 알 수 있습니다.
override func loadView() {
super.loadView()
print("load view")
}
override func viewDidLoad() {
super.viewDidLoad()
print("viewDidLoad")
}
override func viewWillAppear(_ animated: Bool) {
print("viewWillAppear")
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews")
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
}
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
}
override func viewWillDisappear(_ animated: Bool) {
print("viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
print("viewDidDisappear")
}
먼저 거의 처음 보았을 것 같은 loadView와 자주 마주쳤을 viewDidLoad에 대해서 설명할게요.
loadView와 viewDidLoad는 ViewController가 생성될 때 한번만 실행된다는 점에서 공통점을 가지는데요.
그렇다면 차이점은 무엇일까요?
바로 뷰가 생성되어 메모리에 올라가기 전이냐 후이냐에 따라서 차이를 가집니다.
loadView의 경우는 우리가 스토리보드에 버튼 같은 객체를 생성해놓는 작업과 비슷합니다.
즉, 객체들이 생성되어 메모리에 올라가기 전에 선언해놓는 작업과 같은 단계라고 보시면 되요.
그래서 우리가 만약에 스토리보드를 사용한다면 loadView 단계에서 무언가를 생성할 필요는 없습니다.
하지만 코드로 무언가를 생성한다면 load view 단계에서 해주는게 좋겠죠?
그 외에 대부분 처음 한번만 실행되는 작업, 예를 들면 컴포넌트들의 오토레이아웃 설정이라던지 버튼의 action 설정 등등은 viewDidLoad, 즉 컴포넌트들이 메모리에 올라간 후에 하면 됩니다.
두 메소드의 차이점에 대해서는 이 글을 읽어보면 도움이 많이 될 것 같습니다.
이제 viewWillAppear과 viewDidAppear에 대해서 알아볼게요.
viewWillAppear과 viewDidAppear은 뷰가 한번 생성 된 이후에도 다시 나타날 때마다 실행되는 메소드입니다.
이 두 메소드가 나뉘어 있는 이유는 UIView를 Appear하는 작업이 무겁기 때문입니다.
다시 말해서 viewWillAppear에서 무거운 작업을 하게 되면 성능에 영향을 미치게 됩니다.
이곳에서는 단순히 Model에서 저장된 데이터를 가져오는 가벼운 작업 정도를 해주면 좋습니다.
그 다음 viewDidAppear은 View가 다 그려져 있는 상태이기 때문에 비동기처리와 같이 다른 스레드 자원을 활용하는 작업을 해주면 좋습니다.
마지막으로 생소한 부분이 viewWillLayoutSubviews과 viewDidLayoutSubviews일텐데,
이는 autoLayout과 관련된 부분입니다.
대부분 View가 생성되는 시기인
viewWillAppear과 viewDidAppear사이에 그려지지만, 무조건인것은 아닙니다.
예를 들면 autoLayout은 설정된 Frame이나 Bound보다 우선되어 적용되게 되는데,
예를 들면, button에 AutoLayout을 걸어두고, 다음과 같이 viewDidAppear에서 강제로 다시 frame을 설정해줘도
다시 layoutSubviews가 실행되면서 강제로 autoLayout으로 맞춰버리게 됩니다.
override func viewWillAppear(_ animated: Bool) {
print("viewWillAppear")
forwardButton.frame = CGRect(x: 0, y: 0, width: 200, height: 60)
print(forwardButton.frame.size)
}
override func viewWillLayoutSubviews() {
print("viewWillLayoutSubviews")
print(forwardButton.frame.size)
}
override func viewDidLayoutSubviews() {
print("viewDidLayoutSubviews")
print(forwardButton.frame.size)
}
override func viewDidAppear(_ animated: Bool) {
print("viewDidAppear")
print(forwardButton.frame.size)
forwardButton.frame = CGRect(x: 0, y: 0, width: 200, height: 60)
print(forwardButton.frame.size)
}
override func viewWillDisappear(_ animated: Bool) {
print("viewWillDisappear")
}
override func viewDidDisappear(_ animated: Bool) {
print("viewDidDisappear")
}
viewWillAppear
(200.0, 60.0)
viewWillLayoutSubviews
(200.0, 60.0)
viewDidLayoutSubviews
(100.0, 30.0)
viewDidAppear
(100.0, 30.0)
(200.0, 60.0)
viewWillLayoutSubviews
(200.0, 60.0)
viewDidLayoutSubviews
(100.0, 30.0)
그래서 우리가 현재 스크린에 나타나는 컴포넌트의 실제 크기를 이용해야 할 때,
예를 들어서 어떤 버튼의 크기 만큼만 새로운 버튼을 만들어주고 싶을 때 같은 상황을 생각할 수 있겠죠?
그 때는 viewWillAppear가 아닌 viewDidLayoutSubviews에서 크기를 가져와서 잡아줘야 합니다/
세션 준비