전체 글 93

앱에 캐시를 도입하여 UX 개선하기

지금 작성하는 내용은 여행 경비 정산 앱 쓰담(SseuDam)을 개발하면서 있었던 리팩토링 과정을 정리한 내용입니다!사용자는 이 앱을 켜자마자 내가 계획한 여행을 보고, 해당 여행의 경비 사용 내용을 보고싶어 할 것 입니다.하지만 네트워크가 조금 느리거나, 앱이 켜질 때마다 스켈레톤 뷰(Skeleton View)가 나타난다면 아무리 짧은 시간이라도 사용자에게 답답함을 유발할 수 있습니다.그래서 이 부분을 개선하고자 여행 목록(TravelList)과 상세(TravelDetail) 화면에 로컬 캐시(Local Cache)를 적용하여, 네트워크 상태와 무관하게 즉각적인 반응성을 제공하게 된 과정을 공유 하려고 합니다. 1. 문제 인식앱을 개발하고 써보면서 제가 느꼈던 불편함은 크게 두 가지였습니다.Travel..

iOS

Swift에서 클로저 캡처를 값 타입으로 처리하는 방법

Swift를 쓰다 보면 클로저 내부에서 외부 변수를 사용하게 되는 경우가 많습니다. 이때 스위프트는 해당 변수를 캡처해서 클로저가 종료된 이후에도 사용할 수 있게 해주죠. 그런데 여기서 한 가지 중요한 질문이 생깁니다.“클로저가 캡처할 때, 이 값을 ‘값 타입(Value Type)’처럼 캡처하려면 어떻게 해야 할까?”1. 참조 타입을 ‘값처럼’ 클로저에 캡처하는 방법Swift에서는 캡처 리스트 내에 변수를 새로 바인딩하여 이를 해결할 수 있습니다.let closure = { [pCopy = p] in print(pCopy.name)}이 코드는 다음과 같은 의미를 가집니다.캡처 리스트에서 p를 pCopy에 할당클로저 내부에서는 pCopy만 사용이후 외부에서 p가 변경되어도 클로저 내부의 값은 변하지 ..

iOS

Swift의 @autoclosure

iOS 개발을 하다 보면 assert, precondition 같은 함수나 서드파티 라이브러리 내부에서 @autoclosure라는 키워드를 볼 수 있습니다.처음 봤을 때는 자동으로 뭘 해준다는 거지?라는 의문이 들었습니다. 그래서 오늘은 @autoclosure가 무엇인지, 왜 쓰는지, 그리고 언제 사용하면 좋은지 알아보겠습니다.1. @autoclosure란 무엇인가?이름에서 힌트를 얻을 수 있듯이 자동으로 클로저를 만들어주는 속성입니다조금 더 기술적으로 말하자면 함수의 인자로 전달된 '일반 구문(표현식)'을 컴파일러가 자동으로 () -> T 형태의 클로저로 감싸주는 기능 입니다.이게 무슨 의미일까요? 개발자가 함수를 호출할 때 중괄호 {}를 쓰지 않아도, 함수 내부적으로는 이를 지연 실행이 가능한 클로저..

iOS

Static vs Dynamic Framework: 중복 심볼 충돌 해결

1. 문제 발생앱은 정상적으로 실행되지만, 콘솔에 아래와 같은 경고 메시지가 반복적으로 출력되었습니다.objc[...] Class _TtC6Domain15AddAlarmUseCase is implemented in bothShared.framework and WakeyAlarm.debug.dylibobjc[37261]: Class _TtC8Swinject13RecursiveLock is implemented in both ...즉, 런타임에서 동일한 클래스(Domain, Swinject)가 여러 모듈에서 중복 정의되어 있었던 것입니다.✅ 1차 해결 시도첫번째 했던 시도는 '중복 호출'이 문제라고 했으니 중복 호출이 있거나 의존 관계가 잘못된 곳이 있나 살피고 정리하는 작업을 했습니다.import 구조 단..

iOS

Swinject 적용 후 프로젝트 리팩토링

오늘은 tuist + Clean Architecture + swinject를 사용한 WakeyAlarm 앱을 개발하면서 클린 아키텍처의 의존 방향을 지키면서 의존성을 적절하게 주입하는 방법을 찾기위해 리팩토링한 과정을 적어보겠습니다.1. 프로젝트 개요Clean Architecture를 염두에 두고 계층을 분리했어요.APPCoreDataDomainPresentationShared ├── DesignSystem ├── Util └── ThirdParty각 모듈은 다음과 같은 역할을 가졌습니다APP앱의 전체 관리, 시작점Core공용 타입, 유틸, 공통 상수DataRepository, Persistence, Service, EntityDomainModel, UseCase, ProtocolPresentationV..

iOS

ObservableObject와 @Observable의 차이

iOS 17부터 사용할 수 있는 새로운 데이터 바인딩 시스템인 @Observable이 등장했습니다. 기존의 ObservableObject + @Published 조합을 대체하거나 보완하기 위한 기능이죠. 왜 이미 있는 기능과 거의 똑같은 기능이 새로 나왔는지 궁금해서 두가지의 차이점을 알아보았습니다. 이번 글에서는 두 시스템의 동작 차이를 코드와 로그로 직접 비교하면서, 성능적/구조적 차이를 분석해보겠습니다.실험 설정이 코드는 CountModel의 number값을 화면에 보여줄 때 CountModel의 count값을 변경하면 UI가 갱신이 되는지 안되는지를 실험하는 코드입니다.비교를 위해 두 가지 버전의 코드를 작성했습니다.1) @Observable 버전 (Observation 프레임워크)import S..

iOS

SwiftUI의 View 업데이트 방식

SwiftUI를 공부하다 보면 자주 듣는 말이 있습니다.“SwiftUI에서는 상태가 바뀌면 View가 새로 그려진다.”이 이야기를 들었을 때 문득 이런 생각이 들었습니다. “그럼 MainView 안에 ContentView가 있을 때, ContentView의 상태가 바뀌면 전체가 다 다시 그려지는 걸까? 아니면 해당 서브뷰만 바뀌는 걸까?”이 궁금증을 해결하기위해서 직접 코드를 작성해서 뷰를 호출해보았습니다. 먼저 SwiftUI의 View는 struct입니다.struct ContentView: View { var body: some View { Text("Hello, SwiftUI!") }}SwiftUI의 View는 UIKit의 UIView처럼 화면에 붙어 있는 객체가 아니라, 그 ..

iOS

MVI란? (TCA와 비교)

최근에 TCA를 사용해서 상태 관리를 해봤더니 단방향 아키텍처에 관심이 생겨서 MVI(Model-View-Intent) 아키텍처도 사용해보고싶은 마음이 생겼습니다. 그래서 오늘은 두가지를 비교하며 MVI에 대해서 알아보도록 하겠습니다.MVI란 무엇인가MVI는 Model-View-Intent의 약자입니다. 말 그대로 “사용자 의도(Intent)”가 상태(Model)를 바꾸고, 그 상태가 다시 뷰(View)에 반영되는 구조입니다. 단방향 아키텍처이기 때문에 데이터 흐름은 항상 한 방향으로만 흐르는 것이 특징입니다.MVI의 구성 요소State (Model)화면에 필요한 모든 상태 값. 예: 로딩 여부, 데이터, 에러 메시지 등Intent사용자의 액션이나 외부 이벤트를 표현예를 들어 버튼 클릭, 데이터 새로고침..

iOS

Xcode Instruments로 UIView 메모리 누수 잡기

오늘은 Instruments를 통해서 프로젝트의 메모리 누수를 잡아본 내용을 작성해보겠습니다. 분명 Leaks는 없는데 메모리가 줄지 않는다?앱을 테스트하던 중, 화면을 스크롤할 때마다 메모리가 점점 올라가고, 한 번도 줄지 않는 걸 발견했다.특히 UICollectionView의 셀을 많이 사용하는 화면에서 현상이 두드러졌다.그래서 Xcode의 Instruments → Allocations & Leaks를 실행해 확인해봤다.Leaks 트랙에는 아무 경고도 없었지만, Allocations 그래프는 계속해서 우상향 중이었다1. All Heap & Anonymous VM이 계속 증가처음에는 명시적인 누수가 없으니 “정상인가?” 싶었지만,하단 Statistics를 보니 All Heap & Anonymous VM..

iOS

이미지 포맷 이해하기

앱을 개발하다 보면 이미지 포맷을 선택해야할 때가 있습니다. 아이콘부터 이미지까지 각 이미지의 용도에 따라 최적의 포맷이 다릅니다. 이번 글에서는 iOS에서 자주 사용하는 이미지 포맷(PNG, JPEG, HEIC, WebP)의 특징과 어떨 때 사용하면 좋은지 알아보겠습니다이미지 포맷 종류PNG 압축 방식: 무손실특징: 투명도(Alpha 채널) 지원, 색상 손실 없음장점: UI 요소(아이콘, 버튼, 로고)에 적합단점: 파일 크기가 큼PNG는 앱의 UI 그래픽에 가장 많이 사용됩니다. 투명도가 필요한 버튼, 아이콘, 탭 바 이미지 등에서 배경과 자연스럽게 어우러지기 때문입니다. 또한 무손실 압축이기 때문에, 작은 픽셀 차이도 그대로 표현됩니다.JPEG 압축 방식: 손실특징: 투명도 미지원, 색상이 풍부한 사..

iOS

RxSwift, Input / Output / transform 패턴으로 사용하기

iOS 개발을 하다 보면 RxSwift와 MVVM을 함께 사용하는 경우가 많습니다. 이때 복잡해진 viewModel의 코드를 조금 더 깔끔하게 사용하기위한 Input / Output / transform 패턴을 사용해본 경험을 작성해보겠습니다.ViewModel 안에서 로직이 복잡해지면?RxSwift를 처음 접하면 보통 이런 형태로 ViewModel을 작성합니다.searchText .debounce(.milliseconds(300), scheduler: MainScheduler.instance) .distinctUntilChanged() .flatMapLatest { query in self.useCase.execute(query: query) } .bind(to: booksRelay) ...

iOS

iOS 런 루프와 레이아웃 사이클

1. 런 루프(Run Loop)란?UIKit은 메인 스레드 위에서 동작합니다. 그 메인 스레드는 단순히 “코드가 쭉 실행되는” 선형 흐름이 아니라, 계속 반복하며 이벤트를 처리하는 큰 루프 구조로 되어 있습니다. 이것이 바로 런 루프(Run Loop) 입니다.런 루프의 사이클하나의 런 루프 사이클은 다음 순서로 동작합니다:사용자 입력(터치, 키보드 등) 감지이벤트를 해당 뷰로 전달변경된 UI 요소를 표시(setNeedsLayout, setNeedsDisplay 등으로 표시됨)다음 화면 갱신 타이밍에 실제로 layout / display / draw 수행다시 대기 상태로 진입즉, UIKit은 모든 변경을 “즉시” 반영하지 않습니다. 대신 “다음 사이클에 반영하겠다”고 예약하는 구조입니다.이게 바로 setN..

iOS

Content Hugging Priority와 Compression Resistance Priority

iOS 개발을 하다 보면 화면의 크기에 따라서 뷰의 크기가 늘어났다, 줄어들었다 해야할 일들이 있는데 설정을 정확히 해주지 않으면 생각한 것과 다르게 뷰가 늘어나고 줄어들게 됩니다. 뷰가 늘어날 때 어떤 뷰가 먼저 늘어나도록하고, 어떤 뷰가 먼저 줄어들도록 정할 수 있는 키워드가 Content Hugging Priority와 Compression Resistance Priority입니다.Content Hugging Priority뷰가 자기 콘텐츠보다 커지지 않으려는 힘남는 공간이 있을 때 다른 뷰가 대신 공간을 가져감값이 높을수록 커지기 싫어함Content Compression Resistance Priority뷰가 자기 콘텐츠보다 작아지지 않으려는 힘공간이 부족할 때 다른 뷰가 먼저 잘림값이 높을수록 ..

iOS

WWDC25: FoundationModels 튜토리얼 따라해보기 2

지난 글에 이어서 Foundation Models의 사용법을 알아보겠습니다.Dynamic promptingPrompt Builder API를 사용해서 동적으로 프롬프트를 작성하는 방법입니다. Prompt Builder API를 사용하면 프롬프트 내에 if 조건문을 사용하거나, 구조화된 답변의 예시를 전달할 수도 있습니다. 기존의 프롬프트 작성, 전달 방식let prompt = "Generate a 3-day itinerary to the Grand Canyon."let response = try await session.respond(to: prompt, generating: Itinerary.self) Prompt Builder API를 사용한 프롬프트 작성 방법let kidFriendly = true..

WWDC25: FoundationModels 튜토리얼 따라해보기

오늘은 wwdc 영상 중에서 Foundation Models을 사용한 예제를 따라해보면서 어떤 기능이 있는지, 어떻게 사용하는지 알아보겠습니다. https://developer.apple.com/videos/play/meet-with-apple/205/?cid=ht-ml-code-along Code along with the Foundation Models framework - Meet With Apple - Videos - Apple DeveloperGet hands-on experience using the Foundation Models framework to access Apple's on-device LLM. In this online session, you can code along...dev..

Swift Testing 테스트 코드 작성하기

오늘은 Swift Testing을 사용해, TCA로 작성된 코드의 비즈니스 로직을 테스트한 과정을 작성해보겠습니다. TestStore 사용법, 의존성 주입을 통한 네트워크 제어 등을 제가 작성한 코드를 보면서 설명해보겠습니다.예제는 영화 목록 화면의 로직입니다.테스트 목표 .onAppear 액션이 데이터 요청 로직을 정확히 트리거하는지세 가지 다른 API(Now Playing, Upcoming, Popular)가 각각 잘 호출되는지각 API 응답이 올바른 상태(State)를 갱신하는지1. Swift Testing 개념Swift Testing은 기존 XCTest 대비 문법이 간결합니다.@Test("테스트 설명")으로 케이스 선언#expect(조건)으로 결과가 예상과 일치하는지 확인async/await 지원..

iOS

@ObservableState와 @BindingState 충돌 트러블 슈팅

안녕하세요! 오늘은 SwiftUI와 TCA(The Composable Architecture)로 영화 검색 기능을 구현하던 중 만났던 컴파일 에러와 해결 과정을 공유해 보려고 합니다. 문제 발생영화 검색을 위한 MovieSearchFeature를 만들고 있었습니다. 검색어를 입력받을 searchText 상태를 만들고, SwiftUI View에서 양방향으로 데이터를 주고받기 위해 TCA에서 제공하는 @BindingState를 사용했습니다. @Reducerstruct MovieSearchFeature { @ObservableState struct State: Equatable { // ... @BindingState var searchText: String = "" } enum Ac..

iOS

Swift 6에 새로 등장한 InlineArray

몇주 전에 포스팅에서 Swift의 Array가 어떻게 동적으로 크기를 조절하며, 그 과정에서 발생하는 메모리 재할당과 성능 비용에 대해 알아봤었습니다. Array의 편리함 이면에는 힙(Heap) 메모리 할당과 복사라는 숨은 비용이 있다는 점을 기억하실 겁니다.그 숨겨진 비용을 줄여줄 수 있는 또다른 방법인 InlineArray에 대해 이야기해보려 합니다!InlineArray가 뭔가요?InlineArray는 이름 그대로, 데이터를 인라인(inline)으로 저장하는, 컴파일 시간에 크기가 고정된 배열입니다.가장 큰 특징은 기존 Array처럼 데이터를 힙 메모리에 별도로 할당하는 것이 아니라, InlineArray를 감싸고 있는 타입의 메모리 공간 안에 직접 값을 저장한다는 점입니다. 만약 함수 내의 지역 변..

iOS

Alamofire, Sendable 에러 트러블 슈팅

안녕하세요 오늘은 Alamofire를 함께 사용하면서 겪은 Main actor-isolated conformance... 에러를 해결해 나간 과정을 공유해 보려고 합니다. 1. 에러 발생 및 첫 시도Alamofire를 통해 데이터 통신을 시도하던 중 에러가 발생했습니다.func fetchExchangeRates(completion: @escaping (Result) -> Void) { // Alamofire로 GET 요청 AF.request(url, method: .get) .validate() // 상태 코드 200~299가 아니면 실패 처리 .responseDecodable(of: ExchangeRateResponse.self) { response in switch respon..

iOS

WWDC22: Eliminate data races using Swift Concurrency

이번 영상의 핵심 주제는 '데이터 레이스(Data Race)를 어떻게 없앨 것인가' 입니다. 이 문제를 해결하기위해 Swift Concurrency를 어떻게 활용할 수 있는지 알아봅시다.1. 기본 개념: 보트와 바다 바다: 동시성의 세계. 여러 일이 동시에 일어나는 예측 불가능한 공간입니다.보트: Task를 의미합니다. 각 보트는 자신만의 일을 처리하는 독립적인 일꾼입니다. 문제점: 보트들이 서로 소통하고 무언가를 주고받아야하는 상황이 생깁니다. 이때 주고받는 것이 안전하게 전달되는지가 중요한 문제입니다.2. 안전한 데이터 공유: 파인애플 vs. 닭 보트끼리 물건을 주고받는 상황을 생각해봅시다.파인애플 (Struct, Value Type)파인애플을 다른 보트에 주면, 그 보트는 완전히 새로운 복사본을 받..