반응형
SwiftUI Study – 이미지 로딩·캐싱·리사이징을 효율적으로 처리하는 실전 패턴
1. 왜 중요한가 (문제 배경)
SwiftUI에서 이미지 처리는 성능 문제의 핵심 요소입니다.
특히 다음과 같은 상황에서는 렌더링 지연 또는 메모리 폭증이 쉽게 발생합니다.
- 리스트에 많은 이미지가 표시되는 경우
- 고해상도(4K, 원본) 이미지를 그대로 로딩하는 경우
- AsyncImage를 기본 설정으로 사용할 때
- 이미지 리사이징을 body 안에서 수행할 때
- 네트워크 이미지 캐싱을 구현하지 않은 경우
문제의 본질은 다음과 같습니다.
이미지 로딩은 CPU·메모리·디스크·네트워크 모두에 부담이 크므로
“언제·어디서·어떻게” 처리하느냐가 성능에 큰 영향을 준다.
2. 잘못된 패턴 예시
❌ 예시 1: body 안에서 직접 리사이징
Image(uiImage: image.resize(width: 300)) // ❌ 매번 리사이징 반복
문제점
- body 호출 시마다 리사이징 연산 반복
- 연산 비용 매우 큼 (특히 큰 이미지일수록 심각)
- 스크롤 시 끊김 발생
❌ 예시 2: AsyncImage 기본 사용
AsyncImage(url: URL(string: urlString)) // ❌ 캐시 없음 / 플리커링 발생
문제점
- 네트워크 이미지를 매번 새로 요청
- 스크롤할 때 이미지 깜빡임(flickering) 발생
- 메모리 캐시 관리 불가
❌ 예시 3: 고해상도 이미지를 그대로 표시
Image(uiImage: original) // ❌ 4K·원본 이미지 그대로 로딩 → 메모리 폭증
문제점
- iPhone 화면은 실제로 이미지의 5~10%만 사용
- 원본 크기 그대로 로딩 → 메모리 낭비
❌ 예시 4: URLSession + Data → UIImage 변환을 메인스레드에서 실행
let data = try! Data(contentsOf: url) // ❌ 동기 I/O + 메인스레드 블록
let image = UIImage(data: data)! // ❌ 변환 작업도 메인스레드
문제점
- 앱 전체가 멈추는 현상 발생
- 스크롤·입력·애니메이션 지연
3. 올바른 패턴 예시
✅ 예시 1: 이미지 리사이징은 백그라운드에서 수행
ViewModel에서 수행:
@MainActor
final class PhotoViewModel: ObservableObject {
@Published var resized: UIImage?
func loadAndResize(from url: URL) {
Task.detached {
let (data, _) = try! await URLSession.shared.data(from: url)
let original = UIImage(data: data)!
let resized = original.resize(width: 300)
await MainActor.run {
self.resized = resized
}
}
}
}
View:
if let image = viewModel.resized {
Image(uiImage: image)
.resizable()
}
장점
- 리사이징 비용이 UI 스레드를 방해하지 않음
- 스크롤 및 애니메이션 성능 안정적
✅ 예시 2: NSCache 기반 이미지 캐싱
final class ImageCache {
static let shared = NSCache<NSString, UIImage>()
}
사용:
func cachedImage(for url: NSString) -> UIImage? {
ImageCache.shared.object(forKey: url)
}
func saveImage(_ image: UIImage, for url: NSString) {
ImageCache.shared.setObject(image, forKey: url)
}
장점
- 메모리 캐싱 자동 관리
- URLSession + 디스크 캐시와 함께 사용하면 이상적
✅ 예시 3: URLCache 기반 네트워크 캐싱
URLSession은 자동 캐싱 기능을 갖고 있지만, 캐시 정책이 중요함.
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
let session = URLSession(configuration: config)
장점
- 서버 응답이 캐싱 가능할 경우 네트워크 요청 없이 즉시 표시
- 스크롤 시 깜빡임(flicker) 감소
✅ 예시 4: Swift Concurrency + AsyncImage 커스텀 구현
struct CachedAsyncImage: View {
let url: URL
@State private var image: UIImage?
var body: some View {
Group {
if let image {
Image(uiImage: image)
.resizable()
} else {
ProgressView()
}
}
.task {
await load()
}
}
func load() async {
if let cached = ImageCache.shared.object(forKey: url.absoluteString as NSString) {
self.image = cached
return
}
let (data, _) = try! await URLSession.shared.data(from: url)
if let img = UIImage(data: data) {
ImageCache.shared.setObject(img, forKey: url.absoluteString as NSString)
self.image = img
}
}
}
장점
- 캐시 + 비동기 다운로드 + flickering 최소화
- 고성능 이미지 목록 구현 가능
4. 실전 적용 팁
✔ 팁 1 – 이미지 연산(리사이징)은 절대 body에서 하지 말 것
항상 ViewModel 또는 Task.detached에서 수행.
✔ 팁 2 – 리스트에 이미지를 많이 쓴다면 LazyVStack + 캐시는 필수
스크롤 성능이 체감될 정도로 개선됨.
✔ 팁 3 – URLCache와 NSCache를 함께 사용하면 최고 효율
- URLCache → 디스크/네트워크 캐싱
- NSCache → 메모리 캐싱
둘을 병행하면 대부분의 이미지 화면이 부드러워짐.
✔ 팁 4 – AsyncImage는 placeholder가 깜빡일 수 있음
커스텀 CachedAsyncImage 방식이 더 안정적.
✔ 팁 5 – 리사이징은 화면에서 필요한 크기만 로딩
원본 이미지 전체를 쓸 필요는 거의 없음.
5. 정리
- 이미지 로딩은 성능에 매우 큰 영향을 주므로 설계가 중요하다.
- 리사이징은 백그라운드에서 처리해야 하며 body 내부에서는 절대로 실행하면 안 된다.
- 메모리 캐시(NSCache) + 네트워크 캐시(URLCache)를 함께 사용하면 성능이 크게 향상된다.
- AsyncImage 대신 CachedAsyncImage 패턴을 사용하면 깜빡임 없는 안정적인 이미지 로딩을 구현할 수 있다.
반응형
'Dev Study > SwiftUI' 카테고리의 다른 글
| SwiftUI Study – List 성능과 안정성을 높이는 ForEach·id 선택 기준 (셀 깜빡임·재배치·중복 렌더링 방지) (0) | 2025.12.09 |
|---|---|
| SwiftUI Study – @EnvironmentObject 안전하게 사용하기 (크래시 방지·전역 상태 오염 방지) (0) | 2025.12.09 |
| SwiftUI Study – @StateObject와 @ObservedObject를 정확하게 선택하는 법 (뷰 재생성·초기화 문제 방지) (0) | 2025.12.09 |
| SwiftUI Study – NavigationStack을 안정적으로 설계하는 법 (중복 Push·잘못된 경로·상태 꼬임 방지) (0) | 2025.12.09 |
| SwiftUI Study – ScrollView 안에서 LazyVStack을 사용해 성능을 최적화하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – body 안에서 무거운 연산을 실행하지 않는 이유와 해결법 (0) | 2025.12.09 |
| SwiftUI Study – AnyView 남용을 피하고 타입 안정성을 유지하는 방법 (0) | 2025.12.09 |
| SwiftUI Study – View 중첩이 깊어지지 않도록 컴포넌트를 분리하는 실전 패턴 (0) | 2025.12.09 |

