iOS 이미지 포맷과 압축 방식 - 사진은 HEIC인데 스크린샷은 왜 PNG일까?

그거 아세요???


iPhone으로 사진을 찍으면 파일 확장자가 .HEIC인데, 스크린샷을 찍으면 .PNG로 저장된다는 사실을요!!
왼쪽 사진은 제가 직접 쿠알라룸푸르에서 찍은 트윈타워! 그리고 오른쪽은 그 화면을 다시 캡처한 이미지에요!
그래서 파고들기 시작했습니다. 그리고 알게 됐어요.
이게 단순히 "애플이 그냥 그렇게 만들어서"가 아니라, 각 포맷의 압축 방식과 특성이 완전히 다르기 때문이라는 걸요.
오늘은 이 이야기를 해보려고 합니다. iOS에서 이미지가 어떻게 처리되는지, 그리고 왜 상황에 따라 다른 포맷을 쓰는지 말이죠. 그리고 살짝쿵 이미지 압축 처리도 들어있어서 어려울 수 있어요:(
이미지가 화면에 보이기까지
사실 이거는 이전에 https://codeisfuture.tistory.com/96
근본으로 돌아가자(6) Image
이미지 형식과 기본 개념왜 이미지 처리가 중요할까?이미지 처리 최적화를 안 하면 어떻게 될까요? 메모리가 부족해서 앱이 강제 종료되거나, 스크롤이 버벅거리는 경험을 해보신 적 있으실 거
codeisfuture.tistory.com
여기서 정리를 했어서 간략하게만 해볼게요

코드로 UIImage(data: imageData) 한 줄 쓰면 이미지가 뜨잖아요?
1단계: Encoded Image Data
처음에 우리가 가진 건 압축된 데이터 덩어리예요. JPEG이든 HEIC이든 PNG이든, 일단 디스크에 저장될 땐 압축되어 있죠. 파일 크기를 줄이기 위해서요.
2단계: Decoded Image Buffer
그런데 이걸 화면에 그리려면? 압축을 풀어야 합니다. 디코딩을 거치면 "픽셀 하나하나의 색상 정보"가 담긴 버퍼가 만들어져요. 보통 RGBA 형식이고요.
문제는 이 단계에서 메모리를 엄청 먹는다는 거예요. 예를 들어 1000x1000 픽셀 이미지면, 1000 * 1000 * 4바이트(RGBA) = 약 4MB가 필요합니다. 원본 파일이 200KB였어도요!
3단계: FrameBuffer
마지막으로 GPU가 최종 렌더링 결과를 프레임버퍼에 쓰고, Core Animation과 Render Server가 이걸 합성해서 디스플레이로 보냅니다.
그래서 이미지 최적화할 때 "파일 크기"만 보면 안 되는 거예요. 디코딩된 버퍼 크기, 즉 실제 이미지 해상도가 메모리 사용량을 결정하거든요.
그래서 왜 사진은 HEIC고 스크린샷은 PNG야?
HEIC/HEIF - iOS의 기본 선택
HEIC(High Efficiency Image Container)는 HEIF 포맷의 일종이에요. iOS 11부터 기본 포맷이 됐죠.
왜 사진 촬영에 HEIC를 쓸까?
사진은 대부분 "자연스러운 색상 변화"가 많습니다. 하늘의 그라데이션, 피부 톤의 미묘한 변화 같은 거요.
HEIC는 이런 이미지를 압축하는 데 최적화되어 있어요.
내부적으로는 HEVC(H.265) 비디오 코덱의 인트라 프레임 압축 기술을 사용합니다. 간단히 말하면:
- 이미지를 작은 블록으로 나눔
- 각 블록의 패턴을 분석해서 예측값 생성
- 실제 값과 예측값의 차이만 저장
- DCT(이산 코사인 변환) + 양자화로 추가 압축
결과적으로 JPEG 대비 같은 품질에서 파일 크기가 약 50% 작아집니다.
그래서 수천 장의 사진을 저장하는 카메라 앱에선 HEIC가 필수인 거죠.
근데 여기엔 trade-off가 있어요.
압축이 복잡한 만큼 디코딩 시간도 더 걸리고, CPU 연산도 더 필요합니다.
그래서 실시간으로 많은 이미지를 처리해야 하는 상황에선 오히려 부담이 될 수 있어요.
PNG
그럼 스크린샷은 왜 PNG일까요?
스크린샷 특성을 생각해보세요. UI 요소들, 텍스트, 선명한 경계선...
이런 건 "명확한 색상 구분"이 많죠.
PNG는 바로 이런 이미지에 강합니다. 무손실 압축이거든요.
PNG의 압축 과정
1. 필터링: 각 픽셀 행마다 5가지 필터 중 하나를 적용합니다.

2. LZSS (LZ77 + Sliding Window): 반복되는 패턴을 찾아서 "거리와 길이"로 치환

3. Huffman Coding: 자주 나오는 값엔 짧은 비트를, 드문 값엔 긴 비트를 할당

이 과정이 "무손실"인 이유는 수학적 패턴 찾기만 하고, 실제 데이터를 버리진 않기 때문이에요.
그래서 스크린샷처럼 텍스트나 UI가 많으면 PNG가 효율적입니다.
반대로 사진 같은 자연스러운 이미지는 PNG가 오히려 비효율적이에요. 패턴을 찾기 어렵거든요.
JPEG (.jpeg(compressionQuality: 0.0 ~ 1.0))
JPEG는 우리가 가장 많이 보는 포맷이죠. 웹에서도, 카메라에서도 여전히 많이 씁니다.

JPEG은 손실, 무손실 두 방식을 모두 지원해줘요
먼저 손실부터 알아보죠

- 색공간 변환: RGB → YCbCr
- Y: 밝기(사람 눈에 민감)
- Cb, Cr: 색상(사람 눈에 덜 민감)
- 8x8 블록으로 나누기: 이미지를 작은 블록으로 쪼갬
- DCT (Discrete Cosine Transform): 각 블록을 주파수 성분으로 변환
- 저주파(부드러운 변화) → 왼쪽 위
- 고주파(날카로운 변화) → 오른쪽 아래
- 양자화 (여기서 손실 발생!):
원본 DCT 계수: [150, 23, 7, 2, 1, 0, 0, 0]
양자화 후: [150, 23, 7, 0, 0, 0, 0, 0]
작은 값들을 0으로 만들어버려요. 이게 compressionQuality 파라미터가 하는 일입니다.
5. Huffman Encoding: 0이 많아진 데이터를 추가 압축
무손실 압축은 어떻게 할까요?
DPCM을 통해 예측을 하고 엔트로피 코딩을 통해 확률적으로 많이 나오는 VLC/FLC를 구분해줍니다. 무슨소리냐고요?


이런식으로 차로 0으로 최대한 만드는게 DPCM입니다. DPCM만 하면 오히려 용량이 커지는데 이는 결국 엔트로피 코딩을 위한 수단입니다.
WebP
자 오늘 이제 어떻게 보면 메인코스에요. 구글이 "JPEG보다 좋고 PNG보다 빠른" 포맷을 만들겠다고 해서 나온게 이거에요
iOS에서도 14부터 부분적으로 지원해주고 있고요
1. 무손실 알고리즘
PNG가 행 단위로 필터링을 했다면 이 친구는 13가지 예측 모드로 예측을 합니다.
각 블록마다 13가지 시도를 다 해보고 가장 잘맞는걸 선택하니 실제값-예측값 차가 0에 더 가까워져 압축이 올라갈 수 있겠죠?
2. 색공간 변환 트릭
원본 RGB: (250, 245, 243)
G를 기준으로 변환:
- G': 245 (그대로)
- R': 250 - 245 = 5
- B': 243 - 245 = -2
자.. 가만보니 RGB는 비슷하게 흘러가네? 그러면 G로 빼버리면 0에 가까워 압축률이 올라가겠네?하고 나온 방법이에요
3. 색상 캐싱
이미지에 쓰인 색깔이 별로 없다면(예: 아이콘, 로고), 굳이 복잡한 RGB 값을 다 저장하지 않고 "1번 색, 2번 색"하는 식으로 번호표(Index)만 붙임 읽어가면서 사용했던 색깔들을 캐싱해두고 RGB를 다시 적는 대신 1번색이야~라고 기록
4. LZ77 + 산술 코딩: 여기서 Huffman 대신 산술 엔트로피 코딩을 써서 더 압축
손실 WebP (사진용)
손실 모드에선 JPEG처럼 DCT를 쓰는데, 차이점이 있어요:
- DCT 전에 블록 단위 필터링 먼저 함
- JPEG보다 세밀한 양자화 제어
- 결과: JPEG 대비 25-35% 작으면서 비슷한 품질
그래서 웹에선 이미 표준처럼 쓰이고 있어요. 로딩 속도가 곧 사용자 경험이니까요.
iOS에서 WebP는 어떻게 사용할까?
문제는 iOS가 WebP를 완벽하게 지원하진 않는다는 거예요. 14부터 지원했지만 디코딩만 지원해줘요
즉 ImageIO가 WebP 파일을 UIImage로 불러오는거는 해주는데 UIImage를 webP파일로 변환할때는...
SDWebImage 라이브러리를 씁니다:
왜 지원안해줄까를 생각해봤어요....
내가 만약 애플 경영진이라면? 🍎
WebP는 구글이 만든 포맷이에요. 애플은 자기들 포맷(HEIC/HEIF)을 밀고 있고요.
애플 입장에선:
- "웹에서 WebP 이미지를 못 보면 사파리가 욕먹으니까 디코딩은 해줘야지"
- "근데 우리 사용자들이 WebP로 저장하게 만들 필요는 없잖아?"
약간 치사빵꾸같은 느낌이긴 한대.... 뭐 미국은 분쟁의 국가니까요?😅😅
실제로는 클라이언트는 HEIC/PNG/JPEG으로 보내고 서버에서 WebP로 변환하는 방식을 많이 사용하는 거 같더라고요
- 서버 스펙이 클라이언트보다 좋아서 변환 속도 빠름
- iOS/Android 모두 같은 로직 (일관성)
그래도 우리는 학습 목적으로, 그리고 "만약 클라이언트에서 직접 WebP를 다뤄야 한다면?"을 가정하고 한번 비교해볼게요.
SDWebImage로 WebP 다루기
iOS에서 WebP 인코딩을 하려면 SDWebImage 라이브러리의 SDWebImageWebPCoder 모듈이 필요해요.
https://github.com/SDWebImage/SDWebImageWebPCoder
GitHub - SDWebImage/SDWebImageWebPCoder: A WebP coder plugin for SDWebImage, use libwebp
A WebP coder plugin for SDWebImage, use libwebp. Contribute to SDWebImage/SDWebImageWebPCoder development by creating an account on GitHub.
github.com
링크는 여깄슴다
WebP로 인코딩하기
import SDWebImageWebPCoder
let image = UIImage(named: "photo")
// WebP로 변환 (무손실)
let encoder = SDImageWebPCoder.shared
if let webpData = encoder.encodedData(
with: image,
format: .webP,
options: [.encodeWebPLossless: true] // 무손실 모드
) {
// webpData를 파일로 저장하거나 서버로 전송
}
여기서 핵심은 encodedData(with:format:options:) 메서드예요.
- .encodeWebPLossless: true → PNG처럼 무손실 압축
- .encodeWebPLossless: false + .encodeCompressionQuality: 0.8 → JPEG처럼 손실 압축
PNG나 JPEG의 익숙한 API와 비슷하게 만들어놨더라고요.


원본 데이터: 7.9MB
먼저 스크린샷 원본 파일을 앱으로 불러왔더니 7.9MB였죠?
PNG (무손실): 7.3 MB - 원본의 92%
원본 PNG 스크린샷은 "아무 압축도 안 된 상태"가 아니에요. 이미 PNG 인코딩이 된 거죠.
근데 iOS 카메라 앱이 스크린샷 찍을 때는 "빠른 저장"을 우선하기 때문에 압축 레벨을 낮게 설정해요.
우리가 image.pngData()를 호출하면 다시 압축을 하는데, 이때는 기본 압축 레벨(보통 중간 단계)을 쓰니까 조금 더 작아진 거예요.
JPEG (품질 100%): 2.7MB - 원본의 34%
WebP (무손실): 1.7MB - 원본의 22%
별도의 알고리즘 없이도 WebP를 사용하면 무손실로도 이렇게 압축할 수 있겠네요
그런데 의문점이 있습니다. 결국 Data자체가 이미지버퍼 전 단계이잖아요?
[원본 PNG 파일: 7.9MB Data]
↓
[UIImage로 로드] ← 여기서 디코딩 발생 (이미지 버퍼로 확장)
↓
[메모리상 픽셀 데이터: 약 30~40MB] (해상도에 따라)
↓
[다시 인코딩]
├→ pngData(): 7.3MB
├→ jpegData(1.0): 2.7MB
└→ webP 무손실: 1.7MB
왜 Data → UIImage(디코딩) → 다시 Data(인코딩)를 해야 하나에 대해 의문점이 생겼어요
실제로 WebP의 경우 고해상도 이미지를 인코딩하면 앱이 터져버려요 ( CPU사용률 240찍고 메모리 1GB보고 싶으면 시도해보셔도 좋아요)
실무에서는? 어떻게 할까?
1. 원본 그대로 서버에 전송
말 그대로 저 Data자체를 보내는 방법이에요:) 그리고 나서 서버가 webP로 변환을 하든 무손실이든 손실이든 압축을 하겠죠
2. ImageIO를 사용하는 방법
UIImage로 하면 픽셀로 디코딩하는거잖아요? ImageIO는 디코딩을 하지않아요
import ImageIO
func convertWithoutDecoding(data: Data, to format: String, quality: CGFloat) -> Data? {
guard let source = CGImageSourceCreateWithData(data as CFData, nil) else {
return nil
}
let destData = CFDataCreateMutable(nil, 0)
guard let destination = CGImageDestinationCreateWithData(
destData,
format as CFString, // kUTTypeJPEG, kUTTypePNG 등
1,
nil
) else { return nil }
// 핵심: 디코딩 없이 직접 복사!
CGImageDestinationAddImageFromSource(
destination,
source,
0,
[kCGImageDestinationLossyCompressionQuality: quality] as CFDictionary
)
guard CGImageDestinationFinalize(destination) else { return nil }
return destData as Data
}
// 사용
let jpegData = convertWithoutDecoding(
data: pngData,
to: kUTTypeJPEG as String,
quality: 0.8
)
// 메모리: 7.9MB → 10MB (피크) → 2.7MB
// UIImage 방식: 7.9MB → 30MB (피크) → 2.7MB
3. 다운샘플링
- 전체 이미지를 메모리에 로드하지 않음
- 필요한 크기만큼만 디코딩
- 4K 이미지도 10MB 이하 메모리로 처리 가능
결국 부하를 서버쪽에서 할꺼냐 아니면 클라이언트쪽에서 할꺼냐로 달라지겠네요???
아직 찝찝하게 글을 완성하지 못했으나 아마 제가 회사에서 이 업무를 맡아 어떻게든 결론을 지을것같아 추가로 그때마다 글을 수정할께요:)
많관부