iOS Development Guide

대형 이미지가 메모리를 많이 쓰는 이유

파일 용량(JPEG/PNG KB)과 메모리 점유(RAM MB)는 다르다. 화면 표시 시에는 압축이 풀린 픽셀 버퍼 크기가 메모리를 결정한다.

학습 날짜

2026-04-29

핵심 공식

대략적인 디코딩 메모리:

메모리 바이트 ≈ width × height × 4 (RGBA8)
메모리 MB ≈ width × height × 4 / 1024 / 1024
  • 4000 x 3000 이미지: 약 45.8MB
  • 3000 x 3000 이미지: 약 34.3MB

왜 문제가 되나

  • 스크롤 리스트에서 큰 이미지를 여러 장 동시에 들고 있으면 급격히 메모리 증가
  • 첫 렌더링 시 디코딩 비용으로 프레임 드랍 발생
  • 메모리 압박 시 앱이 백그라운드/종료될 위험 증가

파일 용량이 작아도 메모리가 큰 이유

구분의미
파일 용량디스크/네트워크에서 전송되는 압축 데이터 크기
메모리 점유렌더링을 위해 압축을 푼 픽셀 버퍼 크기
JPEG 1MB 이미지라도 해상도가 크면 메모리에서는 수십 MB가 될 수 있다.

실무 대응

  • 표시 크기에 맞춰 다운샘플링
  • 썸네일/원본 분리 저장
  • 메모리 캐시 제한 설정(NSCache cost)
  • 셀 재사용 시 이전 요청 취소 및 이미지 해제

체크리스트

  • 원본 해상도 그대로 표시하고 있지 않은가
  • 동시 표시 셀 수 대비 이미지 메모리 합이 과도하지 않은가
  • 스크롤 시 디코딩이 메인 스레드에 몰리지 않는가

Q&A 정리 (디코딩/캐시/스크롤)

질문핵심 답
첫 렌더링 시 디코딩 비용으로 프레임 드랍?압축 이미지를 픽셀 버퍼로 푸는 작업이 메인 스레드 타이밍과 겹치면 프레임 예산(약 16.7ms)을 초과해 버벅임이 생긴다.
w × h × 4로 메모리 계산?RGBA8 기준의 빠른 추정치로 유효하다. 대략 MB는 w × h × 4 / 1024 / 1024.
imageView.image = nil이면 즉시 메모리 제거?UIImageView의 강한 참조를 끊는 것이다. 다른 참조/캐시에 남아 있으면 즉시 해제되지 않는다.
캐시에 있으면 재디코딩 없음?항상은 아니다. 같은 디코딩 버퍼가 유지되면 비용이 작지만, 메모리 상황/경로에 따라 다시 디코딩될 수 있다.
UIImage면 항상 디코딩 완료?아니다. UIImage(data:)는 표현 객체 생성이고, 실제 디코딩은 첫 표시 시점까지 지연될 수 있다.
셀 재사용 시 무엇을 해야 하나?이전 요청 취소 + 이미지 참조 해제 + 새 모델 기준 재요청. 늦게 도착한 이전 이미지 덮어쓰기를 막는 것이 핵심이다.
백그라운드 디코딩 가능?가능하다. 다운로드/다운샘플/디코딩은 백그라운드, 최종 imageView.image 할당만 메인에서 처리한다.

2026-07-18 학습노트 — 이미지 디코딩·다운샘플·캐싱·네트워크 심화

파일 크기 ≠ 메모리 크기부터 다운샘플, 디코딩 시점·스레드, 2단 캐싱, 그리고 네트워크 응답·취소·캐시 헤더까지 이미지 파이프라인 전체를 정리한다.

§1. 이미지 메모리 기초: 파일 크기 ≠ 메모리 크기

Q. 디스크의 JPEG/PNG 용량과 화면에 뜰 때 메모리가 왜 다른가?

Q. 디코딩된 비트맵 메모리 계산식과 point/pixel 구분?

Q. 뷰 크기를 작게 하면 이미지 메모리가 줄어드나?

§2. Downsampling

Q. downsampling이란? scale(풀디코딩 후 축소)과 뭐가 다른가?

Q. ImageIO 다운샘플 코드와 옵션 의미는?

func downsample(imageAt url: URL, to pointSize: CGSize, scale: CGFloat) -> UIImage? {
    let sourceOpt = [kCGImageSourceShouldCache: false] as CFDictionary
    guard let src = CGImageSourceCreateWithURL(url as CFURL, sourceOpt) else { return nil }
    let maxDimension = max(pointSize.width, pointSize.height) * scale  // pt → px
    let opt = [
        kCGImageSourceCreateThumbnailFromImageAlways: true,   // 임베디드 썸네일 무시, 원본에서 생성
        kCGImageSourceShouldCacheImmediately: true,           // 디코딩을 지금(이 호출) 수행
        kCGImageSourceCreateThumbnailWithTransform: true,     // EXIF 방향 반영
        kCGImageSourceThumbnailMaxPixelSize: maxDimension
    ] as CFDictionary
    guard let cg = CGImageSourceCreateThumbnailAtIndex(src, 0, opt) else { return nil }
    return UIImage(cgImage: cg)
}

§3. 디코딩 시점과 스레드

Q. 디코딩은 언제 일어나나?

Q. 메인 vs 백그라운드 디코딩 — "둘 다 늦게 뜨는 건 같지 않나?"

Q. kCGImageSourceShouldCacheImmediately: true만 하면 백그라운드에서 디코딩되나?

DispatchQueue.global(qos: .userInitiated).async {
    let image = downsample(imageAt: url, to: size, scale: scale)  // 옵션=즉시 디코딩
    DispatchQueue.main.async { cell.imageView.image = image }     // 메인에서 set
}

Q. 백그라운드 디코딩용 공식 API는?

Q. 셀 재사용 취소는 메인 디코딩에도 필요한가?

§4. 캐싱 (메모리 + 디스크 2단)

Q. 왜 메모리 캐시와 디스크 캐시를 둘 다 두나?

저장 형태아끼는 비용수명
메모리 캐시디코딩된(+다운샘플된) UIImage디코딩 + 디스크읽기 + 네트워크휘발성(메모리 압박·앱 종료 시 사라짐)
디스크 캐시압축된 Data네트워크영속(앱 재실행해도 남음)

Q. 메모리 캐시로 NSCache가 Dictionary보다 나은 이유?

Q. 캐시 키를 URL 문자열만 쓰면 생기는 문제?

Q. UIImage는 URL을 자동 캐싱하나?

Q. URLCache란?

Q. Kingfisher는 디코딩/다운샘플/캐싱을 자동으로?

let processor = DownsamplingImageProcessor(size: CGSize(width: 200, height: 200))
imageView.kf.setImage(with: url, options: [.processor(processor), .scaleFactor(UIScreen.main.scale)])

§5. 네트워크 응답 · 취소 · 캐시 헤더 심화

Q. URLSession의 응답은 항상 Data인가?

Q. 응답 메타데이터란?

Q. HTTP 캐시 헤더와 stale(낡은 데이터) 문제?

Q. 셀 재사용 시 이미지 로딩 취소 패턴은?

final class FeedCell: UICollectionViewCell {
    private var loadTask: Task<Void, Never>?
    func configure(url: URL) {
        loadTask?.cancel()                       // 이전 로딩 취소
        loadTask = Task {
            guard let image = try? await ImageLoader.shared.load(url) else { return }
            if Task.isCancelled { return }       // 취소됐으면 결과 버림
            imageView.image = image
        }
    }
    override func prepareForReuse() {
        super.prepareForReuse()
        loadTask?.cancel(); loadTask = nil; imageView.image = nil
    }
}

Q. ImageIO에 백그라운드 디코딩/다운샘플 옵션이 있나?

Q. kCGImageSourceShouldCache=false면 "다운샘플 디코딩을 캐시하라"는 뜻?

Q. dataTask vs downloadTask — 이미지는 압축 데이터인데 downloadTask가 왜 필요?

Q. URLRequest.cachePolicy와 "1시간 캐시" 동작?

Q. 캐시 중에도 재검증 통신이 일어나나? (타이밍)

Q. CDN URL 버전/해시 예시?

쿼리 버전:        https://cdn.example.io/avatar/12345.jpg?v=3
콘텐츠 해시 파일명: https://cdn.example.com/img/a1b2c3d4e5f6.jpg
리사이즈 CDN:      https://images.example.io/photo.jpg?w=200&h=200&q=80

Q. load 함수 내부에서도 취소 체크해야 하나?

let (data, _) = try await session.data(from: url)  // 취소 시 throw
try Task.checkCancellation()                        // 디코딩 전 확인
let image = downsample(data, to: size)              // 취소됐으면 스킵

Q. identity(URL) 비교는 어떻게?

func configure(url: URL) {
    representedURL = url
    loadTask = Task {
        let image = try? await ImageLoader.shared.load(url)
        guard representedURL == url else { return }   // 재사용돼 URL 바뀌면 버림
        imageView.image = image
    }
}

Q. kCGImageSourceShouldCache=false면 스크롤 재진입 시 재디코딩되나?

Q. 취소되면 session.data가 취소되고 checkCancellation에 잡히나? (타이밍)

let (data, _) = try await session.data(from: url)  // ⓐ
try Task.checkCancellation()                        // ⓑ
let image = downsample(data, to: size)              // ⓒ 비싼 디코딩

두 체크는 다른 시간 창을 담당(같은 걸 잡는 게 아님):

취소 시점어디서 잡히나
네트워크 진행 중session.data가 URLError(.cancelled) throw → ⓑ까지 안 감
네트워크 완료 후 ~ 디코딩 전Task.checkCancellation() (ⓐ는 정상 리턴했으므로)

Q. kCGImageSourceShouldCache=true는 언제? (쓸데없이 메모리만 쓰는 것 아닌가)

// A) draw(_:)에서 같은 이미지를 반복 렌더
private lazy var cgImage = CGImageSourceCreateImageAtIndex(source, 0, nil)
override func draw(_ rect: CGRect) {
    for i in 0..<20 { ctx.draw(cgImage!, in: ...) }   // true면 디코딩 1회 재사용
}
// B) 타일 패턴 배경
view.backgroundColor = UIColor(patternImage: tileImage)

관련 문서

UIImage / UIImageView 이미지 파이프라인
pt, px, scale 개념 정리