Share
Sign In

우당탕탕 iOS 공부

Tuist로 프로젝트를 모듈화 할 때, 외부 패키지를 어떻게 활용해야 할까? (feat. static/dynamic & mangled name)
최근 진행하고 있는 프로젝트와 직전에 수행한 프로젝트에서는 Tuist를 이용해서 역할별로 모듈을 구분했다. 앱 구성을 위해 기능 단위로 모듈을 구분하고, 모듈간의 연결이 필요한 곳에만 Target을 지정해 의존성을 관리했다. 외부 패키지(라이브러리)를 활용할 때도, Tuist를 통해 모듈별로 필요한 라이브러리를 주입할 수 있어 프로젝트 관리가 수월했다. 문제는 코드를 런타임에 빌드하였을 때 마주할 수 있었다. Tuist CLI로는 프로젝트가 문제없이 구성되었는데, 지정한 스킴에서 앱 빌드가 성공하지 못했다. 콘솔에서 RxSwift 패키지와 관련된 처음보는 에러를 확인할 수 있었다. 외부 패키지 설정과 관련된 문제일 것이라 판단하고 프로젝트 설정부터 톺아봤다. 문제 인식 Tuist 설정 파일에서 Package.swift 파일에 외부 패키지들을 static framework로 등록하고, 외부 패키지를 활용해야 하는 여러 모듈의 프로젝트 파일에서 external 타겟으로 중복되게 지정하여 tuist generate 로 프로젝트를 생성했더니 콘솔에서 "여러 타겟에서 동일한 외부 타겟이 링크(linked)되었기 때문에 static product 환경에서 예기치 못한 사이드이팩트가 발생할 수 있다"는 Warning을 마주할 수 있었다. 우선은 Warning 정도라고 생각하고, 일단 앱 구성을 위한 모듈별 코드를 작성했다. 그리고 나서 여러 모듈(CommonUI, Domain, Data 모듈)을 조합해서 Feature 모듈을 개별 App Scheme으로 빌드했다. 그 순간 아래와 같은 에러 메시지와 함께 런타임 에러가 발생했다. 문제 서칭 이전에 다른 프로젝트를 진행하면서는 마주한 적 없던 에러였기 때문에 빠르게 서칭하였고, Tuist로 모듈화할 때, 외부 패키지를 static하게 등록하여 활용하여 발생한 문제임을 파악하게 되었다. 그러면 왜 모듈로 구분한 서로 다른 프로젝트에서 동일한 외부 패키지를 활용할 때, static framework로 등록하면 side effect가 발생하여 빌드 과정에 demangle ~ 에러가 발생했던 것일까? 이 궁금증에 조금 더 접근하기 위해서는 static/dynamic framework에 대해 이해를 하는 것이 좋다. 그리고 failed to demangle ~ 로 작성된 에러 메시지의 demangle이 무엇인지도 알아야 한다. Swift 프로젝트에서 Static, Dynamic framework 앱 실행 파일(excecutable file)을 생성하는 시점에 컴파일러가 외부 패키지 파일을 소스 코드 파일과 함께 바이너리 실행 파일로 링킹하는지 여부로 Static, Dynamic 프레임워크를 구분할 수 있다. 쉽게 생각해서, 아래의 두 가지 경우를 구분하는 것이 Static 또는 Dynamic한 프레임워크를 구분하는 기준이라는 것이다.
  • 한결
SwiftUI 프로젝트에서 이미지 캐시를 구현해보았다.
최근까지 SwiftUI로 채팅 서비스를 구현하는 프로젝트를 진행했다. 텍스트 형태의 채팅 메시지뿐만 아니라 이미지를 최대 5장까지 업로드해서 이미지 뷰어로 보여줄 수 있어야 했다. 채팅 전송 시점에 서버에 저장한 이미지 주소를 소캣 채널을 통해 전달 받아 보여줘야 했다. 채팅 방의 업로드된 이미지 조회만 아니라 앱 전반적으로도 유저의 프로필을 보여주거나, 특정 뷰에서 서버의 이미지가 필요한 경우가 많았다. 앱에서 이미지를 보여줄 때마다 서버에 요청을 보내 이미지 데이터를 조회하는 것은 너무나도 고역이었다. 아니 앱에게 너무 몹쓸 짓을 하는 것 같았다. 이미지 캐싱을 왜 적용해야 했을까? 이미지 캐싱을 적용해야겠다고 마음 먹은건, 채팅 방을 개발하면서 메모리 사용 내역을 보고 난 이후였다. 이미지가 업로드 된 채팅 방에서 스크롤을 한 번 했는데, 스크롤을 할 때마다 네트워크 요청이 들어가서 메모리 사용량이 급격하게 100MB씩 치솟는 걸 보았다. 스크롤을 계속 할 수록 메모리 사용 그래프는 우상향을 그렸다. 정말 나쁜 앱이라고 생각했다. 불러 올 이미지가 많았다면 아마 앱이 터졌을 수도 있다. Kingfisher와 같은 URL 기반으로 이미지 캐싱을 적용해주는 라이브러리 도입을 제일 먼저 고려했다. UIKit 프로젝트를 할 때부터 애용했던 라이브러리라 사용법도 익숙했고 당연히 이미지 캐싱이 잘 될거라고 생각했는데, 'Kingfisher는 어떻게 이미지 캐싱을 적용하는 걸까?'는 질문은 크게 던져보지 않았던 것 같다. 그래서 이번 프로젝트에서는 이미지 캐싱을 담당하는 모듈을 구분하여 직접 만들어보기로 했다. 이미지 캐싱 방식을 서칭해보았다. 이미지 캐싱은 붙은 단어 그대로 '이미지'를 '캐싱'하는 것이고, 캐싱은 자주 쓸 것 같은 데이터를 어떤 공간에 임시적으로 저장해두고 활용하는 것을 의미한다. 그래서, 이미지 캐싱은 자주 쓸 것 같은 이미지 데이터를 특정 공간에 임시적으로 저장해두고 필요할 때마다 조회해서 사용하고, 필요 없으면 지우는 작업을 의미했다. 내가 이해한 바를 정리해 보면, 이미지 캐싱을 위해서는 이미지 데이터 저장을 위한 어떤 공간이 필요하고 임시적으로 저장하는 것이기 때문에 저장 만료 시간에 대한 반영과 계산이 필요하고 (캐싱 전략) 이미지 데이터를 저장하고 이미지 데이터를 불러오고 이미지 데이터를 삭제하는 작업이 반영되어야 한다. 저장 공간 결정
  • 한결
Swift에서의 싱글턴 패턴
싱글턴 패턴. 싱글턴은 어떤 하나의 공유되는 인스턴스를 생성해서 앱 전반적으로 활용하는 방법이다. 위 문장에서 밑줄 그어진 부분이 아무래도 중요할 것인데, 내가 이해한 싱글턴 패턴은 앱이 동작하고 있는 라이프사이클 전반에 걸쳐 하나만 생성되어 마치 값이 메모리에 올라가 있는 것처럼 활용되는 인스턴스를 의미한다. 많은 사람들이 싱글턴 패턴에서 아래의 조건을 잘 충족하는 것이 중요하다고 한다. 하나의 인스턴스만 참조하여 재사용할 것 (= 생성자로 다른 인스턴스 참조를 만들지 못해야 할 것) 앱 라이프사이클 상에서는 계속 유지되는 참조를 가져야 한다. 값이 변경되지 않은 상태(=멤버 프로퍼티)만 정의할 것 그래서 보통은 아래와 같은 형태로, 앱을 이루는 여러 코드에서 '공유(=shared)' 될 수 있는 타입 상수 멤버를 통해 자기 자신의 하나뿐인 인스턴스에 접근한다. 하나뿐인 인스턴스에 모든 뷰 객체(뷰 컨트롤러 객체)나 뷰 모델 객체들이 접근할 수 있는 것은 모두 하나의 주소값만 참조한다는 뜻이다. 그렇기 때문에 앱 전체에서 변경되지 않아야 하는 글로벌한 설정에 대한 부분을 싱글턴 패턴으로 많이 작성하고는 한다. static 상수를 통해서 메모리에 올라와 있는 하나의 객체에만 접근할 수 있게된다. (앱이 동작하는 동안 참조 지속) 다른 여러 파일에서 shared라고 하는 변수가 초기화 되는 것처럼 보여질 수 있는데, 실제로 SomeSingleton.shared 형식으로 접근을 하면, 메모리에 올라간 그 객체의 주소를 참조하게 되는 것이다. 싱글턴 패턴은 어떤 이유에서 사용될까. 다른 여러 객체에서 하나의 참조 주소만 바라볼 수 있기 때문에. 메모리 측면에서 관리가 용이하다. 1번의 연장선에서, 앱 전반에서 '굳이' 필요시마다 객체를 생성할 필요 없이, 하나의 객체로 작업을 처리시킬 수 있다. 아무래도 해당 객체가 단 한 번의 인스턴스 초기화가 되어 앱 라이프 사이클 내에서 관리되기 때문에 1번에서 말한 것처럼 메모리에서의 재사용, 누수 관리가 편할 수 있다. 더 연장해서 살펴보면, 늘 같은 주소를 참조하는 하나의 객체만 재사용하기 때문에 그 객체에 대한 접근 속도도 조금은 더 빨라질 수 있겠다. (초기화 시점을 제외하고는) 2번에 대해서, 앱에 처음 접근하거나 필요에 의해서 저장된 유저 정보를 가져와야 하는 UserManager 객체를 구성한다고 생각해보자. 이럴 때마다 해당 객체를 매번 새롭게 인스턴스 초기화하고, 내부의 자원이나 메서드를 활용하는 방식은 비효율적일 수 있다. 공유되는 하나의 객체가 있고, 유저 정보에 대한 상태를 가지는게 아니라, 그 유저정보를 불러오거나 인증을 처리하는 로직을 하는 메서드들 있다면 싱글턴이 효율적일 수 있다.
  • 한결
👏
1