현대 웹에서 이미지 최적화는 성능 향상의 핵심 요소입니다. WebP와 AVIF는 기존 JPEG/PNG보다 30-50% 작은 파일 크기를 제공하지만, 변환 과정에서 중요한 메타데이터가 손실될 수 있습니다. 특히 **AI 생성 이미지**의 프롬프트 정보, 카메라 EXIF, PNG 텍스트 청크 등은 많은 도구에서 제대로 보존되지 않습니다. 이번 글에서는 메타데이터를 안전하게 보존하면서 최신 이미지 포맷으로 변환하는 실전 기법을 소개하겠습니다.
1. 이미지 포맷별 메타데이터 특성
각 이미지 포맷은 메타데이터를 저장하는 방식이 다릅니다. JPEG은 EXIF, PNG는 텍스트 청크, WebP는 EXIF와 XMP, AVIF는 EXIF를 지원합니다. 포맷 간 변환 시 지원하지 않는 메타데이터는 별도로 관리해야 합니다.
// 포맷별 메타데이터 지원 현황
const METADATA_SUPPORT = {
jpeg: ['exif', 'iptc', 'xmp'],
png: ['text_chunks', 'exif'],
webp: ['exif', 'xmp'],
avif: ['exif'],
heic: ['exif', 'xmp']
};
// 메타데이터 추출 유틸리티
class MetadataExtractor {
static async extractAll(file) {
const arrayBuffer = await file.arrayBuffer();
const format = this.getImageFormat(arrayBuffer);
const metadata = {
format,
exif: null,
textChunks: null,
xmp: null,
customData: null
};
switch (format) {
case 'png':
metadata.textChunks = this.extractPngTextChunks(arrayBuffer);
metadata.exif = this.extractExifFromPng(arrayBuffer);
break;
case 'jpeg':
metadata.exif = this.extractExifFromJpeg(arrayBuffer);
metadata.xmp = this.extractXmpFromJpeg(arrayBuffer);
break;
case 'webp':
metadata.exif = this.extractExifFromWebp(arrayBuffer);
metadata.xmp = this.extractXmpFromWebp(arrayBuffer);
break;
}
return metadata;
}
static getImageFormat(arrayBuffer) {
const header = new Uint8Array(arrayBuffer, 0, 12);
if (header[0] === 0x89 && header[1] === 0x50) return 'png';
if (header[0] === 0xFF && header[1] === 0xD8) return 'jpeg';
if (header[0] === 0x52 && header[1] === 0x49) return 'webp';
if (header[4] === 0x66 && header[5] === 0x74) return 'avif';
return 'unknown';
}
}
2. PNG 텍스트 청크 보존
PNG의 텍스트 청크는 AI 이미지의 프롬프트 정보 등을 저장하는 중요한 공간입니다. NovelAI, Stable Diffusion 등에서 생성된 이미지는 이 영역에 설정값을 저장합니다. WebP나 AVIF로 변환 시 이 정보를 별도로 보존해야 합니다.
// PNG 텍스트 청크 추출 및 보존
class PngTextChunkHandler {
static extractTextChunks(arrayBuffer) {
const view = new DataView(arrayBuffer);
const chunks = [];
let offset = 8; // PNG 시그니처 스킵
while (offset < arrayBuffer.byteLength) {
const length = view.getUint32(offset);
const type = new TextDecoder().decode(new Uint8Array(arrayBuffer, offset + 4, 4));
if (type === 'tEXt' || type === 'zTXt' || type === 'iTXt') {
const data = new Uint8Array(arrayBuffer, offset + 8, length);
chunks.push({
type,
length,
data: this.parseTextChunk(type, data)
});
}
offset += 12 + length; // length + type + data + crc
}
return chunks;
}
static parseTextChunk(type, data) {
if (type === 'tEXt') {
const text = new TextDecoder('latin1').decode(data);
const nullIndex = text.indexOf('\0');
return {
keyword: text.substring(0, nullIndex),
text: text.substring(nullIndex + 1)
};
}
// zTXt, iTXt 파싱 로직 추가
return { raw: data };
}
static async preserveInWebp(textChunks, webpBuffer) {
// WebP는 XMP 청크에 커스텀 메타데이터 저장
const xmpData = this.convertTextChunksToXmp(textChunks);
return this.embedXmpInWebp(webpBuffer, xmpData);
}
static convertTextChunksToXmp(textChunks) {
const xmpTemplate = `
${textChunks.map(chunk =>
`${chunk.data.text} `
).join('\n ')}
`;
return new TextEncoder().encode(xmpTemplate);
}
}
3. 브라우저 기반 이미지 변환
Canvas API를 활용하면 클라이언트에서 직접 이미지 포맷 변환이 가능합니다. 품질 설정, 크기 조절과 함께 메타데이터를 별도로 관리하는 시스템을 구축할 수 있습니다. 브라우저 지원 상황에 따른 폴백 처리도 중요합니다.
// 브라우저 기반 이미지 변환기
class ImageConverter {
constructor() {
this.canvas = document.createElement('canvas');
this.ctx = this.canvas.getContext('2d');
}
async convertToWebp(file, quality = 0.8, preserveMetadata = true) {
// 원본 메타데이터 추출
const originalMetadata = preserveMetadata ?
await MetadataExtractor.extractAll(file) : null;
// 이미지 로드 및 변환
const img = await this.loadImage(file);
this.canvas.width = img.width;
this.canvas.height = img.height;
this.ctx.drawImage(img, 0, 0);
// WebP로 변환
const webpBlob = await new Promise(resolve => {
this.canvas.toBlob(resolve, 'image/webp', quality);
});
// 메타데이터 임베드
if (originalMetadata) {
return this.embedMetadata(webpBlob, originalMetadata, 'webp');
}
return webpBlob;
}
async convertToAvif(file, quality = 0.8) {
// AVIF 브라우저 지원 확인
if (!this.supportsAvif()) {
throw new Error('AVIF format not supported in this browser');
}
const img = await this.loadImage(file);
this.canvas.width = img.width;
this.canvas.height = img.height;
this.ctx.drawImage(img, 0, 0);
return new Promise(resolve => {
this.canvas.toBlob(resolve, 'image/avif', quality);
});
}
supportsAvif() {
const canvas = document.createElement('canvas');
return canvas.toDataURL('image/avif').indexOf('data:image/avif') === 0;
}
supportsWebp() {
const canvas = document.createElement('canvas');
return canvas.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
loadImage(file) {
return new Promise((resolve, reject) => {
const img = new Image();
img.onload = () => resolve(img);
img.onerror = reject;
img.src = URL.createObjectURL(file);
});
}
async embedMetadata(imageBlob, metadata, targetFormat) {
// 메타데이터를 JSON으로 직렬화하여 별도 저장
const metadataJson = JSON.stringify(metadata);
return {
imageBlob,
metadata: metadataJson,
originalFormat: metadata.format,
targetFormat
};
}
}
4. 서버사이드 변환과 메타데이터
대용량 이미지나 일괄 처리에는 서버사이드 변환이 더 효율적입니다. Sharp, ImageMagick 등의 라이브러리를 활용하면 더 정교한 메타데이터 보존이 가능합니다. 클라우드 함수나 워커에서 실행하여 확장성도 확보할 수 있습니다.
// Node.js + Sharp를 활용한 서버사이드 변환
const sharp = require('sharp');
const ExifReader = require('exifreader');
class ServerImageConverter {
static async convertWithMetadata(inputBuffer, targetFormat, options = {}) {
// 원본 메타데이터 추출
const metadata = await this.extractMetadata(inputBuffer);
let sharpInstance = sharp(inputBuffer);
// 변환 설정
switch (targetFormat) {
case 'webp':
sharpInstance = sharpInstance.webp({
quality: options.quality || 80,
effort: options.effort || 4
});
break;
case 'avif':
sharpInstance = sharpInstance.avif({
quality: options.quality || 80,
effort: options.effort || 4
});
break;
}
// 기본 메타데이터 유지
if (options.preserveMetadata !== false) {
sharpInstance = sharpInstance.withMetadata();
}
const outputBuffer = await sharpInstance.toBuffer();
// 커스텀 메타데이터 처리
if (metadata.customData) {
return {
buffer: outputBuffer,
metadata: metadata.customData,
originalFormat: metadata.format
};
}
return outputBuffer;
}
static async extractMetadata(buffer) {
try {
const tags = ExifReader.load(buffer);
const customData = {};
// PNG 텍스트 청크 추출
if (tags.Textual && tags.Textual.value) {
customData.textChunks = tags.Textual.value;
}
// EXIF 데이터 정리
if (tags.DateTime) {
customData.dateTime = tags.DateTime.description;
}
return {
format: this.detectFormat(buffer),
exif: tags,
customData: Object.keys(customData).length > 0 ? customData : null
};
} catch (error) {
console.error('메타데이터 추출 실패:', error);
return { format: this.detectFormat(buffer), exif: null, customData: null };
}
}
static detectFormat(buffer) {
const header = buffer.slice(0, 12);
if (header[0] === 0x89 && header[1] === 0x50) return 'png';
if (header[0] === 0xFF && header[1] === 0xD8) return 'jpeg';
if (header.toString('ascii', 0, 4) === 'RIFF') return 'webp';
return 'unknown';
}
}
// Express API 엔드포인트 예시
app.post('/api/convert', upload.single('image'), async (req, res) => {
try {
const { targetFormat, quality, preserveMetadata } = req.body;
const result = await ServerImageConverter.convertWithMetadata(
req.file.buffer,
targetFormat,
{ quality: parseInt(quality), preserveMetadata: preserveMetadata === 'true' }
);
res.set({
'Content-Type': `image/${targetFormat}`,
'X-Original-Metadata': result.metadata ? JSON.stringify(result.metadata) : null
});
res.send(result.buffer || result);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
5. 메타데이터 활용 사례
보존된 메타데이터는 다양한 용도로 활용할 수 있습니다.
- AI 이미지의 프롬프트 정보 추출 및 재사용
- 촬영 정보 기반 이미지 분류 및 검색
- 저작권 정보 표시 및 워터마크 자동 생성
- 이미지 품질 분석 및 최적화 권장사항 제시
- 위치 정보 기반 지도 표시 및 여행 기록
6. 마무리하며
WebP와 AVIF로의 전환은 웹 성능 향상에 필수적이지만, 메타데이터 손실에 주의해야 합니다. 특히 AI 이미지나 전문 사진의 경우 메타데이터가 중요한 가치를 가지므로 체계적인 보존 전략이 필요합니다. 브라우저 호환성과 성능을 모두 고려한 점진적 적용으로 최상의 사용자 경험을 제공할 수 있습니다.