← 홈으로 돌아가기
INSIGHTS

JPG·PNG·WebP 포맷 변환, 확장자만 바꾸면 될까? 이미지 변환 원리 알아보기

사진 파일의 확장자만 바꾼다고 이미지 포맷이 변환되지는 않습니다. JPG·PNG·WebP의 변환 원리를 살펴보고, Canvas로 이미지 데이터를 처리한 뒤 포맷별 인코더와 WebAssembly를 이용해 실제 이미지 파일로 변환하는 과정을 코드와 함께 알아봅니다.

Pixoas 본문 이미지

이미지 작업을 하다 보면 JPGPNG로, PNGWebP로 등 다양한 포맷으로 변환해야 할 때가 있습니다. 그런데 이미지 포맷은 실제로 어떻게 변환되는 걸까요?

물론 사진 파일의 이름에서 .png.jpg.webp로 바꾼다고 이미지 형식이 변환되는 것은 아닙니다.

이미지 포맷을 변환하려면 원본 이미지의 데이터를 읽고, 변환하려는 포맷의 규칙에 맞춰 다시 인코딩하는 과정이 필요합니다. 그렇다면 JPG, PNG, WebP은 각각 어떤 방식으로 변환되고, 실제로 이 과정을 어떻게 구현할 수 있을까요?

저는 이미지 일괄 변환 웹툴인 🔗ImagePrep을 개발하면서 이 과정을 직접 구현했습니다. 이 과정에서 이미지의 크기나 비율을 변경하는 이미지 처리와, 완성된 픽셀 데이터를 실제 이미지 파일로 만드는 인코딩은 서로 다른 작업이라는 것도 알게 되었습니다.

🎨 Canvas에 이미지를 그리는 것과 파일로 만드는 것은 다릅니다

이미지 포맷을 변환하려면 먼저 원본 이미지의 데이터를 읽고, 변환할 이미지의 형태를 만들어야 합니다. 이 과정에서 이미지의 크기나 비율을 변경하거나 필요한 영역만 잘라내는 작업도 함께 처리할 수 있습니다.

이런 이미지 처리는 Canvas를 이용하면 비교적 간단하게 구현할 수 있습니다. 이미지를 Canvas에 그린 다음 Canvas의 크기와 이미지가 그려지는 영역을 조절하면, 원하는 형태의 새로운 이미지 데이터를 만들 수 있습니다.

이미지 크기와 비율을 변경하는 구체적인 구현 방법은 이전 글에서 자세히 다뤘습니다.

관련 글

그런데 Canvas에 이미지가 만들어졌다고 해서 JPG, PNG, WebP 파일이 완성된 것은 아닙니다.

Canvas에 담겨 있는 것은 이미지의 픽셀 데이터입니다. 이 픽셀 데이터를 특정 이미지 포맷에서 사용할 수 있는 파일 형태로 변환하는 과정을 인코딩(Encoding)이라고 합니다.

이미지 변환은 단순히 파일의 확장자를 바꾸는 작업이 아닙니다. 먼저 픽셀 데이터를 원하는 형태로 만든 다음, 이를 JPG, PNG, WebP와 같은 포맷의 규칙에 맞게 다시 인코딩하는 과정이 필요합니다.

⚙️ 이미지 포맷에 따라 인코딩 방식도 달라집니다

그렇다면 같은 이미지 데이터를 왜 포맷에 따라 다르게 인코딩해야 할까요?

JPG, PNG, WebP는 이미지를 저장하는 방식이 서로 다르기 때문입니다. JPG는 사진처럼 색상이 많은 이미지의 용량을 줄이는 데 적합하고, PNG는 투명한 배경을 지원하면서 이미지 데이터를 보존하는 데 유리합니다. WebP는 손실·무손실 압축과 투명도를 지원해 다양한 이미지에 활용할 수 있습니다.

결국 같은 픽셀 데이터를 사용하더라도 어떤 포맷으로 변환하느냐에 따라 인코딩하는 방법도 달라집니다.

간단한 이미지 변환이라면 Canvas에서 제공하는 기능만으로도 여러 포맷의 결과물을 만들 수 있습니다. 하지만 압축 품질을 세밀하게 조절하거나 포맷별 인코딩 방식을 직접 제어하려면 별도의 이미지 인코더를 사용하는 방법이 필요합니다.

실제로 ImagePrep을 구현할 때도 JPG, PNG, WebP를 같은 방식으로 처리하지 않았습니다. 각 포맷의 특성에 맞는 인코딩 방식을 사용해야 했기 때문입니다.

이처럼 이미지 데이터를 원하는 포맷으로 변환하려면 해당 포맷을 실제로 인코딩해 주는 프로그램, 즉 이미지 인코더가 필요합니다.

🚀 오픈소스 이미지 인코더를 WebAssembly로 사용하기

이미지 인코더는 직접 구현하기보다, 이미 만들어진 오픈소스 라이브러리를 가져와 사용하는 방법이 일반적입니다.

이런 라이브러리는 보통 GitHubnpm에서 찾을 수 있으며, image encoder wasm, Squoosh codecs 등의 키워드로 검색하면 JPG, WebP, PNG 등 다양한 이미지 포맷을 처리할 수 있는 프로젝트를 찾을 수 있습니다.

실제로 ImagePrep을 개발할 때도 JPGWebP 인코더를 직접 구현하지 않고, Squoosh에서 사용되는 이미지 코덱을 WebAssembly 형태로 제공하는 jSquash를 활용했습니다. @jsquash/jpeg@jsquash/webp를 사용해 각각 JPGWebP 인코딩을 처리했습니다.

이때 오픈소스라고 해서 무조건 자유롭게 사용할 수 있는 것은 아닙니다. 실제 서비스에 적용하기 전에는 해당 라이브러리의 라이선스를 확인해야 합니다. 라이선스에 따라 상업적 이용 가능 여부나 지켜야 할 조건이 달라질 수 있기 때문입니다. ImagePrep에서 사용한 두 패키지는 Apache-2.0 라이선스로 공개되어 있어 상업적 이용이 가능합니다.

라이브러리를 선택했다면 이렇게 만들어진 이미지 인코더를 웹에서는 어떻게 실행할 수 있을까요?

여기서 활용되는 기술이 바로 WebAssembly(WASM)입니다.

WebAssemblyC, C++, Rust 같은 언어로 작성된 프로그램을 웹 브라우저에서 실행할 수 있도록 만든 기술입니다. JavaScript와 함께 사용할 수 있으며, 이미지 인코딩처럼 계산량이 많은 작업을 웹에서 처리할 때 활용할 수 있습니다.

WebAssembly로 만들어진 코드는 보통 .wasm 파일 형태로 제공됩니다. 이미지 인코더 라이브러리를 사용하면 이미 만들어진 인코더와 필요한 .wasm 파일을 가져와 JavaScript에서 사용할 수 있습니다.

예를 들어 @jsquash/jpeg를 이용해 JPG 인코딩을 구현하려면, 먼저 JPG 인코더가 사용할 .wasm 파일을 준비해야 합니다. ImagePrep에서는 이 파일을 프로젝트의 assets/wasm/ 폴더에 넣어두고 JavaScript에서 불러오는 방식으로 구현했습니다.

const response = await fetch(
  './assets/wasm/mozjpeg_enc.wasm'
);

const buffer = await response.arrayBuffer();

const wasmModule =
  await WebAssembly.compile(buffer);

여기서 ./assets/wasm/mozjpeg_enc.wasm프로젝트 폴더 안에 저장해 둔 실제 파일의 경로와 파일명입니다. mozjpeg_enc.wasmJPG 인코딩에 사용하는 WebAssembly 파일로, 이미지 인코더를 브라우저에서 실행하기 위해 프로젝트에 포함해 둔 것입니다.

fetch()로 이 파일을 가져온 뒤 arrayBuffer()로 파일의 데이터를 읽고, WebAssembly.compile()을 통해 브라우저에서 사용할 수 있는 모듈로 준비합니다.

이제 준비한 WebAssembly 모듈을 실제 JPG 인코더와 연결합니다.

const { default: init, encode } =
  await import(
    'https://unpkg.com/@jsquash/[email protected]/encode.js?module'
  );

await init(wasmModule);

이 코드는 @jsquash/jpeg에서 제공하는 인코더를 불러온 뒤, 앞에서 준비한 WebAssembly 모듈과 연결하는 과정입니다.

init()으로 인코더를 초기화하면 이후 encode()를 사용해 Canvas에서 얻은 이미지 데이터를 JPG로 인코딩할 수 있습니다.

즉, JPG의 복잡한 인코딩 방식을 직접 구현하는 것이 아니라 이미 만들어진 인코더와 필요한 WebAssembly 파일을 가져와 브라우저에서 실행하는 방식입니다.

WebP도 같은 개념으로 처리하지만, WebP 인코딩에는 webp_enc.wasm처럼 해당 인코더에 필요한 별도의 .wasm 파일을 사용합니다.

🧩 PNG는 어떻게 인코딩할까?

그렇다면 PNGJPGWebP와 같은 방식으로 처리하면 될까요?

앞에서 JPGWebP는 각각 @jsquash/jpeg, @jsquash/webpWebAssembly를 이용해 인코딩한다고 설명했습니다. PNG에는 이와 다른 방식으로 UPNG.js라는 오픈소스 JavaScript 라이브러리를 사용했습니다.

UPNG.jsPNG 이미지를 인코딩하고 최적화할 수 있는 라이브러리입니다. PNG의 특성에 맞춰 투명도를 유지하면서 이미지를 저장하거나, 설정에 따라 색상 수를 줄여 파일 크기를 줄이는 등의 처리가 가능합니다.

실제 인코딩 과정에서는 먼저 Canvas에서 픽셀 데이터를 가져옵니다.

const imageData = ctx.getImageData(
  0,
  0,
  canvas.width,
  canvas.height
);

const pngData = UPNG.encode(
  [imageData.data.buffer],
  canvas.width,
  canvas.height,
  0
);

getImageData()Canvas에 그려진 이미지의 픽셀 데이터를 가져옵니다. 이렇게 얻은 imageData.dataUPNG.encode()에 전달하면 UPNG.js가 해당 픽셀 데이터를 PNG 형식으로 인코딩합니다.

즉, 이 코드에서도 앞에서 살펴본 것처럼 Canvas에서 이미지 데이터를 가져오는 과정과, 그 데이터를 실제 이미지 파일 형식으로 인코딩하는 과정이 구분되어 있습니다.

마지막 인자인 0은 색상 수를 제한하지 않고 인코딩하는 설정입니다. 0 대신 숫자를 입력하면 해당 숫자를 기준으로 사용할 색상 수를 줄일 수 있으며, 숫자가 작을수록 파일 크기는 작아지는 대신 색상 표현이 단순해지고, 숫자가 클수록 원본에 가까운 색상을 표현하는 대신 파일 크기가 커질 수 있습니다. 따라서 이미지의 특성에 따라 적절한 색상 수를 선택해야 합니다.

이처럼 JPG, WebP, PNG 모두 픽셀 데이터를 이미지 파일로 만드는 인코딩 과정은 필요하지만, 포맷마다 저장 방식과 특성이 다르기 때문에 사용하는 인코더와 구현 방식도 달라집니다.

🔍 이미지 포맷 변환의 핵심은 인코딩입니다

이미지 포맷 변환은 단순히 파일의 확장자를 바꾸는 작업이 아닙니다. 이미지 데이터를 읽은 뒤 변환하려는 포맷의 규칙에 맞게 다시 인코딩해야 새로운 이미지 파일이 만들어집니다.

JPG, PNG, WebP는 각각 이미지를 저장하는 방식과 특성이 다르기 때문에 사용하는 인코더와 처리 방식도 달라집니다. JPGWebP는 각각의 인코더를 WebAssembly와 연결해 사용할 수 있고, PNGUPNG.js와 같은 라이브러리를 이용해 이미지 데이터를 PNG 형식으로 인코딩할 수 있습니다.

결국 웹에서 이미지 포맷 변환 기능을 구현할 때 중요한 것은 이미지 데이터를 원하는 포맷의 파일로 만들어주는 인코딩 과정을 이해하는 것입니다. 어떤 포맷으로 변환하느냐에 따라 그에 맞는 인코더와 처리 방법을 선택해야 한다는 것이 핵심입니다.