Share
Sign In

우당탕탕 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라고 하는 메서드를 가지고 있고, 이 메서드를 통해서 변경된 상태를 인지하고 뷰를 새롭게 업데이트 해주게 된다.