- Published on
iOS 이미지 업로드 문제를 해결하며 알아본 HEIC 처리
- Authors

- Name
- GW
개요
모바일 WebView 기반 서비스에서 이미지 업로드 기능을 개발하면서 iOS에서 촬영한 이미지를 처리할 때 예상하지 못한 문제를 경험했습니다.
JPG나 PNG 이미지는 기존 업로드 흐름에서 정상적으로 처리할 수 있었지만, iOS에서 전달되는 HEIC/HEIF 이미지는 기존 이미지 처리 과정에서 그대로 사용하기 어려운 경우가 있었습니다.
처음에는 단순한 이미지 포맷 호환성 문제처럼 보였지만, 해결 과정에서는 HEIC/HEIF 변환뿐 아니라 이미지 방향을 결정하는 EXIF Orientation과 Canvas 재인코딩까지 함께 고려해야 했습니다.
iOS App의 WebView
↓
사용자가 이미지 선택
↓
HEIC / HEIF 파일 전달
↓
웹에서 처리 가능한 이미지 포맷으로 변환
↓
EXIF 정보를 확인해 이미지 방향 보정
↓
Canvas로 이미지를 다시 생성
↓
JPEG 파일 생성
↓
서버에 업로드
여기에 이미지 변환을 위해 사용하는 라이브러리의 로딩 비용까지 고려하면 또 다른 문제가 생깁니다.
HEIC 이미지를 업로드하는 일부 사용자를 위해 모든 사용자에게 변환 모듈을 처음부터 로드할 필요가 있을까요?
따라서 이미지 처리뿐 아니라 필요한 경우에만 변환 모듈을 로드하는 방법도 함께 고민했습니다.
이번 글에서는 다음 내용을 살펴보겠습니다.
- HEIC와 HEIF는 무엇인지
- iOS WebView에서 전달받은 이미지를 웹에서 어떻게 처리할 수 있는지
- HEIC 이미지를 브라우저에서 JPEG로 변환하는 방법
- 이미지가 회전되어 표시될 수 있는 이유
- EXIF Orientation과 이미지 방향의 관계
- Canvas를 활용한 이미지 재인코딩
- Dynamic Import를 활용한 이미지 변환 모듈 로딩
- 클라이언트 이미지 처리에서 고려해야 할 트레이드오프
iOS WebView에서 이미지 업로드는 왜 문제가 되었을까?
WebView 기반 서비스에서도 웹 영역의 이미지 업로드는 <input type="file">을 통해 파일을 전달받아 처리할 수 있습니다.
이번에 문제가 발생한 지점도 WebView 내부의 웹 페이지에서 iOS 기기의 이미지를 선택한 이후였습니다.
전체 흐름을 단순화하면 다음과 같습니다.
iOS App의 WebView
↓
사용자가 이미지 선택
↓
웹 페이지에서 File 객체로 전달받음
↓
이미지 미리보기 및 필요한 처리
↓
서버에 업로드
예를 들어 웹에서는 다음과 같이 이미지 파일을 선택할 수 있습니다.
<input type="file" accept="image/*" />
그리고 선택한 파일을 File 객체로 받아 미리보기를 제공하거나 서버로 업로드할 수 있습니다.
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (!file) {
return;
}
// Preview 또는 Upload
};
여기서 한 가지 주의할 점이 있습니다.
accept="image/*"를 사용했다고 해서 선택된 모든 이미지 포맷을 이후의 이미지 처리 과정에서 문제없이 사용할 수 있다는 의미는 아닙니다.
accept는 파일 선택기에서 어떤 종류의 파일을 선택하도록 유도할지를 지정하는 힌트이며, 파일 형식 자체를 검증하는 기능은 아닙니다.
따라서 다음 세 가지는 서로 다른 문제로 봐야 합니다.
파일 선택 가능
≠
웹 환경에서 이미지 Decode / Preview 가능
≠
서비스의 Image Pipeline에서 처리 가능
JPG나 PNG는 기존 이미지 처리 흐름에서 사용할 수 있었지만, iOS WebView 환경에서 이미지 업로드를 처리하면서 HEIC/HEIF라는 다른 포맷도 함께 고려해야 했습니다.
중요한 것은 WebView 자체가 문제라기보다 WebView 기반 서비스에서 전달받은 HEIC/HEIF 이미지가 기존 이미지 처리 흐름과 맞지 않았다는 점입니다.
따라서 이미지 업로드에서는 파일을 선택할 수 있는지만 보는 것이 아니라, 선택 이후의 미리보기, 변환, 업로드 과정까지 해당 포맷을 처리할 수 있는지 함께 고려해야 합니다.
HEIF와 HEIC는 무엇일까?
HEIC와 HEIF는 비슷한 의미로 사용되는 경우가 많지만 정확히 같은 개념은 아닙니다.
HEIF는 High Efficiency Image File Format의 약자로 이미지와 관련 데이터를 저장할 수 있는 컨테이너 포맷입니다.
Apple에서도 HEIF를 사진을 위한 고효율 미디어 포맷으로 사용하고 있으며, iPhone과 iPad에서는 설정에 따라 사진을 HEIF 형식으로 촬영할 수 있습니다.
*Apple - Apple 기기에서 HEIF 또는 HEVC 미디어 사용하기
HEIC는 HEIF 기반 이미지에서 흔히 볼 수 있는 파일 확장자입니다.
개념적으로 보면 다음과 같습니다.
HEIF
→ 이미지와 관련 데이터를 담을 수 있는 컨테이너 포맷
HEIC
→ HEIF 기반 이미지에서 흔히 사용하는 파일 확장자
HEIF는 단순히 하나의 이미지만 저장하는 구조가 아니라 여러 이미지와 관련 데이터를 하나의 컨테이너 안에 저장할 수도 있습니다.
Apple은 HEIF가 JPEG보다 높은 압축 효율을 제공하여 비슷한 시각적 품질을 유지하면서 저장 공간을 줄일 수 있다고 설명합니다.
웹 서비스 입장에서는 여기서 중요한 문제가 하나 생깁니다.
기존 이미지 처리 과정이 JPG나 PNG를 중심으로 구성되어 있다면 HEIC가 들어왔을 때 동일한 방식으로 처리할 수 있다고 가정해서는 안 됩니다.
JPG / PNG 중심의 Image Pipeline
↓
HEIC / HEIF 이미지 입력
↓
Preview / Decode / Processing 과정에서 문제 발생 가능
따라서 서비스 내부에서 최종적으로 사용하는 이미지 포맷을 통일하고 싶다면 HEIC/HEIF를 기존 이미지 파이프라인에서 처리할 수 있는 포맷으로 변환하는 방법을 고려할 수 있습니다.
확장자와 MIME Type만 확인하면 충분할까?
HEIC 파일인지 확인하는 가장 단순한 방법은 파일 이름의 확장자를 확인하는 것입니다.
const isHeic = file.name.toLowerCase().endsWith('.heic');
하지만 파일 이름과 실제 파일 데이터가 항상 일치한다고 보장할 수는 없습니다.
브라우저의 File 객체에서는 MIME Type도 확인할 수 있습니다.
console.log(file.type);
환경과 파일에 따라 다음과 같은 MIME Type을 다룰 수 있습니다.
image/jpeg
image/png
image/heic
image/heif
따라서 애플리케이션에서 HEIC/HEIF 변환 여부를 판단하기 위한 1차 분류로 확장자와 MIME Type을 함께 고려할 수 있습니다.
const isHeicFile = (file: File) => {
const extension = file.name.split('.').pop()?.toLowerCase();
return (
file.type === 'image/heic' ||
file.type === 'image/heif' ||
extension === 'heic' ||
extension === 'heif'
);
};
하지만 이것 역시 실제 파일 내용 자체를 검증하는 방법은 아닙니다.
File Name
↓
실제 파일 형식을 보장하지 않음
MIME Type
↓
실제 파일 내용을 검증하는 수단이 아님
File Content
↓
별도의 검증 필요
MDN에서도 accept를 파일 검증 수단으로 사용해서는 안 되며, 서버에서 적절한 검증을 수행해야 한다고 설명합니다.
따라서 클라이언트의 확장자와 MIME Type 확인은 어떤 이미지 처리 흐름을 사용할지 결정하기 위한 분기 정도로 사용하는 것이 적절합니다.
최종적으로 파일을 저장하거나 처리하는 서버에서는 별도의 파일 검증이 필요합니다.
HEIC를 어떤 시점에 변환할까?
HEIC를 기존 이미지 처리 흐름에서 직접 사용하기 어렵다면 크게 두 가지 방향을 생각할 수 있습니다.
첫 번째는 원본 파일을 서버로 업로드한 뒤 서버에서 변환하는 방법입니다.
iOS WebView에서 HEIC 이미지 선택
↓
원본 HEIC 파일 업로드
↓
서버에서 이미지 변환
↓
JPEG 등 서비스에서 사용하는 포맷으로 저장
두 번째는 WebView의 웹 영역에서 먼저 JPEG로 변환한 뒤 기존 업로드 흐름으로 전달하는 방법입니다.
iOS WebView에서 HEIC / HEIF 이미지 선택
↓
웹 영역에서 JPEG로 변환
↓
JPEG 파일 생성
↓
기존 이미지 업로드 과정으로 전달
↓
서버에 업로드
서버에서 변환하면 이미지 처리 환경을 서버에서 통제할 수 있다는 장점이 있습니다.
반면 클라이언트에서 먼저 변환하면 기존 서버의 JPG/JPEG 업로드 흐름을 크게 변경하지 않고, 입력 단계에서 이미지를 기존 파이프라인에서 사용할 수 있는 형태로 정규화할 수 있습니다.
이번에는 후자의 방식을 사용했습니다.
즉, WebView에서 전달받은 HEIC/HEIF 이미지를 웹 영역에서 먼저 JPEG로 변환한 뒤 기존 이미지 업로드 흐름으로 전달했습니다.
이미지 입력
↓
파일 포맷 확인
↓
├─ JPG / PNG → 기존 파일 그대로 사용
└─ HEIC / HEIF → JPEG로 변환
↓
기존 이미지 업로드 과정으로 전달
이를 위해 heic2any를 활용했습니다.
heic2any로 브라우저에서 이미지 변환하기
heic2any는 브라우저 환경에서 HEIC/HEIF 이미지를 JPEG, PNG, GIF 등의 포맷으로 변환하기 위해 만들어진 라이브러리입니다.
기본적인 사용 방식은 다음과 같습니다.
import heic2any from 'heic2any';
const convertHeicToJpeg = async (file: File) => {
const result = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: 0.9,
});
return result;
};
변환 과정을 단순화하면 다음과 같이 볼 수 있습니다.
HEIC / HEIF 파일
↓
heic2any로 변환
↓
HEIC / HEIF 이미지 디코딩
↓
JPEG 형식으로 다시 인코딩
↓
JPEG Blob 생성
변환 결과가 Blob이고 기존 업로드 로직에서 File을 사용하고 있다면 새로운 File 객체로 만들어 전달할 수도 있습니다.
const convertHeicToJpeg = async (file: File) => {
const result = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: 0.9,
});
const blob = Array.isArray(result) ? result[0] : result;
const fileName = file.name.replace(/\.(heic|heif)$/i, '.jpg');
return new File([blob], fileName, {
type: 'image/jpeg',
lastModified: Date.now(),
});
};
이렇게 하면 이후의 업로드 로직에서는 HEIC/HEIF 처리 방법을 각각 알 필요 없이 기존에 사용하던 이미지 처리 흐름을 활용할 수 있습니다.
즉, HEIC 처리 로직을 서비스 곳곳에 추가하는 대신 이미지 입력 단계에서 포맷을 정규화하고 이후의 이미지 처리 흐름을 일관되게 유지할 수 있습니다.
하지만 HEIC를 JPEG로 변환하는 것만으로 모든 문제가 끝난 것은 아니었습니다.
이미지 변환 과정에서 사진의 방향이 의도와 다르게 표시되는 문제도 함께 고려해야 했습니다.
JPEG로 변환했는데 왜 이미지가 돌아갈까?
스마트폰으로 촬영한 이미지가 사용자가 보고 있는 방향 그대로 픽셀 데이터에 저장되어 있다고 생각하기 쉽습니다.
하지만 이미지에는 촬영 방향을 나타내는 메타데이터가 포함될 수 있습니다.
대표적인 것이 EXIF Orientation입니다.
개념적으로 다음과 같은 상황이 가능합니다.
이미지의 실제 픽셀 데이터
→ 촬영된 이미지 자체의 픽셀 정보
EXIF Orientation
→ 이미지를 어떤 방향으로 표시할지 알려주는 정보
이미지를 표시하는 환경에서 Orientation 정보를 적절하게 반영하면 사용자는 올바른 방향의 이미지를 볼 수 있습니다.
이미지의 실제 픽셀 데이터 + EXIF Orientation의 방향 정보
↓
표시할 때 이미지 방향 보정
↓
사용자에게 올바른 방향으로 표시
문제는 이미지를 다른 포맷으로 변환하거나 다시 인코딩하는 과정입니다.
원본 이미지에 존재하던 메타데이터가 결과 이미지에도 그대로 유지된다고 가정할 수 없기 때문입니다.
특히 heic2any는 공식 README에서 원본 HEIC 파일의 메타데이터를 결과 JPEG, PNG 등에 복사하는 데 초점을 두고 있지 않으며, 결과 파일에는 원본 메타데이터가 포함되지 않는다는 제한 사항을 명시하고 있습니다.
*heic2any - Usage and limitations
따라서 이미지 처리 과정에서는 원본 픽셀 데이터의 방향과 Orientation 메타데이터, 변환 이후의 결과를 함께 확인할 필요가 있습니다.
원본 이미지 (픽셀 데이터 + EXIF 방향 정보)
↓
이미지 변환
↓
방향 정보가 어떻게 반영되는지 확인
↓
변환된 이미지의 최종 방향 확인
다만 여기서 한 가지 주의해야 할 점이 있습니다.
이미지가 회전되어 보인다고 해서 단순히 브라우저가 EXIF Orientation을 지원하지 않기 때문이라고 볼 수는 없습니다.
EXIF Orientation은 이미지 파일에 저장되는 사진의 방향 정보입니다.
예를 들어 실제 픽셀 데이터가 회전된 상태로 저장되어 있더라도, EXIF Orientation을 통해 화면에는 올바른 방향으로 표시할 수 있습니다.
현대 브라우저는 이러한 방향 정보를 반영할 수 있으며, CSS의 image-orientation 속성도 기본적으로 이미지 자체의 방향 정보를 사용하는 from-image 값을 사용합니다.
따라서 이미지 방향 문제를 확인할 때는
브라우저가 EXIF Orientation을 지원하지 않음
↓
그래서 이미지 방향이 잘못됨 ❌
으로만 생각할 것이 아니라,
이미지 디코딩
↓
EXIF Orientation 확인
↓
방향 정보에 따라 이미지 보정
↓
이미지를 다시 인코딩
↓
EXIF 정보의 유지 / 제거 여부 확인
↓
최종 이미지가 올바른 방향으로 표시되는지 확인
처럼 이미지를 변환하고 다시 인코딩하는 과정에서 방향 정보가 어떻게 처리되었는지를 함께 확인해야 합니다.
EXIF Orientation은 무엇일까?
EXIF는 이미지에 촬영과 관련된 여러 정보를 저장할 수 있는 메타데이터 형식입니다.
Orientation 역시 그중 하나로, 이미지의 픽셀 데이터를 어떤 방향으로 표시해야 하는지를 나타낼 수 있습니다.
쉽게 말해서 EXIF Orientation은 이미지 파일 안에 저장된 "사진을 어떤 방향으로 보여줘야 하는지"에 대한 메타데이터 입니다.
단순하게 보면 다음과 같습니다.
이미지의 실제 픽셀 데이터
→ 이미지 파일에 실제로 저장된 방향
EXIF Orientation
→ 화면에 표시할 때 적용해야 하는 방향 정보
따라서 화면에서 사용자가 보는 이미지와 실제 Pixel Data의 방향이 항상 동일하다고 가정해서는 안 됩니다.

위 그림은 카메라의 촬영 방향과 이미지에 저장된 실제 픽셀 방향, EXIF Orientation을 적용한 최종 결과의 관계를 보여줍니다.
두 번째 줄의 파란색 F는 파일에 저장된 실제 픽셀 방향입니다.
카메라를 돌려 촬영하면 픽셀도 옆으로 눕거나 거꾸로 된 상태로 저장될 수 있으며, EXIF Orientation에는 화면에 표시할 때 어느 방향으로 보정해야 하는지가 기록됩니다.
| EXIF Orientation | 필요한 방향 보정 |
|---|---|
1 | 회전하지 않음 |
6 | 시계 방향으로 90도 회전 |
3 | 180도 회전 |
8 | 시계 방향으로 270도 또는 반시계 방향으로 90도 회전 |
EXIF Orientation에는 실제로 1부터 8까지의 값이 있습니다.
위 그림에서는 일반적인 회전 방향을 설명하기 위해 1, 3, 6, 8만 보여줍니다.
나머지 2, 4, 5, 7은 좌우 또는 상하 반전이 포함된 경우입니다.
1, 3, 6, 8
→ 회전 중심
2, 4, 5, 7
→ 반전과 회전의 조합
브라우저나 이미지 처리 프로그램이 Orientation 값을 적용하면 회전되거나 반전된 픽셀 데이터가 올바른 방향으로 보정됩니다.
그림 아래쪽의 모든 F가 똑바로 보이는 것도 픽셀이 처음부터 똑바로 저장되어 있기 때문이 아니라 EXIF Orientation을 기준으로 표시 방향을 보정했기 때문입니다.
예를 들어 사용자가 세로로 촬영한 사진이 있다고 해보겠습니다.
세로로 촬영한 사진
↓
픽셀 데이터는 회전된 상태로 저장될 수 있음
↓
EXIF Orientation에 방향 정보를 저장
↓
표시할 때 방향 정보를 반영해서 올바른 세로 사진으로 표시됨
단순히 이미지를 표시하는 경우에는 브라우저의 이미지 처리 과정에서 Orientation 정보가 반영될 수 있습니다.
하지만 이미지 자체를 새로운 파일로 만들어 저장해야 한다면 다른 접근을 고려할 수 있습니다.
최종 결과물이 원본 Orientation 메타데이터에 의존하지 않도록 원하는 방향으로 픽셀을 다시 그린 뒤 새로운 이미지로 인코딩하는 방법입니다.
이때 활용할 수 있는 것이 Canvas입니다.
Canvas를 이용해 이미지 자체를 다시 그리기
Canvas는 JavaScript에서 픽셀 기반 그래픽을 다룰 수 있는 Web API입니다.
이미지를 Canvas에 그릴 때는 단순히 원본 이미지를 복사하는 것뿐 아니라 이동이나 회전 등의 Transform을 적용할 수도 있습니다.
원본 이미지
↓
Canvas에 이미지 준비
↓
필요한 방향으로 이동 / 회전
↓
drawImage()로 다시 그리기
↓
회전이 실제 픽셀에 반영된 이미지 생성
예를 들어 90도 회전이 필요한 이미지라면 개념적으로 다음과 같이 처리할 수 있습니다.
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
if (!context) {
throw new Error('Canvas context를 생성할 수 없습니다.');
}
canvas.width = image.height;
canvas.height = image.width;
context.translate(canvas.width, 0);
context.rotate(Math.PI / 2);
context.drawImage(image, 0, 0);
여기서 중요한 것은 CSS로 화면에 보이는 이미지의 방향만 변경하는 것과 Canvas를 이용해 픽셀 자체를 다시 그리는 것은 다르다는 점입니다.
CSS Transform
→ 화면에서 보이는 방향만 변경
→ 원본 이미지의 픽셀 데이터는 그대로
Canvas Transform
→ 이미지를 회전한 뒤 다시 그림
→ 회전이 반영된 새로운 픽셀 데이터 생성
이후 Canvas에 그려진 결과를 새로운 이미지 데이터로 만들 수 있습니다.
브라우저에서 제공하는 HTMLCanvasElement.toBlob()을 사용하면 Canvas에 그려진 현재 결과를 JPEG나 PNG 등의 새로운 이미지 Blob으로 생성할 수 있습니다.
*MDN - HTMLCanvasElement.toBlob()
예를 들어 다음과 같이 사용할 수 있습니다.
const canvasToBlob = (canvas: HTMLCanvasElement) => {
return new Promise<Blob>((resolve, reject) => {
canvas.toBlob(
(blob) => {
if (!blob) {
reject(new Error('이미지 변환에 실패했습니다.'));
return;
}
resolve(blob);
},
'image/jpeg',
0.9
);
});
};
이렇게 하면 최종 이미지를 표시할 때 원본 Orientation 메타데이터에 의존하는 대신 원하는 방향이 반영된 픽셀 결과를 새로운 이미지로 재인코딩할 수 있습니다.
하지만 이 방식 역시 비용이 발생합니다.
이미지 디코딩 (이미지 데이터를 브라우저가 처리할 수 있도록 해석)
↓
Canvas에 방향을 보정해 다시 그리기 (필요한 방향으로 회전하여 픽셀에 반영)
↓
새로운 JPEG 이미지로 인코딩(결과를 새로운 JPEG 이미지로 생성)
이 모든 과정이 클라이언트에서 수행되기 때문입니다.
특히 WebView 기반 모바일 서비스에서는 이미지 변환 작업이 사용자의 모바일 기기에서 수행되므로 고해상도 이미지 처리에 따른 CPU와 메모리 비용도 함께 고려해야 합니다.
스마트폰으로 촬영한 이미지는 해상도가 높을 수 있기 때문에 이미지 파일 자체의 크기뿐 아니라 디코딩 이후의 픽셀 데이터와 Canvas가 사용하는 메모리도 고려할 필요가 있습니다.
따라서 클라이언트에서 이미지 포맷을 변환하는 구조를 선택했다면 호환성뿐 아니라 이미지 변환에 필요한 연산 비용과 사용자 기기의 자원까지 함께 고려해야 합니다.
그리고 여기서 또 하나의 성능 문제가 생깁니다.
HEIC를 변환하기 위해 추가한 라이브러리는 HEIC를 업로드하지 않는 사용자에게도 필요한 코드일까요?
다음으로는 이 문제를 Dynamic Import와 Code Splitting을 이용해 어떻게 개선할 수 있는지 살펴보겠습니다.
모든 사용자에게 HEIC 변환 코드가 필요할까?
HEIC 이미지 변환 기능을 추가하면서 이미지 호환성 문제는 해결할 수 있었습니다.
하지만 다른 관점에서 보면 새로운 비용이 생긴 것입니다.
import heic2any from 'heic2any';
위와 같이 모듈을 정적으로 Import하면 heic2any는 해당 모듈의 의존성 그래프에 포함됩니다.
그런데 실제 이미지 업로드를 생각해보면 모든 사용자가 HEIC/HEIF 파일을 선택하는 것은 아닙니다.
JPG, PNG
↓
heic2any 불필요
HEIC / HEIF
↓
heic2any 필요
즉, HEIC 변환은 특정 조건에서만 필요한 기능입니다.
그렇다면 이미지 업로드 기능을 사용하는 모든 사용자에게 처음부터 변환 코드를 제공하기보다, 실제 HEIC/HEIF 파일이 들어왔을 때 필요한 모듈을 로드하는 방법을 생각해볼 수 있습니다.
이때 사용할 수 있는 것이 Dynamic Import입니다.
Static Import와 Dynamic Import는 무엇이 다를까?
일반적으로 사용하는 import는 Static Import입니다.
import heic2any from 'heic2any';
Static Import는 최상위 레벨에서 선언되며 모듈의 의존 관계를 정적으로 분석할 수 있습니다.
반면 JavaScript는 import()를 이용한 Dynamic Import도 제공합니다.
const { default: heic2any } = await import('heic2any');
import()는 Promise를 반환하며 필요한 시점에 모듈을 비동기적으로 불러올 수 있습니다.
가장 큰 차이는 조건에 따라 모듈을 로드할 수 있다는 것입니다.
예를 들어 다음과 같이 작성할 수 있습니다.
const processImage = async (file: File) => {
if (!isHeicFile(file)) {
return file;
}
const { default: heic2any } = await import('heic2any');
const result = await heic2any({
blob: file,
toType: 'image/jpeg',
quality: 0.9,
});
const blob = Array.isArray(result) ? result[0] : result;
return new File([blob], file.name.replace(/\.(heic|heif)$/i, '.jpg'), {
type: 'image/jpeg',
lastModified: Date.now(),
});
};
이제 이미지 처리 흐름은 두 가지로 나뉩니다.
Image 선택
↓
HEIC / HEIF인가?
↓
No
↓
기존 File 사용
Image 선택
↓
HEIC / HEIF인가?
↓
Yes
↓
heic2any Dynamic Import
↓
JPEG 변환
↓
변환된 File 사용
JPG나 PNG를 선택한 경우에는 import('heic2any') 자체가 실행되지 않습니다.
따라서 실제로 HEIC 변환 기능이 필요한 시점까지 모듈 로딩을 미룰 수 있습니다.
Dynamic Import와 Code Splitting은 어떤 관계일까?
여기서 Dynamic Import를 사용했다고 해서 JavaScript 코드 자체가 사라지는 것은 아닙니다.
이 부분을 구분하는 것이 중요합니다.
웹 애플리케이션에서 webpack과 같은 Bundler는 여러 JavaScript Module의 의존 관계를 분석하여 배포할 파일을 생성합니다.
정적인 의존 관계가 다음과 같다고 해보겠습니다.
ImageUploader
↓
heic2any를 처음부터 Import
↓
heic2any 관련 코드도 번들에 포함
↓
페이지를 불러올 때 함께 로드될 수 있음
heic2any를 Static Import하면 ImageUploader의 정적인 Module Graph에 해당 의존성이 포함됩니다.
import heic2any from 'heic2any';
반면
await import('heic2any');
처럼 Dynamic Import를 사용하면 Bundler가 이 지점을 별도의 Chunk로 분리할 수 있는 경계로 활용할 수 있습니다.
(Chunk는 Bundler가 코드를 나누어 생성한 JavaScript 코드 묶음을 의미합니다.)
webpack 역시 공식 문서에서 import()를 Code Splitting을 위한 방법으로 지원합니다.
개념적으로 보면 다음과 같습니다.
Static Import
→ 이미지 업로드 코드 + HEIC 변환 코드를 처음부터 함께 로드
Dynamic Import를 사용하면 Bundler 구성에 따라 다음과 같이 분리할 수 있습니다.
Dynamic Import
→ 이미지 업로드 코드는 처음부터 로드
→ HEIC 변환 코드는 변환이 필요한 시점에 별도의 Chunk로 로드
이렇게 하면 HEIC 변환에 필요한 코드를 초기 로딩에서 제외하고, 실제 HEIC 이미지가 들어왔을 때만 별도로 불러올 수 있습니다.
Dynamic Import를 하면 번들 크기가 줄어드는 걸까?
여기서 표현을 정확하게 할 필요가 있습니다.
흔히 Dynamic Import를 사용하면
Bundle Size가 줄어든다.
라고 표현합니다.
사용자가 처음 내려받아야 하는 JavaScript의 관점에서는 맞을 수 있지만, 조금 더 정확하게는 초기 로딩에 필요한 JavaScript의 크기를 줄일 수 있다고 표현하는 것이 좋습니다.
예를 들어 전체 애플리케이션 코드가 개념적으로 다음과 같다고 해보겠습니다.
전체 애플리케이션 코드
├─ 기본 애플리케이션 코드: 100
└─ HEIC 변환 코드: 30
전체: 130
Static Import에서는 초기 로딩 경로가 개념적으로 다음과 같이 구성될 수 있습니다.
Static Import
초기 로딩
↓
기본 코드 100 + HEIC 변환 코드 30
↓
총 130 로드
Dynamic Import를 통해 분리하면 다음과 같은 형태가 될 수 있습니다.
Dynamic Import
초기 로딩
↓
기본 코드 100만 로드
HEIC 변환이 필요한 시점
↓
HEIC 변환 코드 30 추가 로드
전체 빌드 산출물에서 HEIC 변환 코드 자체가 사라진 것은 아닙니다.
Dynamic Import를 사용해도 전체 빌드 결과에는 기본 코드 100과 HEIC 변환 코드 30이 모두 존재합니다.
HEIC 변환 코드가 사라진 것이 아니라, 처음부터 로드하지 않고 필요한 시점에 다운로드하고 실행할 것인지가 달라진 것입니다.
그렇다면 모든 코드를 Dynamic Import하면 더 좋을까?
그렇지는 않습니다.
Dynamic Import에도 비용이 있습니다.
모듈이 필요한 순간 아직 로드되지 않았다면 추가적인 네트워크 요청과 모듈 로딩을 기다려야 합니다.
사용자가 기능 실행
↓
Dynamic Import 실행
↓
필요한 Chunk 다운로드
↓
모듈 로딩 및 실행 준비
↓
기능 실행
따라서 자주 사용되고 초기 화면에 반드시 필요한 기능까지 무조건 Dynamic Import하는 것이 항상 좋은 선택은 아닙니다.
MDN 역시 초기 의존성을 불러올 때는 Static Import가 더 적합할 수 있으며, Dynamic Import는 코드가 필요할 가능성이 낮거나 나중에 필요한 경우 등에 사용할 수 있다고 설명합니다.
결국 중요한 것은 해당 기능이 언제, 얼마나 자주 필요한가입니다.
클라이언트에서 이미지를 변환하는 것은 항상 좋은 선택일까?
여기까지 보면 브라우저에서 HEIC를 JPEG로 변환하는 방식이 상당히 편리해 보입니다.
기존 서버의 이미지 업로드 흐름을 크게 변경하지 않고 클라이언트에서 입력 이미지를 정규화할 수 있기 때문입니다.
하지만 클라이언트 변환에는 분명한 비용도 있습니다.
첫 번째는 연산 비용입니다.
HEIC를 JPEG로 변환하려면 이미지를 디코딩하고 다시 인코딩해야 합니다.
Canvas까지 사용한다면 추가적인 이미지 처리도 필요합니다.
HEIC 이미지 디코딩
↓
필요한 이미지 처리
↓
Canvas에 다시 그리기
↓
JPEG 이미지로 인코딩
이 작업은 사용자의 기기에서 수행됩니다.
따라서 고해상도 이미지나 성능이 낮은 모바일 기기에서는 처리 시간이 길어질 수 있습니다.
두 번째는 메모리 사용량입니다.
압축된 이미지 파일의 크기가 작다고 해서 디코딩된 이미지가 메모리에서도 작은 것은 아닙니다.
예를 들어 RGBA 형태의 비압축 픽셀 데이터를 단순하게 생각하면 한 픽셀을 표현하는 데 여러 바이트가 필요합니다.
따라서 고해상도 이미지를 디코딩하고 Canvas에 다시 그리는 과정에서는 원본 파일 크기보다 훨씬 많은 메모리가 필요할 수 있습니다.
압축된 이미지 파일
↓
이미지 디코딩
↓
메모리에 비압축 픽셀 데이터 생성
↓
Canvas에서 이미지 처리
↓
JPEG로 다시 인코딩
세 번째는 이미지 품질과 메타데이터입니다.
JPEG로 변환하면서 품질 설정에 따라 손실 압축이 발생할 수 있고, 변환 라이브러리나 처리 방식에 따라 원본 이미지의 메타데이터가 유지되지 않을 수도 있습니다.
따라서 클라이언트 이미지 변환은
장점
├─ HEIC / HEIF 호환성 문제 해결
└─ 기존 이미지 업로드 과정 재사용
고려할 비용
├─ 기기 연산 비용
├─ 메모리 사용량
├─ 이미지 변환 시간
├─ 이미지 품질 손실 가능성
└─ 메타데이터 유실 가능성
라는 요소를 종합적으로 고려한 Trade-off 선택이라고 볼 수 있습니다.
서버에서 변환하는 방법과는 무엇이 다를까?
다른 방법은 원본 HEIC 파일을 서버로 보내고 서버에서 변환하는 것입니다.
클라이언트에서 HEIC 원본 선택
↓
원본 HEIC 파일을 서버에 업로드
↓
서버에서 HEIC 이미지 디코딩
↓
JPEG로 변환
↓
변환된 이미지 저장
이 방식에서는 클라이언트 기기의 CPU와 메모리를 이미지 변환에 사용하지 않아도 됩니다.
또 서버에서 사용하는 이미지 처리 환경과 라이브러리를 통제하기도 상대적으로 쉽습니다.
반면 원본 HEIC를 서버까지 전송해야 하고, 서버에 별도의 이미지 변환 파이프라인을 구축하고 운영해야 합니다.
어느 방식이 항상 더 좋다고 볼 수는 없습니다.
이미지 크기, 업로드 빈도, 지원해야 하는 기기, 서버 인프라, 이미지 품질 정책 등에 따라 선택이 달라질 수 있습니다.
현재 시점에서도 heic2any를 그대로 선택할까?
여기까지는 실제로 문제를 해결할 당시의 선택을 기준으로 살펴봤습니다.
하지만 라이브러리와 이미지 포맷의 호환성은 시간이 지나면서 달라질 수 있습니다.
따라서 과거에 적절했던 선택이 현재도 그대로 최선이라고 가정해서는 안 됩니다.
heic2any의 GitHub Issues를 확인하면 최신 iPhone과 iOS 18에서 생성된 일부 HEIC/HEIF 파일을 변환할 때 문제가 발생한다는 보고가 존재합니다.
예를 들어 현재 공개된 Issue에는 다음과 같은 사례가 있습니다.
새로운 iPhone에서 HEIC 생성
↓
기존 heic2any가 사용하는 HEIC Decoder로 처리
↓
일부 최신 HEIC 구조를 해석하지 못하는 사례 발생
↓
iOS 18 / 최신 HEIC 변환 실패 Issue 보고
이러한 Issue가 존재한다는 사실이 모든 iOS 18 HEIC 파일이 변환되지 않는다는 의미는 아닙니다.
특정 파일이나 인코딩 특성, 라이브러리 내부 Decoder 버전 등에 따라 문제가 발생할 수 있으므로 실제 지원 범위는 테스트를 통해 확인해야 합니다.
중요한 점은 이미지 변환 라이브러리를 선택할 때 단순히 API 사용 방법만 볼 것이 아니라 내부에서 사용하는 Decoder가 최신 이미지 포맷 변화를 따라가고 있는지도 확인해야 한다는 것입니다.
현재는 어떤 대안을 검토할 수 있을까?
현재 다시 같은 기능을 설계한다면 heic2any만을 전제로 결정하기보다 다른 선택지도 함께 검토할 수 있습니다.
그중 하나가 heic-to입니다.
heic-to는 브라우저에서 HEIC/HEIF를 JPEG 또는 PNG로 변환하는 라이브러리이며, 프로젝트 README에서는 heic2any와 libheif-web에서 영감을 받았다고 설명합니다.
특히 프로젝트의 목적 중 하나로 libheif의 새로운 릴리스를 지속적으로 따라가는 것을 명시하고 있습니다.
libheif 자체는 HEIF와 AVIF 등의 이미지 포맷을 디코딩하고 인코딩하기 위한 오픈소스 라이브러리입니다.
따라서 HEIC 처리 라이브러리를 선택할 때는 "단순히 어떤 API가 편한가?" 만 보는 것이 아니라
- 어떤 Decoder를 사용하는가?
- Decoder가 얼마나 최신 상태인가?
- 최신 iPhone HEIC/HEIF 대응을 위해 기반 디코더가 지속적으로 업데이트되고 있는가?
- 브라우저 Bundle 비용은 어느 정도인가?
- 실제 Target Device에서 안정적으로 동작하는가?
까지 함께 확인할 필요가 있습니다.
마치며
HEIC 이미지 업로드 문제를 처음 마주했을 때는 단순히 이미지 포맷을 JPEG로 변환하면 되는 문제라고 생각했습니다.
하지만 원인을 하나씩 확인하면서 이미지 포맷뿐 아니라 EXIF Orientation, Canvas를 이용한 재인코딩, 클라이언트의 이미지 처리 비용과 모듈 로딩까지 함께 고려하게 되었습니다.
iOS WebView에서 HEIC / HEIF 이미지 업로드 문제 발생
↓
기존 이미지 처리 흐름에서 사용할 수 있도록 JPEG로 변환
↓
변환 과정에서 이미지 방향 문제 확인
↓
EXIF Orientation을 기준으로 방향 보정
↓
Canvas에 보정된 이미지를 다시 그려 JPEG 생성
↓
모바일 기기에서 발생하는 이미지 처리 비용까지 고려
↓
HEIC일 때만 변환 모듈을 동적으로 로드하도록 개선
특히 WebView 기반 서비스에서는 웹의 기능을 사용하더라도 실제로는 모바일 기기의 카메라와 파일 시스템을 통해 만들어진 데이터를 전달받게 됩니다.
이 과정에서 운영체제와 기기에서 사용하는 이미지 포맷이나 메타데이터가 웹의 이미지 처리 과정까지 영향을 줄 수 있기 때문입니다.
일반적인 웹 환경에서는 쉽게 지나칠 수 있는 부분도 WebView에서는 네이티브 환경과 웹의 경계에서 새로운 문제로 나타날 수 있습니다.
결국 WebView 환경에서는 웹 내부의 구현뿐만 아니라 데이터가 어떤 환경에서 만들어지고, 어떤 과정을 거쳐 웹과 서버로 전달되는지까지 함께 고려하는 것이 중요합니다.