-
회고: 개발자 2주년을 앞둔 나일기 2024. 7. 29. 18:21
# 근황
9개월 간 (개발 외에) 무얼 했는가? 되돌아 보니,,,
### 운동
건강하게 살기 위해서 운동을 시작했었습니다 ( 과거형 -> 현재는 안한다는 뜻 )
덜아프고, 바쁘고, 피곤하게 되니까 또 안가게 되었습니다 ( 1달하고 효험을 봄 )
하지만 항상 마음 한 켠에 무겁게 남아있는 운동해야 한다는 생각..
### 새 취미
미니어쳐 모델을 칠하게 되었습니다.
이런걸 만들고 칠해요! 올 해 1월부터 시작해서 반년째 즐기고 있습니다.
### 최근 개발에 대한 생각
프로젝트의 컴포트 존에 안착했다는 생각이 자주듭니다.
개발을 엄청 잘하고 모든 문제를 다 해결하고 그런 것은 아니겠으나, 그저 업무를 수행하기만 한다는 기분이 듭니다.
몰랐던 버그를 알아서 찾아서 해결하거나, 일을 주지 않아도 찾아서 하지만,,,
반복되는 개발을 하고 있는 기분이 듭니다. 뭐가 잘 못 된걸까요?
# 지난 이야기
라고했던나 잘했는가? 본인에게 질문해 봅니다. 잘,,, 했나요? (아니오)
계획을 세웠던 나, 계획은 잘 했으나, 이행은 잘하진 못했다!
### 회사에서
대 - 성 - 공
- API 변경
- 프로젝트 아키텍처 전환
실 - 패 ( "대 실 패"라고 적기에는 슬퍼서요.. )
- 모듈화와 test
### 개인적으로
반 - 성 ( 실패,,, )
- 개인 프로젝트
는 기획을 마치고 proj 까지 만들긴 했는데,,,
- 알고리즘 공부는 하다가 바빠지니까 멈춰버렸고,,,
- combine은 하다보니 조금 더 나아진 것 같아요! ㅎㅎ!
# 그럼 뭘 했나요?
변명 해 보세요 ( 출처: 호호와 거난이 만화 ) 일.... 일을 했습니다!
## 한 일(변명) 목록
- 아이돌 위젯 구독 프로젝트
- zip, Json파일을 사용하는 컨텐츠 제공 방식 -> API를 통해 server side에서 컨텐츠를 pagination하여 전달받는 방식
- 프로젝트 아키텍쳐 대부분 전환
- 회원 정보 관리 객체 리팩토링 -> TCA.Store instance -> Dependency, Actor를 이용한 방식
됐고! 그래서 그게 뭔데 설명해봐 ( 출처: 유튜버 흑백리뷰 ) ### 아이돌 구독 위젯 프로젝트
아이돌 구독 프로젝트의 기능은 다음과 같다.
"구독중인 아티스트의 미공개 사진을 위젯으로 제공한다!"
이 프로젝트는 외주 개발과 내부 개발이 병행되었다.
내부 개발은 위젯의 로직을 구현한다 ( 본인 )
외주 개발은 위젯에 쓰일 데이터, 앱 내의 UI 및 사용자 Flow를 구현한다
위젯의 로직은 기존과 유사하여 크게 어렵지 않았지만, 로그인, 구독상태 등을 확인하는 방법을 어떻게 할 지 고민이 많이 되었었다.
그런데,,, 출시 한 달전 두둥!
외주개발로 원하는 퀄리티와 일정을 맞출 수 없다는 의사결정이 내려졌고, 진행사항을 전달받아서 수정에 착수했다...
UI의 뼈대와 일부 로직을 제외하고 팀원과 모두 다 다시 만들었다. 아키텍쳐 수정, 로직 리팩토링 등...
그래서 연말에 다 퇴근하는데 나는 퇴근 못하고 일하다가 집에갔다,,,,,😭
그 과정에서 기억에 남는 문제가있었다.
"미공개 사진"이며, 저작권이 존재하기 떄문에, 앱 내에서 캡쳐를 할 수 없어야 한다는 조건이 존재했다. ( 해당 기능도 외주 개발에서 구현되지 않았다 )
당시에 팀원 분이 이 방법을 SwiftUI로 구현하였는데,
struct 보여줄View: View { var body: some View { PreventCapureView { ... some View ... } } }
PreventCapureView { ... some View ... } 와 같이 View를 Wrapping하는 방식이었다.
더보기관습적?으로? 보통 iOS의 캡쳐방지를 구현하기위해, textField를 배치하고, isSecureTextEntry = true로 설정하는 방법을 많이 사용한다.
이 방법을 이용해서,
PrevenCaptureView의 closure로 감싸인 뷰를 UIHostingViewController로 변환하고, UITextField를 배치하고 다시 UIRepresentable로 반환했다.
일단, 요구되는 기능은 구현이 되었는데 다른 문제가 발생했다.
.sheet 등의 화면전환 Modifier에서 PreventCaptureView의 기능을 사용할 때 전체 화면 Layout배치에 문제가 생겼다..
그래서 캡쳐방지 기능에 대하여 다시 생각해보았다.
이미지가 캡쳐되면 안되는 것 이었으니까, 꼭 화면을 다 가릴 필요는 없지않나? 라고 생각하여
캡쳐방지기능이 포함된 Image를 구현했다!
struct PrivateImage: View { ... } struct 보여줄화면: View { var body: some View { VStack { Text("자! 이게 사진이야!") PrivateImage(url: URL("어딘가의 주소")!) } } }
더보기AVSampleBufferDisplayLayer의 preventCapure기능을 통해 UIKit으로 구현되었고, 이를 Representable로 Wrapping하여 SwiftUI로 구현된 View에서 사용하였다.
그런데,, 이제 저 이미지는 CDN에서 받아와야했다.
평소에 KFImage의 편함에 녹아있던 나는,, 다른 방법으로 이미지를 가져와서 할당하고싶지 않았다.
extension PrivateImage: KFImageHoldingView { ... Protocol 채택에 필요한 이것저것!... } struct KFPrivateImage: View, KFImgaeProtocol { ...구현체와 이런저런 프로토콜 요구사항!... }
KingFisher를 한참동안 분석하여 캡쳐방지와 KingFisher 둘 다 잡을 수 있는 View를 구현했다...!
빠?른 시일내에 해당 구현에 대한 글을 따로 작성할 예정,,,!!
### 컨텐츠 제공 방식 변경
2년째 함께하고 있는 프로젝트는 컨텐츠 제공방식이 약간? 특이하다.
- 앱 시동 시, 로딩을 진행하며 현재 저장하고있는 컨텐츠의 버전과 서버의 컨텐츠 버전을 비교한다.
- 버전이 다른 경우, 컨텐츠 정보를 담고있는 10MB가 넘는 JSON을 압축한 .zip 파일을 다운로드한다.
- unzip하고, JSON파일을 최신화 한다.
- 저장하고 있는 컨텐츠 버전을 기록한다.
- 이제 저장된 JSON을 decode하여, 적절한 데이터로 가공하여 singleton 패턴으로 init된 static 객체의 property에 할당한다.
- 그리고 앱 전역에서 해당 객체에서 컨텐츠를 조회한다!
해당 방식으로 인해 발생한 문제는 다음과 같다.
- JSON파일이 잘 못 생성되었는지 확인이 쉽지 않음. ( 해당하는 특정 컨텐츠가 노출되지 않는지 찾아야했다)
- 앱 시동 시, 로딩이 길었다.
- 특정 컨텐츠를 수정하거나, 제거하기 어려웠다.
그래서 입사 이후 API를 통해 데이터를 받는 방식으로 바꿔야 한다고 주장해왔다...
그리고 그걸 이뤄냄!
이제는 API를 통해서 server로부터 컨텐츠 정보를 받아서 보여 줄 수 있어졌다!
프로젝트를 진행하며, 입사 당시의 나를 돌이켜 보니,, 정말 기가 차다. 얼마나 많이 바꿔야하는지 알기나 하고, 컨텐츠 제공 방식을 바꾸자고 했는지? 정말 만용이었던 것 같다 ㅋㅋ
앱의 절반도 넘는 플로우를 바꿨다. 이 프로젝트에서 꼭 하고싶었던 일을 해냈다!
### 프로젝트 아키텍쳐 전환 Explorer Proj
프로젝트의 디자인 아키텍쳐는 대부분 UIKit MVVM-C로 구성되어 있었다!
그 중 일부만 SwiftUI, TCA로 구현되어 있었는데, 초기 설계상의 실수들이 모여서 유지보수할 때 굉장이 품이 많이드는 생산성이 떨어지는 환경이었다.
또한, 프로젝트에 익숙한 나에게도 어지러운데, 새로이 적응하는 동료분들은 어떠했을까? 굉장히 혼란스러워 하셨다...
하지만 이제 그런 일은 끝! 프로젝트 대부분의 디자인 아키텍쳐를 SwiftUI, TCA로 리팩토링했다!
이로 인하여, 프로젝트 파악이 꽤 용이하게 되었으며, 실제로 빌드 스프린트 일정이 매우 잘 지켜지며, 트러블 슈팅등의 상황에서도 생산성이 매우 증대하였다.
아키텍쳐를 전환하면서 기억나는 어려운 점이 몇가지 있었다.
- TCA 버전 업
- State StackMemoryOverflow
- Tree-base Navigation의 한계
- Foreach Store의 한계 (제가 잘 못 쓴 걸지도 몰라요!)
- 애니메이션
1. TCA 버전 업
해당 프로젝트는 특수하게도,,, TCA 0.55를 포크하여, 특정 코드를 수정하여 사용하고 있다.
그래서 버전전환에 자유롭지 않은데,,, 프로젝트 전반적인 아키텍쳐를 전환하며 1.5.6으로 업데이트하였다!
전환 당시 1.7 버전이 release되었으나, 바뀐 버전에 대하여 동료 분들에게 많은 변경점을 인지 부탁드릴 수 있는 상황이 아니어서, 최대한 부담 없는 버전 업을 시도하다보니 1.5.6이 한계였다.
심지어 이 버전으로 올리는데 포크 브랜치 수정, 이를 적용하여 프로젝트 내 변경점을 혼자 수정했다! 다른 작업 병행하면서 열심히 했다!
글을 작성하는 지금은 1.12.1까지 버전이 올라간 모양이지만,,,
추후에 또 더 높은 버전으로 반영 할 수 있을 것이다! ( Deployment Target이 iOS16이 되면 정말 쉽게되겠지! )
2. State StackMemoryOverflow
State에 특정 자료형을 선언했다.
그런데 이 자료형 Model은 아래처럼 생겼다.
struct MODELA { var small: MODELB var medium: MODELB var large: MODELB } struct MODELB { ... var sizes: [MODELB] ... }
이렇게 타입 자체가 nested되거나, 너무 큰 경우 overflow가 발생할 수 있다!
그리고 이러한 큰 타입은 Action에 들어가도 문제를 일으킨다.
@reducer struct FeatureA { struct State { } enum Action { case problemAction(BigMassiveRecursiveModel) // 이런 경우도 크래쉬 발생 함 } }
나는 프로젝트 내부에 이를 대체 할 수 있는 모델이 존재하여, 전부 대체하였다. 하지만 그게 아니라면, 아래와 같이 class로 wrapping하여 사용하도록 권장한다.
물론 화면전환에 관련되었다면, PresentationState, StackState를 쓰면전혀 상관없다! ( 내부에 이미 구현되어있음 )
class EquatableBox<T:Equatable>: Equatable { var value: T init(value: T) { self.value = value } static func ==(lhs: EquatableBox<T>, rhs:EquatableBox<T>) -> Bool { lhs.value == rhs.value } } /* // TCA 내부의 아래 코드와 같은 의도이다! private class Storage: @unchecked Sendable { var state: State? init(state: State?) { self.state = state } } */
3. Tree-base Navigation의 한계
프로젝트는 주로 StackState를 사용하여 Stack-base Navigation으로 구현되어있다.
그럼에도 불구하고,,, 탭바컨트롤러의 RootView같은 경우 Tree-base Navigation방식인 Destination으로 구현되어있는데,
특정 Flow의 경우 Root로부터 최대 8단계의 depth를 가진다.
더보기Root -> TabFeature -> TabA -> TabA_Destination -> FeatureB -> Feature_StackState -> FeatureC -> FeatureD
이렇게 긴 Tree-base Navigation의 경우, Root에서 FeatureD의 Action을 전달받게 되므로 성능 하락을 불러오며 심한 경우 Crash가 발생한다.
이번에는 FeatureB가 단독으로 작동 할 수 있기에, TabA와 FeatureB의 Scope를 제거하였다!
더보기Root -> TabFeature -> TabA -> TabA_Destination
<- On-Off Biniding 만 존재 ->
FeatureB -> Feature_StackState -> FeatureC -> FeatureD
이렇게 해결!
4. View 최적화
WaterfallGrid -> Cell의 형태( 크기 및 구성요소 ), Cell의 컨텐츠 종류가 여러가지인데, 이걸 핀터레스트 처럼 보여주세요!
성공! 성능 개선 과정은 길었다. 그리고 쉽지 않았다...!
1. 처음엔 Instruments - CPU Profiler로 접근을 시작했다.
좋아요 갯수와 북마크영역을 그리면서 성능이 많이 떨어지는 양상을 보였다.
하지만 제거해도 hang은 여전히 발생했다.
2. 설계방식 변경
TCA를 사용하지않고, 순수하게
View에 State변경점을 publish하고, 이를 받아서 그리는 View였다.
그런데! data.count가 100개에서 -> 100개로 바뀔때 데이터가 바뀌는 지 확인하기 위해 일정한 비교문을 수행했다.
data가 커질수록 이 성능이 크게 문제가되어서
TCA를 사용하여 State를 관리하는것으로 변경!
3. ForEachStore
Cell마다 ForEachStore를 통해서 Cell을 만들어주었다.
이 내용을 포기하니까 성능개선이 굉장히 많이 되었다.
4. View에서 _printChanges()를 사용했다!
Cell의 크기는 고정값이 아니기에,,, ( 비율만 존재 함 )
Grid가 차지할 수 있는 크기를 읽어야 했었다. 이는 @State로 관리하였고,, Scroll하며 그리드의 크기가 계속 바뀌니까
해당 @State가 계속 변경되었고, 이는 성능 손실로 이어졌다.
그래서 이것도 수정!
기억나지 않는 여타 과정을 통해서,,, 성능 최적화를 해내었고
여러 화면에 사용하며 트러블 슈팅까지 완료하였다!
5. 애니메이션
그간 SwiftUI와 TCA를 쓰면서, 앱 내의 모든것이 Reducer.State로 관리되어야 한다고 생각했다!
그런데,, 이번 프로젝트에서 거대한 View와 여러 애니메이션을 다루다보니,,, Animation 관련 코드는 View에서 다루는 것이 좋겠다는 생각이 많이 들었다.
View에서 Action이 생성되고, State에 반영되고, 이 변경점이 다시 View에 반영되어야 하는데,,, 이 과정에서 성능 손실이 있다보니
Animation관련된 내용은 피치 못한게 아니라면 View에서 끝내는게 좋았다.
틀릴 수도 있지만, 현재로써는 이렇게 해서 안될 이유를 모르겠다!
관리상 문제 없고 동료분들도 동의했으니 된 것 아닐까!?
### 회원 정보 관리 객체 변경
기존에 회원 정보를 관리하는 객체를 TCA.Store의 instance를 사용했다.
fileprivate struct AccountStoreKey: DependencyKey { static let liveValue: StoreOf<AccountStore> = Store(initialState: .init()) { AccountStore() } } extension DependencyValues { var accountStore: StoreOf<AccountStore> { get { self[AccountStoreKey.self] } set { self[AccountStoreKey.self] = newValue } } }
몇가지 문제만 빼면 나름 편하게 잘썼다.
ViewStore로 변환해서 얻어지는 ViewStore.publisher: StorePublisher를 정말 잘 쓰고 있었다!
하지만 문제가 생겼다...
앱의 초기 비정상 종료를 유발하기 시작했다.
추정되는 원인으로는 아래와 같다.
AccountStore는 Singleton Pattern으로 구현된 Store. App Launch 이전에 생성됨. AccountStore는 class Store<State, Action>의 convenience Init에 의하여 RootStore로써 init됨. 이 때 생성되는 DemandBuffer의 init에 문제가 있다고 추정했다.
이 객체의 문제는 문제는 아래와 같다!
1. 비정상 종료 유발
2. 특정 메소드를 async-await으로 결과 값을 받을 수 없고, 다른 State를 만들어 이 State가 변경됨을 관측하여 특정 action의 완료 여부를 알 수 있다.
객체를 새로 만들어야 겠다는 생각이 들었다.
그래서 이를 대체하는 객체를 개발했다! actor를 사용하여 개발하였고, 앱 내 전체 인프라를 문제 없이 교체하는데 큰 노력을 쏟았다..
폭풍전야.... 사내 QA는 무사통과 하였지만, 정말 별 문제 없을까? 앱 초기 비정상 종료를 해결 할 수 있을까?모를일이다...화이팅!-> 24.08.02 (금) 확인 시, 버그 없고, 목표하던 Crash가 모두 해소되었다!
# 마치며..
여차저차 얼기설기 2년차를 마무리한다...
나는 잘 성장하고 있는 개발자일까..?
일은 잘 하고 있는 것 같지만, 히스토리 기반으로 일을 잘 하고 있는 것 같다는 기분이 든다.
잘 하고 있다고 생각하는 점은 팀원 PR Review를 적극적으로 잘 하고 있다는 점!
더 나은 사람이 되기를 바래본다.
### 앞으로의 계획
- Tuist 공부해보기
- SideProject 해보기
- TestCode 써보기
'일기' 카테고리의 다른 글
3.31 Apple 온라인 세션: 앱에 SwiftUI 적용하기 (0) 2025.04.01 회고: iOS 개발자 1년 3개월 (0) 2023.10.28 개발자가 되고싶었다. 부제: 개발자가 되는 과정 (2) 2023.10.14