iOS Development Guide

이미지 최적화 Q/A 통합 정리

이번 세션에서 나온 이미지 로딩/디코딩/캐시/측정 질문과 답변을 한 문서로 묶어, 구현·리뷰·복습 시 바로 참고할 수 있게 정리했다.

학습 날짜

기록 없음

핵심 결론

Q/A 요약 (FAQ)

Q. 셀에서 매번 API 요청하는 게 문제인가?

A. 맞다. 중복 요청 + 중복 디코드 + 재사용 경쟁이 동시에 발생한다.

Q. `fallbackImage`는 무엇인가?

A. 로드 실패 시 빈 셀을 피하고 파이프라인 동작 여부를 확인하는 디버그용 대체 이미지다.

Q. `targetPixelSize`는 어떻게 정하나?

A. 보통 `표시 크기(pt) * screenScale` 기준으로 설정한다.

Q. `CGImageSourceCreateThumbnailAtIndex` 결과는 디코딩인가?

A. 실무적으로 다운샘플된 디코드 결과로 본다.

Q. `UIImage`를 NSCache에 넣으면 디코드 보장인가?

A. 아니다. 저장 전에 미리 디코드한 이미지여야 보장된다.

Q. `kCGImageSourceShouldCache`는 언제 true를 쓰나?

A. 같은 source를 반복 재사용하고 메모리보다 CPU 절감이 우선일 때 고려한다.

Q. `kCGImageSourceShouldCacheImmediately`는 무엇을 바꾸나?

A. 캐시 허용 여부가 아니라 디코드/캐시 생성 시점을 앞당기는 옵션이다.

Q. `draw`와 `makeImage` 중 디코드 시점은?

A. `context.draw(...)` 시점에 디코드가 트리거되고 `makeImage()`는 결과 추출 단계다.

ImageIO 캐시 옵션 상세 Q/A

Q. `kCGImageSourceShouldCacheImmediately`는 이름상 캐시인데, `NSCache` 저장과 같은가?

A. 다르다. 전자는 ImageIO 내부 디코드/캐시 시점 제어이고, `NSCache`는 앱 레벨 결과 캐시다.

Q. `load`에서 저장이 안 보이는데 어디서 저장되나?

A. `load`가 아니라 `startTaskIfNeeded` 내부에서 `cache.setObject(...)`로 저장한다.

Q. `CGImageSourceCreateThumbnailAtIndex` 결과는 디코딩인가?

A. 실무적으로 다운샘플된 디코드 결과로 본다.

Q. `CGImageSourceCreateImageAtIndex`도 즉시 디코드인가?

A. 항상 그렇지 않다. 지연 디코드가 남을 수 있고, 강제 디코드는 `draw` 경로가 더 명확하다.

Q. `context.draw(...)`와 `makeImage()` 중 디코드 시점은?

A. `draw` 시점에 디코드가 트리거되고 `makeImage()`는 결과 추출에 가깝다.

Q. `NSCache`에 `UIImage`만 넣으면 디코드 보장인가?

A. 아니다. 미리 디코드된 이미지를 넣어야 디코드 결과 캐시 효과가 확실하다.

Q. `kCGImageSourceShouldCache` 디폴트는 false인가?

A. 환경에 따라 다르며 최근 64-bit 기준으로는 true 쪽으로 동작하는 경우가 일반적이다.

Q. 두 옵션이 왜 따로 있나?

A. `ShouldCache`는 캐시 허용 정책, `ShouldCacheImmediately`는 디코드/캐시 생성 타이밍 정책이다.

Q. `ShouldCache=false`, `ShouldCacheImmediately=true`는 모순인가?

A. 모순 아니다. 원본(source) 내부 캐시는 억제하고, 필요한 결과(썸네일)는 즉시 준비하도록 유도하는 조합이다.

Q. 캐시 허용 + 지연은 무슨 의미인가?

A. 캐시 자체를 금지하지 않되, 생성 호출 시점이 아니라 실제 렌더 시점까지 디코드가 밀릴 수 있다는 뜻이다.

Q. 캐시만 하고 디코드 안 할 수 있나?

A. 원본 Data 캐시는 가능하지만, 표시용 결과 캐시(픽셀)는 디코드 없이 성립하지 않는다.

Q. `kCGImageSourceShouldCache=true`는 언제 유효한가?

A. 같은 `CGImageSource`를 반복 재사용하고 메모리보다 CPU 절감이 우선일 때 고려한다.

Q. `CGImageSource` 반복 사용 예시는?

A. 줌/팬 뷰어, 다중 크기 썸네일 생성, 미리보기 슬라이더, 멀티프레임 접근 등 동일 원본 반복 참조 시나리오다.

Q. `ShouldCache=true`면 VC 벗어나도 유지되나?

A. 유지될 수도 있지만 보장되지 않는다. 내부 캐시는 시스템/메모리 상태에 따라 사라질 수 있다.

혼동이 많았던 문답 (원문형)

Q. `lock.lock()` 해두고 completion 안에서 또 lock을 잡는 이유?

A. 시작 시점 보호(중복 task 등록 방지)와 종료 시점 정리(`running[id] = nil`)는 서로 다른 시점이라 각각 lock이 필요하다.

Q. 첫 lock이 아직 유지된 상태 아닌가?

A. 아니다. `dataTask` 클로저는 등록만 되고 즉시 실행되지 않으므로 `unlock` 후 `resume`이 먼저 진행된다.

Q. `ShouldCache=false`, `ShouldCacheImmediately=true` 조합은 모순 아닌가?

A. 모순 아님. 원본 내부 캐시는 억제하면서 필요한 결과(썸네일) 디코드는 지금 수행하도록 유도하는 조합이다.

// 실무 기본 조합 (리스트/피드)
let sourceOptions = [kCGImageSourceShouldCache: false] as CFDictionary
let thumbOptions = [
  kCGImageSourceCreateThumbnailFromImageAlways: true,
  kCGImageSourceShouldCacheImmediately: true,
  kCGImageSourceCreateThumbnailWithTransform: true,
  kCGImageSourceThumbnailMaxPixelSize: maxPixel
] as CFDictionary

측정 해석 기준

URLSession과 URLCache

Q. URLSession이 이미지를 받으면 기본적으로 캐시하나?

A. 가능하다. `URLSessionConfiguration`의 `urlCache`와 `requestCachePolicy` 설정, 그리고 서버 응답 헤더(`Cache-Control`, `ETag` 등)에 따라 원본 응답(Data) 캐시가 동작한다.

Q. URLCache는 디코드된 UIImage를 저장하나?

A. 아니다. URLCache는 HTTP 응답 바이트(압축 원본 Data) 캐시다. 디코드 결과는 별도(`NSCache`)로 관리해야 한다.

Q. 캐시가 있어도 왜 다시 느릴 수 있나?

A. 네트워크 다운로드는 줄어도, 매번 디코드하면 CPU/메인 렌더 비용은 남는다. 그래서 URLCache만으로는 스크롤 성능 문제가 완전히 해결되지 않는다.

Q. 실무 기본 조합은?

A. `URLCache`로 원본 응답 재사용 + 다운샘플/선디코드 + `NSCache`로 결과 이미지 재사용 조합이 기본이다.

// 예시: URLSessionConfiguration에서 원본 응답 캐시 설정
let config = URLSessionConfiguration.default
config.requestCachePolicy = .returnCacheDataElseLoad
config.urlCache = URLCache(
  memoryCapacity: 64 * 1024 * 1024,
  diskCapacity: 256 * 1024 * 1024,
  diskPath: "image-url-cache"
)
let session = URLSession(configuration: config)

아키텍처 문답 연결

세부 내용: VIPER 계열 아키텍처 전달 정리

컬렉션뷰 Q/A (호출 순서와 취소)

Q. `prefetchItemsAt` / `cancelPrefetchingForItemsAt`는 무엇을 하나?

A. 곧 보일 셀에 대한 선행 로딩 시작과, 더 이상 필요 없는 선행 로딩 취소를 담당한다.

Q. 이 메서드들이 `cellForItemAt`보다 항상 먼저 호출되나?

A. 보통은 먼저 오지만 보장되지 않는다. 따라서 최종 로딩 보장은 `cellForItemAt`에서도 반드시 수행해야 한다.

Q. 컬렉션뷰 기본 호출 흐름은?

A. 일반적으로 `numberOfItemsInSection -> sizeForItemAt -> cellForItemAt` 순으로 표시되고, 스크롤 중에는 `prefetch -> cellFor -> didEndDisplaying -> cancelPrefetching`이 섞여 호출된다.

Q. `didEndDisplaying`에서 취소를 왜 하나?

A. 화면에서 사라진 셀의 요청/디코드를 계속 수행할 이유가 없기 때문이다. 빠른 스크롤 시 네트워크/CPU 낭비를 줄인다.

Q. `guard indexPath.item < imageIDs.count` 체크는 왜 필요한가?

A. 데이터 소스가 갱신된 직후 이탈 콜백이 들어올 수 있어 범위 오류를 막기 위한 안전장치다.

func collectionView(_ collectionView: UICollectionView,\n                    didEndDisplaying cell: UICollectionViewCell,\n                    forItemAt indexPath: IndexPath) {\n    guard indexPath.item < imageIDs.count else { return }\n    imageLoader.cancel(ids: [imageIDs[indexPath.item]])\n}

실습 프로젝트 링크

관련 문서: UIImage / UIImageView 이미지 파이프라인, 대형 이미지 메모리 사용, 메모리 누수 vs 메모리 증가