Swift | 학습 내용 정리

iOS 개발자로 성장하면서 학습한 내용을 글로 풀어 기록합니다. 제 글에 대한 어떤 리액션도 다 완전 환영!!
SwiftUI 프로젝트에서 이미지 캐시를 구현해보았다.
최근까지 SwiftUI로 채팅 서비스를 구현하는 프로젝트를 진행했다. 텍스트 형태의 채팅 메시지뿐만 아니라 이미지를 최대 5장까지 업로드해서 이미지 뷰어로 보여줄 수 있어야 했다. 채팅 전송 시점에 서버에 저장한 이미지 주소를 소캣 채널을 통해 전달 받아 보여줘야 했다. 채팅 방의 업로드된 이미지 조회만 아니라 앱 전반적으로도 유저의 프로필을 보여주거나, 특정 뷰에서 서버의 이미지가 필요한 경우가 많았다. 앱에서 이미지를 보여줄 때마다 서버에 요청을 보내 이미지 데이터를 조회하는 것은 너무나도 고역이었다. 아니 앱에게 너무 몹쓸 짓을 하는 것 같았다. 이미지 캐싱을 왜 적용해야 했을까? 이미지 캐싱을 적용해야겠다고 마음 먹은건, 채팅 방을 개발하면서 메모리 사용 내역을 보고 난 이후였다. 이미지가 업로드 된 채팅 방에서 스크롤을 한 번 했는데, 스크롤을 할 때마다 네트워크 요청이 들어가서 메모리 사용량이 급격하게 100MB씩 치솟는 걸 보았다. 스크롤을 계속 할 수록 메모리 사용 그래프는 우상향을 그렸다. 정말 나쁜 앱이라고 생각했다. 불러 올 이미지가 많았다면 아마 앱이 터졌을 수도 있다. Kingfisher와 같은 URL 기반으로 이미지 캐싱을 적용해주는 라이브러리 도입을 제일 먼저 고려했다. UIKit 프로젝트를 할 때부터 애용했던 라이브러리라 사용법도 익숙했고 당연히 이미지 캐싱이 잘 될거라고 생각했는데, 'Kingfisher는 어떻게 이미지 캐싱을 적용하는 걸까?'는 질문은 크게 던져보지 않았던 것 같다. 그래서 이번 프로젝트에서는 이미지 캐싱을 담당하는 모듈을 구분하여 직접 만들어보기로 했다. 이미지 캐싱 방식을 서칭해보았다. 이미지 캐싱은 붙은 단어 그대로 '이미지'를 '캐싱'하는 것이고, 캐싱은 자주 쓸 것 같은 데이터를 어떤 공간에 임시적으로 저장해두고 활용하는 것을 의미한다. 그래서, 이미지 캐싱은 자주 쓸 것 같은 이미지 데이터를 특정 공간에 임시적으로 저장해두고 필요할 때마다 조회해서 사용하고, 필요 없으면 지우는 작업을 의미했다. 내가 이해한 바를 정리해 보면, 이미지 캐싱을 위해서는 이미지 데이터 저장을 위한 어떤 공간이 필요하고 임시적으로 저장하는 것이기 때문에 저장 만료 시간에 대한 반영과 계산이 필요하고 (캐싱 전략) 이미지 데이터를 저장하고 이미지 데이터를 불러오고 이미지 데이터를 삭제하는 작업이 반영되어야 한다. 저장 공간 결정
  • 한결
Swift에서의 싱글턴 패턴
싱글턴 패턴. 싱글턴은 어떤 하나의 공유되는 인스턴스를 생성해서 앱 전반적으로 활용하는 방법이다. 위 문장에서 밑줄 그어진 부분이 아무래도 중요할 것인데, 내가 이해한 싱글턴 패턴은 앱이 동작하고 있는 라이프사이클 전반에 걸쳐 하나만 생성되어 마치 값이 메모리에 올라가 있는 것처럼 활용되는 인스턴스를 의미한다. 많은 사람들이 싱글턴 패턴에서 아래의 조건을 잘 충족하는 것이 중요하다고 한다. 하나의 인스턴스만 참조하여 재사용할 것 (= 생성자로 다른 인스턴스 참조를 만들지 못해야 할 것) 앱 라이프사이클 상에서는 계속 유지되는 참조를 가져야 한다. 값이 변경되지 않은 상태(=멤버 프로퍼티)만 정의할 것 그래서 보통은 아래와 같은 형태로, 앱을 이루는 여러 코드에서 '공유(=shared)' 될 수 있는 타입 상수 멤버를 통해 자기 자신의 하나뿐인 인스턴스에 접근한다. 하나뿐인 인스턴스에 모든 뷰 객체(뷰 컨트롤러 객체)나 뷰 모델 객체들이 접근할 수 있는 것은 모두 하나의 주소값만 참조한다는 뜻이다. 그렇기 때문에 앱 전체에서 변경되지 않아야 하는 글로벌한 설정에 대한 부분을 싱글턴 패턴으로 많이 작성하고는 한다. static 상수를 통해서 메모리에 올라와 있는 하나의 객체에만 접근할 수 있게된다. (앱이 동작하는 동안 참조 지속) 다른 여러 파일에서 shared라고 하는 변수가 초기화 되는 것처럼 보여질 수 있는데, 실제로 SomeSingleton.shared 형식으로 접근을 하면, 메모리에 올라간 그 객체의 주소를 참조하게 되는 것이다. 싱글턴 패턴은 어떤 이유에서 사용될까. 다른 여러 객체에서 하나의 참조 주소만 바라볼 수 있기 때문에. 메모리 측면에서 관리가 용이하다. 1번의 연장선에서, 앱 전반에서 '굳이' 필요시마다 객체를 생성할 필요 없이, 하나의 객체로 작업을 처리시킬 수 있다. 아무래도 해당 객체가 단 한 번의 인스턴스 초기화가 되어 앱 라이프 사이클 내에서 관리되기 때문에 1번에서 말한 것처럼 메모리에서의 재사용, 누수 관리가 편할 수 있다. 더 연장해서 살펴보면, 늘 같은 주소를 참조하는 하나의 객체만 재사용하기 때문에 그 객체에 대한 접근 속도도 조금은 더 빨라질 수 있겠다. (초기화 시점을 제외하고는) 2번에 대해서, 앱에 처음 접근하거나 필요에 의해서 저장된 유저 정보를 가져와야 하는 UserManager 객체를 구성한다고 생각해보자. 이럴 때마다 해당 객체를 매번 새롭게 인스턴스 초기화하고, 내부의 자원이나 메서드를 활용하는 방식은 비효율적일 수 있다. 공유되는 하나의 객체가 있고, 유저 정보에 대한 상태를 가지는게 아니라, 그 유저정보를 불러오거나 인증을 처리하는 로직을 하는 메서드들 있다면 싱글턴이 효율적일 수 있다.
  • 한결
👏
1
[SwiftUI] 스유에서 뷰를 그리고(draw) 업데이트하는 데이터 흐름 관리 - @propertyWrappers 1
@propertyWrapper..? propertyWrapper는 Swift 5.1+ 에 소개된, 이름 그대로 property를 감싸는 역할을 하는 어트리뷰트다. 어떤 상태를 가지는 property를 정의하고 값을 반영하는 과정을 추상화 할 수 있게 해준다. 제네릭을 이용해서 다양한 타입을 가질 수 있는 멤버 변수도 동일한 로직을 타게 할 수 있다. 말이 너무 어려운데, propertyWrapper로 많이 관리되는 UserDefaults가 역시 나에게도 이해가 쉬웠다. UserDefaults는 String으로 된 키와 저장할 값을 기반으로 값(상태)을 저장하거나 불러올 수 있다. 저장은 새로운 값으로 업데이트가 될 수 있다는 뜻으로도 쓰일 수 있다. (덮어 쓸 수 있다는 말) UserDefaults에 값을 저장, 업데이트,불러오는 등의 로직은 아주 단순하지만 사용할 때마다 그 구문을 반복해야 한다. 그래서 보통 UserDefaults를 관리하는 객체를 만들어서 활용하는데, 이때 propertyWrapper 개념을 접목하면 조금 더 활용도가 높아진다. propertyWrapper 어트리뷰트가 붙은 객체는 내부적으로 wrappedValue라고 하는 연산 프로퍼티를 구현해주어야 한다. 이 wrappedValue 로직을 통해 멤버에 대해 동일한 로직을 반영해줄 수 있다. 생성자를 통해서 기본값도 넣어줄 수 있다. 정의한 propertyWrapper 객체는 어트리뷰트처럼 활용할 수 있다. @로 호출한 propertyWrapper의 생성자에 필요한 초기 값을 넣어준 형태로 멤버에 반영해주면, 따로 해당 멤버의 값(상태)을 불러오거나 업데이트 하는 로직을 신경쓰지 않아도 된다. SwiftUI에서도 다양한 propertyWrapper들이 활용되고 있는데, 특히 뷰를 그리고, 뷰를 업데이트하는 데이터(상태 값)를 효율적으로 관리할 수 있게 도와주는 로직이 구현되어 있다. @State 이름에서 알 수 있듯이, View 객체의 데이터 상태를 관리하는 멤버 앞에 붙는다. View struct 안에서 활용되기에 당연히 View 객체 안에서 초기화 되어야 한다. 특정 View 객체 안에서 사용되고, 초기화되기 때문에 외부에서 주입을 받아 상태가 관리되지 않아야 한다. (사실 주입은 받을 수 있겠지만, 내부에서 그 주입받은 상태가 관리되지 않기 때문에 private 사용이 권장된다.) 뷰 객체 내부에서 활용되는 자식 뷰 객체에게 그 상태값을 바인딩 프로퍼티와 함께 전달해줄 수 있다. 가장 좋은 점은, @State로 관리되는 상태값에 따라 뷰가 fresh-render되고 뷰가 살아있는 동안 그 상태값이 유지된다는 것이다. class의 인스턴스와 같은 참조형을 상태값으로 활용할 수는 있지만, 해당 인스턴스의 값을 변경한다고 하더라도 상태가 변경되었다고 인지되지는 않는다. (@Published, @ObservableObject와 같은 wrapper가 붙으면 말이 달라지긴 하지만) State 의 구현부를 타고 가보면 DynamicProperty 라는 프로토콜을 채택하고 있는 것을 볼 수 있다. DynamicProperty 프로토콜은 내부적으로 update라고 하는 메서드를 가지고 있고, 이 메서드를 통해서 변경된 상태를 인지하고 뷰를 새롭게 업데이트 해주게 된다.
  • 한결
[UIKit + RxSwift] MVVM 구조에서 Input-Output 패턴으로 UI 컨트롤하기
RxSwift의 기본 컨셉은 아래 콘텐츠를 통해 간단하게 정리해보았습니다. 커스텀한 Observable 객체를 이용해서 MVVM 패턴으로 간단한 iOS 앱을 만들어왔다. View 객체에서 발생하는 이벤트를 인식하고 이벤트에 따른 비즈니스 로직을 ViewModel에서 처리하고, 그걸 View가 원하는 Output으로 돌려주는 형태로 코드를 작성했다. 자연스럽게 앱 전반에서 '이벤트 방출 - 구독 - 연산 - 결과 도출'의 매커니즘으로 모든 이벤트를 Asynchronous하게 처리하는 RxSwift의 여러 API를 학습하게 되었고, 이를 통해 간단하게 UI를 컨트롤 하는 방식을 기록한다. MVVM(Input-Output 패턴) + RxSwift 로 코드를 구성한 화면은 아래의 이미지와 같다. 로그인 화면에서는 email, password를 입력받는 TextField의 텍스트 입력 이벤트를 구독하여 조건에 따라 UI를 컨트롤한다. shopping list 라는 타이틀을 가진 화면에서는 역시 TextField의 텍스트 입력 이벤트를 구독해서 테이블뷰에 셀을 보여주고, 컬렉션 아이템 터치 이벤트를 통해 테이블뷰를 컨트롤한다. Input - Output 패턴 MVVM 아키텍쳐의 주요 골자는 View를 컨트롤 하는 View 객체, 앱에서 활용될 데이터에 대한 명세~메서드를 구축하는 Model 객체, 그리고 그 두 객체 사이를 연결하여 이벤트에 대한 로직을 처리하는 ViewModel 객체의 조합이다. Input - Output 패턴도 이런 골자를 지키면서, 이벤트가 들어오는 Input 객체와 로직의 처리 결과로 View에게 반환되는 Output 객체를 각각 구분하는 컨셉을 가진다. ViewModel에서 활용되는 패턴으로, View에서는 Input 객체에 이벤트를 실어 ViewModel이 이벤트를 구독할 수 있게 한다. Input과 Output 객체를 연결하는 통로는 transform(Input) → Output 이라고 하는 메서드를 보통 활용한다. 프로토콜로 Input - Output 패턴으로 ViewModel 구현의 제약을 정해줄 수도 있겠다. Input - Output 패턴이 적용된 ViewModel의 전형적인 모습은 아래와 같다. 내가 파악한 Input - Output 패턴의 핵심은 요정도이다. View단에서 전달하려고 하는 Event Input과 View단에서 필요로 하는 Data(Disposable한) Output을 각각의 구조체에 묶어 보다 명확하게 인지할 수 있다. 결국 Output 객체의 프로퍼티로 전달하는 값도 Disposable을 반환하는 Observable 객체이다. 이벤트에 따라서 변화하는 Output 값을 View 단에서도 Observer에 구독시켜서 UI를 업데이트 한다. RxSwift, RxCocoa 라이브러리가 가지는 API를 잘 활용하면 보다 편하게 Event Emit을 컨트롤 할 수 있다.
  • 한결
Swift - 의존성 주입과 MVVM
의존성과 주입 의존성 : 다른 것에 의지하여 생활하거나 존재하는 성질. 이라고 국어 사전에 정의되어 있다. 코드에 의존성이 생긴다는 것은 어떤 것일까? 쉽게 생각해보면, 어떤 클래스 내부에서 다른 클래스로 인스턴스를 생성하고, 그 인스턴스를 활용하는 것을 코드적인 의존성이 만들어졌다고 볼 수 있을 것 같다. 위의 SomeViewController 코드에서 보면, SomeViewController라고 하는 클래스는 APIService라고 하는 특정한 클래스에 의존성을 가지고 있다. APIService 내부에 구현된 fetch 라고 하는 메서드를 내부에서 활용하는데, APISerivce 클래스 구현부에서 해당 메서드의 이름을 fetches 라고 바꾼다고 하면 어떻게 될까? 당연하게도 SomeViewController 코드에서도 그 이름에 맞게 코드를 바꿔줘야 한다. 이것만이 아니라, 해당 인스턴스를 다른 클래스의 인스턴스로 바꾼다고 가정하면 코드로 바꿔줘야 하는 부분은 더 늘어날 수 있다. 코드에서는 의존성이 발생하지 않게 하는 것은 쉽지 않다. *싱글톤으로 작성된 객체를 타입 멤버로 접근한다면, 클래스 내부에서 의존성이 생기지 않을 수는 있겠다. 주입 : 흘러 들어가도록 부어 넣음. 이라고 국어 사전에 정의되어 있다. 코드에서 '주입'을 한다는 것은 쉽게 생각해보면 인스턴스를 생성할 때, 생성자에 값을 넣어(=주입하여) 내부의 저장 속성의 값을 초기화 시켜주는 것이라고 생각해볼 수 있다. 그러면 의존성을 주입한다는 것은, 외부에서 특정 클래스의 인스턴스를 만들어서 생성자로 전달을 하는 행위를 말하는 걸까? 아래의 코드처럼 APIService의 인스턴스를 단순히 생성자로 넣어준다고 하더라도 SomeViewController는 여전히 APIService에 의존성을 가지고 있다. 의존성을 넣어주는 것이기 때문에 의존성 주입이라고 볼 수 있겠지만, 정확하게 APIService 라고 하는 타입의 인스턴스를 넣어줘야 해서 확장성이 떨어진다. Swift에서의 의존성 주입 그러면 확장성을 갖추면서도 의존성을 외부에서 분리시키는 방법은 Swift에서 어떻게 할 수 있을까? 혼자 질문을 던지고 바로 답을 내려버리는 것 같아서 좀 그렇지만, Protocol as a Type의 개념을 활용할 수 있을 것 같다. Swift에서 프로토콜은 정말 다양한 방식으로 활용되는데, 단순히 어떤 객체에 구현부를 강제하는 것을 넘어서 비슷한 역할을 하는 객체들을 하나의 타입으로 묶어줄 수 있는 모듈러 역할로 활용할 수 있다. 즉, 특정 프로토콜을 동일하게 채용하는 클래스는 어느정도 '아 그 프로토콜의 일을 하겠구나'를 쉽게 유추할 수 있다. 이렇게 동일한 프로토콜을 채용한 객체(=클래스의 인스턴스)를 생성자로 주입시키면 어떨까? 이럴경우, SomeViewController는 이제부터 SomeDataRelatedProtocol이라는 프로토콜 타입에 의존성을 가지게 된다. 그러면 외부에서 해당 프로토콜을 채용한 어떤 객체 타입도 주입할 수 있게 된다.(Protocol as a Type) 주입하는 객체를 쉽게 교체할 수 있고, 쉽게 확장할 수 있다. 역으로, 주입되는 객체 역시 SomeDataRelatedProtocol이라는 프로토콜에 의존성을 가지기 때문에 오히려 역의 의존성 체계가 설계된다. 즉 구체적인 객체가 추상적인(=프로토콜 타입) 요소에 의존하게 되는 의존 관계 역전 원칙을 따르게 된다. MVVM 아키텍처는 왜 쓰는걸까? 아니 아키텍처는 왜 나누는 건가? 작은 앱을 혼자서 만들어간다면 ViewController 안에 데이터 모델링 코드, Network API 코드, 뷰를 그리는 코드 등을 모두 구현하는 것도 나쁘지 않을 수 있다. 어차피 ViewController가 어떻게든 접근해야 하는 객체나 뷰를 만들기 때문이다.
  • 한결
[Swift - UIKit] Swift 프로젝트에서 VC간에 데이터를 전달하는 4가지 방법
모바일 앱의 특성상, 작은 화면에서 한 번에 많은 데이터를 보여주는 것이 쉽지 않다. 그래서 하나의 앱에서도 다양한 형태의 데이터를 다양한 페이지에서 보여주고 있다. 그런 이유로 페이지간 데이터 전달이 중요한데, 지금까지 내가 학습한 4가지의 데이터 전달 방식에 대해 기록해두고 기억해보고자 한다. Swift는 기본적으로 클래스, 구조체로 화면에 대한 구조(=인스턴스)를 만든다. 구조의 멤버들은 뷰의 객체로서(아닌 경우도 있지만) 짜여진 구조에 맞게 데이터를 그려낸다. 인스턴스를 만드는 것이기 때문에 그 인스턴스가 생성되는 시점 + 생성되어 활용되는 그 구조의 공간이 무엇보다 중요하다. 전환하고자 하는 페이지(거의 VC)의 인스턴스를 생성하면, 내부의 멤버에 직접/간접적으로 접근해서 값을 할당해주는 것이 값 전달의 기본이다. 인스턴스의 멤버에 값 할당하기 해당 방법으로는, 넘어가고자 하는 VC의 인스턴스를 생성하고 해당 VC의 생명주기가 활성화되기 전에 인스턴스 멤버나 인스턴스 메서드를 실행시키는 형태로 다른 VC에 값을 전달한다. 넘어가고자 하는 VC에 대한 명확한 공간(=인스턴스)를 하나의 스코프 내에서 컨트롤 할 수 있기 때문에, 어떻게보면 가장 쉬운 데이터 전달 방식이 될 수 있다. '이전 VC에 대한 인스턴스를 생성하여 값을 넘겨주는 방식으로 넘어오기 전 VC로 접근할 수 있겠구나!' 라고 생각할 수 있겠지만, (어떻게 보면 당연하지만) 그렇지 않다. 넘어온 VC 환경에서 이전 VC에 대한 인스턴스를 생성한다는 것은 (아래 그림처럼) 완전히 새로운 VC의 인스턴스 환경을 구축하는 것과 같다. (⇒ 평행 세계에 이름만 같은 공간을 찾아가는 느낌이랄까) 그렇기 때문에, 넘어오기 전의 환경으로 컨택스트를 넘겨줄 수 없다는 의미가 된다. 클로저 활용하기 해당 방법으로는, 넘어가거나, 넘어온 VC에 값을 전달하는 간단한 익명함수(클로저)를 선언하고 (멤버 변수) VC 인스턴스에 접근하는 곳에서 해당 익명함수(클로저)의 내부 로직을 구현해주고 필요한 시점에 클로저를 호출시켜 컨택스트를 해당 시점으로 가져오는 형태로 다른 VC에 값을 전달한다.
  • 한결
[Swift - UIKit] BaseView, APIService 싱글턴 패턴, DispatchGroup 등을 활용한 간단한 영화 평점 매기기 서비스를 구현해보았다.
영화 앱에서는 TMDB API를 활용하여 다음과 같은 페이지를 구현했습니다. 자세한 코드는 아래 깃헙에서 확인 가능합니다. 메인페이지 - 현재 상영작 / 요즘 주목받는 작품 / 역대 최고의 평가를 받은 작품 리스트를 테이블뷰 + 가로 컬랙션뷰 형태로 노출 영화 상세 페이지 - 컬랙션 아이템 터치시 해당 영화 id값을 기반으로 내가 매긴 별점, 줄거리, 출연진, 유사한 영화 리스트가 한 페이지에서 노출 영화 검색 페이지 - 텍스트 필드에서 키워드로 영화를 검색한 결과를 테이블뷰로 노출 (검색 결과가 없을 경우 핸들링) BaseView를 만들어 뷰의 상속 관계 이용하기 UIView를 상속받는 기본적인 BaseView를 만들어서 해당 뷰를 상속받는 뷰 객체들이 미리 정의된 메서드를 오버라이딩하여 UI와 기능을 보다 빠르고 편하게 구축할 수 있게 코드를 작성했다. 프로젝트 코드에서 UI의 레이아웃을 잡거나 서브뷰를 구성한다던지, UI와 관련된 코드를 작성하는 경우에서 반복이 많다. 위의 코드처럼 베이스가 되는 뷰 클래스를 하나 만들어두면, 어떤 메서드에서 레이아웃을 잡고, 어떤 메서드에서 UI 코드를 잡을지 따로 신경쓰지 않아도 된다. (제약을 미리 하나 만들어두는 느낌이라 Protocol을 활용하는 것과 비슷하다.) 자주 사용되는 UI 객체 역시 BaseView를 만들어서 활용했다. 특히, 라벨이라던지 박스 형태의 컴포넌트는 프로젝트 페이지별로 많이 활용되기 때문에(=인스턴스를 생성할 때가 많음) 베이스가 되는 클래스를 만들어 두는 것이 정말 유용했다. 인스턴스로 생성된 뷰 객체들은 외부에서 되도록이면 속성을 직접 건드리지 않고, 내부에 구현된 메서드를 이용할 수 있도록 제어자를 만드는 것에 집중했다. BaseLabel.swift BaseBoxItem.swift ViewController에서 컨트롤할 뷰 객체 전체를 감싸는 mainView를 분리하였다. VC에서는 해당 전체 뷰 객체의 인스턴스를 만들어서 loadView 메서드 내부에서 기존의 root view를 mainView로 대치하여 사용했다. 즉, 커스텀한 뷰로 nil 상태의 root view를 대체해주는 것이 loadView의 역할인 것이다. loadView 메서드를 오버라이드 할 때 주의해야 할 점은 (어떻게 보면 당연한 이야기 일 수 있는데) super의 loadView 구현부를 상속받지 않는 것이다. ViewController에서 View의 레이아웃이나 UI 코드에 대한 관심사를 분리시킬 수 있어 아주 유용해 보인다. 다만, 뷰 컴포넌트들의 구조가 복잡해지거나, 한 페이지에 정말 다양한 뷰 요소들이 들어가는 앱이라면 클래스의 뎁스를 많이 두는 것이 코드를 유지보수 하는 관점에서 좋을지는 계속 고민해볼 필요가 있을 것 같다.
  • 한결
[Swift - UIKit] do - try - catch 구문을 이용해서 UITextField의 입력값 조건에 맞게 검증하기
회원가입을 한다던지, 맞춤형 서비스에서 네이밍을 하는 형태에서는 유저가 입력하는 값을 특정한 조건에 맞게 제한할 수 있다. 서비스 코드에서 '조건에 따른 제한'을 가한다는 것은 데이터에 대한 '조건별 제어'가 가능해야 한다는 것이고, 조건을 충족하지 못 한 경우에는 에러가 발생할 수 있다는 의미이기도 하다. do~try - catch 구문을 활용하면 특정 코드의 액션의 결과를 try로 기다린다. 성공할 경우 do 스코프 이내에서 try 이후의 구문이 실행된다. 실패할 경우 catch 스코프에서 실패에 따른 로직이 실행된다. 에러 케이스별로 실패 구문을 관리할 수 있다는 점이 좋다. 유저의 닉네임을 아래 조건에 따라 검증한다고 할 때, do~try - catch 구문으로 어떻게 에러에 따른 처리를 할 수 있을지 한 번 살펴보자. ValidationService 클래스를 구성해서 검증하는 로직과 에러 여부에 따라 문구를 보여주는 영역을 구분해보았다. 닉네임은 빈 문자열로 저장될 수 없다. 닉네임은 공백 포함 2자 이상 10자 이하로 구성되어야 한다. 닉네임에는 숫자가 들어갈 수 없다. 닉네임에는 @, % 라는 특수문자가 들어갈 수 없다. 1. ValidationService 클래스 생성하기 클래스 내부에 에러 조건에 대한 케이스 관리를 할 Errors 열거형을 만들었다. validateNickname 이라는 타입 메서드에서 각각의 조건을 검증하는 private 함수들을 실행시키다. 각각의 검증 함수에서 true 값을 확인 할 경우, 해당하는 에러 케이스로 쉽게 throw 해줄 수 있다. 외부에서는 이 메서드만 호출해서 입력 받은 값을 검증하고, 열거형 케이스에 따라 분기처리 할 수 있다. 2. UITextFieldDelegate 내부에서 do~try - catch 구문으로 에러 핸들링하기 TextField가 있는 VC에 UITextFieldDelegate를 위임해주면 textFieldDidChangeSelection 이라는 메서드 내부 로직을 구성해줄 수 있다. (꼭 이 메서드가 아니어도 괜찮다.) 해당 메서드 내부에서 do 구문으로 ValidateService 클래스의 validateNickname 타입 메서드를 호출한다. (textfield에 입력되는 글을 인자로 넣어준다.) 만약 조건에 충족되는 텍스트가 입력될 경우 성공 문구 보여주고, 확인 버튼이 터치할 수 있는 상태가 된다. 조건을 충족하지 않는 텍스트가 입력될 경우, 각 에러 케이스에 맞는 catch 구문으로 빠진다. 각각의 에러 케이스를 굳이 만들지 않고 catch 스코프 하나로 모든 에러 케이스를 핸들링 할 수도 있다.
  • 한결
[Swift - UIKit, CoreLocation] iOS에서 사용자 정보 권한과 위치 권한 획득
iOS, iPadOS와 같이 애플 기기에서 돌아가는 운영체제는 사용자의 개인정보 보호에 아주 민감하다는 것을 많은 기사와 사례를 통해서 알 수 있다. 애플은, 어떤 기기에서 돌아가는 앱이든, 사용자의 앱 사용 경험을 헤치지 않으면서 유저가 권한 부여에 동의한 정보에 대해서만 앱이 활용할 수 있는 강력한 통제 시스템을 구축하고 있다. 사용자 > 기술 이라는 기본적인 모토에서 앱이 사용자의 정보 통제권을 쉽게 가질 수 없도록 만든다. iOS의 메이저 버전이 올라가면서 사용자가 앱에 줄 수 있는 개인 정보 통제권 유형을 *더 세분화 하고 있다. * 위치 정보를 허용한다고 했을 때, 어느 순간부터 아래와 같은 선택지를 사용자에게 제공해주었다. (iOS13+) - 허용하지 않음 - 한 번만 허용 - 앱을 사용할 때만 허용 - 항상 허용 정리하면, 앱을 만들 때 사용자가 부여한 권한의 정도가 어떻게 되는지를 계속 신경써야 한다는 것이고, 사용자가 갑자기 해당 정보에 대한 권한을 차단하지는 않았는지를 계속 인앱 상황에서 모니터링 하고 있어야 한다는 점이다. 위치 정보에 대한 권한을 획득하고 획득한 위치 기반으로 현재 날씨를 보여주는 간단한 앱을 만들어 보면서 위치 서비스에 대한 권한 획득을 경험해봤다. 순서대로 기록해본다. 간단히 위치 정보 활용 동의받고 이용하기 info.plist에서 Privacy 항목 활성화하고 사용자 위치 정보를 수집하는 이유가 담긴 문구 수정하기 해당 항목에 작성하는 문구가 추후에 권한을 획득하는 Alert의 메시지에 반영된다. 위치 정보를 왜 수집하고, 어떻게 사용되는지 등에 대해 최대한 자세히 작성해야 앱 출시에 제한이 없다. 사용자가 애플 기기 시스템에서 위치 서비스에 대한 권한을 켜뒀는지 확인하기 CoreLocation 패키지를 가져오고 CLLocationManager 클래스의 인스턴스를 생성하여 CLLocationManagerDelegate 로 위치 권한을 획득하는 역할을 VC에 위임해준다. .locationServicesEnabled 라는 타입 메서드로 유저가 시스템 상에서 위치 서비스를 활성화했는지 확인한다. else 구문으로 빠지면, 보통 alert 등을 통해서 시스템 설정에서 위치 서비스를 켜달라는 식으로 사용자의 선제 액션을 유도할 수 있다. (혹은 이동시킬 수도 있겠다.)
  • 한결
[Swift - UIKit] iOS에서의 기본적인 Notification System
알림센터나 사이드 푸쉬 등으로 특정 블록 형태의 UI가 뜨는 것을 Notification 이라고 한다. 카카오톡에서 친구가 채팅을 보냈을 때, 쿠팡에서 특가 상품이 떴을 때, 주변에 토스를 사용하는 사용자가 발견되었을 때와 같은 상황에서 주로 알림이 온다. 이런 기본적인 '알림'이라고 하는 것을 다룰 수 있는게 iOS Notification System이다. 내장된 User Notification Framework를 이용하여 쉽게 알림을 구현할 수 있다. 리모트 방식의 알림은 서버를 통해 인증된 유저에게만 보여줄 수 있기 때문에 APNs 라고하는 서비스를 거쳐야 한다. Notification System의 두 가지 방식 Notification 방식은 Local, Remote 형태가 있다. 이름에서 두 방식의 차이를 대강 알 수 있다. Local 알림에 전달할 내용이 전부 앱에 들어있다. 루틴한 내용이나 비슷한 컨텐츠를 전달해주는 용도로 사용된다. (디데이, 루틴, 투두 어플 등) Remote 서버단에서 보내는 알림을 컨트롤하는 형식. (푸시 알림이라고도 함) (서버에서 데이터가 오기 때문에) 유저가 알 수 없는 시간에 서로 다른 컨텐츠를 전달해주는 용도로 사용된다. 광고, 개인화된 추천, 채팅 알림(ex. 카카오톡 채팅별 알림) 등의 알림이 리모트 형식이다. Notification System에서 유의해야 할 점 Notification이 동작하기 위해서는 반드시 유저에게 권한 동의를 받아야 한다. 아이폰의 여러 앱을 써보면 알겠지만, 앱을 처음 실행할 때나 앱을 실행시키고 있는 도중에도 계속 '알림'에 대한 권한을 획득하려고 하는 뷰가 나타난다. 어떤 앱에서는 '권한이 어떤 시점에서 부여되지 않았다'는 사실을 알려주는 alert을 보여줄 때도 있다. 애플에서는 알림에 대한 유저 동의 여부를 강하게 체크한다. 무분별하게 알림을 내뱉는 앱은 앱의 사용 경험을 넘어서 아이폰의 사용 경험 자체를 헤칠 수 있기 때문에 애플에서 권한 획득을 중요하게 생각하는 것 같다. 커머스나 앱 자체로 비즈니스 가치가 발생하는 서비스들은 사용자에게 알림 권한을 획득하기 위해서 노력한다. 앱이 종료된 시점에도 우리 서비스가 있고, 우리 서비스를 통해 새로운 이점을 얻을 수 있다는 사실을 알림을 통해 간편하게 전달할 수 있기 때문이다.
  • 한결
[Swift - UIKit] UIViewController 위에 여러 Protocol을 적용하여 동작하는 다양한 뷰 만들기
코드 베이스로 뷰를 그리는 연습을 하기 전, SB + Outlet 코드로 뷰를 그리는 연습을 하면서 최종적으로 만들어 본 TravelProject에 대한 정리 글입니다. 전체 코드는 해당 링크에서 볼 수 있습니다. Swift를 학습하면서 정말 다양한 뷰를 직접 만들어보고 있다. 24년 5월 27일부터 30일까지 스토리보드 기반으로 UIViewController 위에서 코드로 여러 화면을 그리는 연습을 했다. 그 과정에서 나 스스로 '이건 꼭 기록으로 남겨봐야겠다.' 하는 점들이 있어서 남겨본다. 1. UIViewController 위에 커스텀한 TableView 올려보기 (with. XIB) UIViewController에 UITable을 핸들링하기 위한 Delegation, Datasource 프로토콜을 적용해준다. *Protocol을 통해 delegation을 위임하는 것에 대한 내용은 요 링크로 짧게 작성해봤다. UITable에 들어갈 커스텀한 TableCell을 작업하기 위해 Swift 파일과 XIB 파일을 함께 생성해준다. XIB에서 작업한 커스텀 셀을 재사용이 가능한 셀로 만들기 위해 정확한 Identifier를 지정해줘야 한다. 이 부분 역시 UITableViewCell 클래스를 상속받는 클래스들이 identifier라는 멤버 변수를 가질 수 있도록 protocol + extension 조합으로 코드를 미리 작성해두면 편하다. UIViewController 위에 올려진 UITableView의 인스턴스에 커스텀한 Cell을 연결하고, 어떤 행에 어떤 셀을 어떻게 보여줄 것인지를 위임받은 코드에 설정해준다. 코드로 한 번 보자. 위 코드 형태가 Swift에서 테이블 뷰를 그리는 가장 기본적인 방법이다. cellForRowAt 에서 커스텀하게 생성한 TableViewCell 클래스의 인스턴스를 as 키워드로 정확하게 캐스팅 해줘야 해당 클래스 내부에 구현한 멤버 요소들을 불러와 사용할 수 있다. 2. 연결된 VC에 데이터 넘겨주기 테이블 뷰에서 특정 셀을 터치하여 선택할 경우, 연결된 페이지 뷰(UIViewController)로 데이터를 전달해야하는 경우가 있다. 여러 방법이 있겠지만, 가장 쉽다고 생각하는 방법은 해당 VC의 멤버 변수(Outlet 변수 포함)에 값을 할당하는 것이다. 테이블의 특정 셀이 선택되어 연결된 VC 인스턴스가 Present되기 전에 반드시 데이터 자체를 VC의 내부 멤버들에 할당하는 코드가 반영되어야 한다.
  • 한결
[Swift - System] Swift의 Sandbox 시스템과 UserDefaults.
App Sandbox 시스템 App Sandbox 시스템은 유저의 데이터를 보호하는 목적으로 커널 레벨에서 앱 자체의 데이터 접근을 제어할 수 있는 기술이다. (말이 좀 어렵다.) Swift는 기본적으로 C언어 - Objective-C 언어를 기반하기 때문에 다른 가상 머신 없이 바로 커널에 명령을 쏠 수 있다. 이 과정에서 앱이 동작하는데 필요한 특정 목적의 데이터(ex. 유저의 개인 정보라던지, 네트워크 연결성 등)를 위험에서 보호한다. 위험은 다양할 수 있는데, 앱 규모가 커질수록 더 많은 위험에 노출될 수 있겠다. 샌드박스에 보관하는 데이터를 앱과 어떻게 상호작용 할지를 개발자단에서 컨트롤 하면서, 유저로부터 올 수 있는 문제를 사전에 컨트롤 할 수 있다. 이 데이터를 앱이 사용한다는 것을 유저에게 특정 *인지 이벤트로 인식시키고, 앱이 그 데이터만 활용한다는 명확성을 부여해야 하는 시스템이 샌드박스 시스템이다. 명시적으로 허락 받지 않은 접근은 런타임이 차단한다. *내가 이해한 인지 이벤트는 다음과 같다. 우리가 앱을 설치하고 처음 켰을 때, 런치 스크린 이후에 각종 동의를 받는 경험을 했을 것이다. 이런 데이터에 대한 접근 및 활용 동의가 샌드박스를 이용해 인앱에서 데이터를 관리할 수 있는 권한을 부여한다고 볼 수 있다. - 위치 기반 데이터 활용 동의 - 사진 데이터 접근 및 활용 동의 우리가 앱스토어에서 특정 앱을 설치하면 iOS는 해당 앱을 위한 샌드박스 영역을 생성한다. 위 이미지처럼 Bundle, Data, iCloud 를 위한 공간이 생성된다. 쉽게 생각해서 유저의 의도와는 상관없이 앱의 운영을 위해 읽기, 쓰기를 위한 문서 저장 공간(애플은 데이터 파일을 Documents라고 한다 보통)을 만드는 것이다. 외부 다른 앱이나 사용자가 접근하기 힘든 공간을 이용해서 앱의 보안을 높이는 시스템이라고 볼 수 있다. 앱을 실행하면, 앱은 iOS 전체 파일관리 시스템에서 자기자신의 샌드박스를 바라본다. 샌드박스에서 필요한 데이터를 활용한다. (읽고, 쓰고, 업데이트하고 다 한다) 앱을 종료한다. 해당 앱을 위한 샌드박스 위치를 자체적으로 변경한다. 앱을 다시 실행한다. 해당 앱을 위한 변경된 샌드박스 위치를 바라볼 수 있게 한다. 즉, 특정 앱을 위한 샌드박스가 늘 같은 위치에 있는 것이 아니다. User Defaults 샌드박스 내부에 해당 앱을 위한 user's defaults database가 생성된다. 이 데이터베이스는 문서형 NoSQL 데이터베이스처럼 데이터를 키-값 형태로 저장한다. 정해진 여러 필드가 있고, 하나의 데이터가 해당 필드의 값들을 가지는 관계형 데이터베이스 형태는 아니다. UserDefaults로 관리되는 데이터는 앱이 가동되거나 어떤 이벤트를 처리할 때, 기본적인 상태들을 관리한다. (공식 문서에 따르면 - determine an app’s default state at startup or the way it acts by default.) 핵심 사용 방식은 공식 문서의 다음 문장으로 정리될 것 같다. UserDefaults 클래스가 데이터를 바라보고 있다가 캐싱을 통해 데이터를 앱에 필요하게 세팅한다. 당연히 서비스 레이어에 민감하게 적용될 수 있는 데이터나 처리는 좋지 않다. At runtime, you use UserDefaults objects to read the defaults that your app uses from a user’s defaults database. set 메서드를 통해서 키-값 형태로 값을 저장하고, UserDefaults 클래스의 인스턴스를 생성해서 저장한 값을 키로 조회할 수 있다. Dictionary를 사용하는 방식과 뭔가 비슷하다.
  • 한결
[Swift - UIKit] UIViewController에서의 View life Cycle API
UIViewController에서 View 객체를 로드하고 (그리고) 어떤 페이지로 전환되어 그 객체가 사라지는 과정을 관리하는 시스템을 내가 학습하기로는 View Life Cycle 이라고 한다. View 객체의 생명주기를 관리하는 UIViewController 클래스의 내장 오픈 메서드(API라고 부르겠다)는 여러가지가 있지만, 대표적으로 위 이미지의 5가지가 대표적일 것 같다. viewWillAppear viewIsAppearing viewDidAppear viewWillDisappear viewDidDisappear 사실, API의 prefix에 붙은 키워드만 보더라도 View 객체가 어떤 상태일지에 따라 서로 다른 API가 동작한다는 것을 쉽게 유추해 볼 수 있을 것 같다. (will, -ing, did) 모달이나 페이지 전환에 따라 특정 View 객체가 어떤 생명 주기를 가지는지를 각 API에 print 구문으로 확인해봤다. XCode로 앱을 처음 빌드할 때. UIController의 viewDidLoad가 가장 먼저 앱을 그리는 로직을 수행한다. 그리고 나서 순서대로 willAppear → isAppearing → didAppear API가 호출된다. (prefix 만으로도 쉽게 유추가 가능하다.) 버튼 액션으로 모달 페이지가 떠오를 때. 앱 화면 하단에서 떠오른 모달 페이지가 직전 View를 가리더라도, 해당 View 객체 생명 자체를 사라지게 하지 않는다. 즉, 이전 화면을 완전히 가리지 않는다면 해당 View는 생명선을 유지한다는 뜻이다. 해당 모달을 내리더라도 willAppear, isAppearing, didAppear API가 콘솔에 전혀 찍히지 않는다. 모달 페이지에서 특정 액션을 발생시켜서 뒤에 있는 View에 어떤 화면 전환이 필요하면, 그걸 처리하는 로딩 로직을 만들어야 할 것 같다. 버튼 액션으로 페이지 자체가 전환될 때. 화면이 Push되어서 완전 새로운 화면이 이전 View를 가릴 경우, 해당 View 객체는 생명을 다한다. 순서대로 willDisAppear, didDisappear API가 호출된다.
  • 한결
[Swift - UIKit] Alert, ActionSheet 구현하기
들어가면서. 웹이나 모바일 앱을 사용할 때, 유저에게 특정 정보를 '확인' 받거나 유저가 한 행동에 대해서 '주의'를 주거나 어떤 이유에서든 앱 내부에서 유저에게 정보를 주고 싶을 때 보통 Alert 라고 하는 요소를 활용한다. 웹 브라우저 중앙 상단에서 팝업 되거나, 모바일 앱 중앙 영역에서 작은 박스 형태로 팝업 되는 경우가 보통이다. iOS에서 동일한 역할을 하는 Alert, ActionSheet는 UIAlertController 클래스의 인스턴스로 생성한다. 문서를 살펴보면 알겠지만, UIAlertController는 기본적으로 UIViewController를 상속받는다. Alert 요소에 반영된 버튼을 통해 유저의 특정 행동(이벤트)을 받기 때문에 당연히 Control 요소가 있다. 이 말은 @IBAction을 코드로 지정해줄 수 있다는 것이겠다. 그러면 정말 간단하게 iOS Alert를 만드는 기록을 남겨본다. Alert 만들기. UIAlertController 클래스로 우리가 만들려고 하는 Alert 인스턴스의 속성들을 지정하여 생성하면 된다. 생성한 Alert 인스턴스에 액션을 추가해준다. 액션이라고 해서 거창한 것 같지만, Alert에 '확인', '취소'와 같은 버튼을 추가하는 것이라고 생각하면 편하다. 인스턴스에 addAction 메서드를 이용해서 등록하고 싶은 액션을 연결하면 된다. UIAlertAction 클래스로 인스턴스를 생성해 인자로 넘겨주면 된다. UIAlertAction의 style 속성에는 기본적으로 .default, .cancel, .destructive 라고 하는 미리 지정된 Enum의 값이 있다. Alert 버튼으로는 보통 '확인'을 받기 때문에 특정 액션을 잘 컨트롤 하지 않겠지만, *handler 속성에 특정 액션을 주입할 수도 있다. *내가 파악한 바로는, handler 속성에는 액션 객체들의 묶음이 오는데, { key in actions } 형태로 작성해서 등록하는 여러 액션을 순환하며 순차적으로 동작하는 방식인 것 같다. 공식 문서에 보면, Alert에 대한 스타일링, 액션을 지정하고 present 메서드를 이용해서 Alert 인스턴스를 등록하라고 되어 있다. 그러면 하면 된다. After configuring the alert controller with the actions and style you want, present it using the present(_:animated:completion:) method.
  • 한결
[Swift - UIKit] NavigationItem에서 #selector로 메서드 바인딩
UINavigationItem 아웃렛 위에 UIBarButtonItem을 설정하기 위해서는 다양한 방식을 사용할 수 있다. 버튼 UI기 때문에 버튼의 액션을 컨트롤 할 필요가 있다. 즉, Button에 target-action 설정이 필요하다. UIBarButtonItem 클래스 인스턴스를 코드로 생성해보면 삽입해야 하는 action 속성의 타입이 Selector 로 나오는 경우를 볼 수 있다. 이 경우에 @objc 라고하는 어트리뷰트를 붙인 메서드를 등록하게 된다. 어트리뷰트 이름에서 유추할 수 있듯이 해당 속성은 Objective-C Structure에 등록된 구조체 타입이다. Objective-C에서 동적인 무언가를 만들어내는 메서드를 바인딩 할 때 사용되었던 구조체다. Objective-의 경우런타임단에서 메서드를 바인딩하여 동적인 처리를 할 수 있게 해주는데, Swift에서는 컴파일 단계에서 메서드를 바인딩하기 때문에 Swift에서 Selector 타입을 사용하기 위해서 @objc 라고 하는 어트리뷰트가 필요해진 것이라고 보면 되겠다. (나는 그렇게 이해했다.) 정리하자면, Swift에서 Objective-C에 의존적인 API를 활용하기 위해서 @objc 라고 하는 어트리뷰트를 자주 사용하게 될 수 있다. 이는 Swift 컴파일러가 해당 어트리뷰트를 이해할 수 있게 도와주는 역할을 하는 것이라고 쉽게 이해하면 되겠다. 참고한 문서 👇
  • 한결