2024. 02. 05 > 2025. 06. 23 블로그 A/S 프로젝트🚧 로 추가한 글입니다.
이미지 압축의 원리와 sharp 라이브러리에 대해서 알아보고, 예제를 추가합니다.
이미지는 웹에서 가장 큰 용량을 차지하고 있다! 그만큼 웹 최적화에 많은 영향을 끼치기 때문에 중요하다.
이미지 압축
PNG | JPEG | WebP |
👍 비손실 압축 | 👍 색상이 다양한 이미지에 적합 👍 PNG보다 용량이 비교적 작다 |
👍 용량이 PNG, JPEG보다 작다 |
👎 색상이 다양한 이미지에 부적합 | 👎 손실 압축 👎 투명도 지원 X |
👎 구형 브라우저 지원 X |
⏩️ WebP 가 95%의 커버리지를 가지고 있다고 하니 웬만하면 WebP 쓰자!
JPEG 손실 압축의 원리
1. Color Space Conversion
- 인간의 눈은 밝기를 감지하는 세포가 RGB를 구분하는 세포보다 압도적으로 더 많다. 그렇기 때문에 밝기 정보는 유지하고 색 정보를 분리해서 압축시켜도 눈이 잘 알아채지 못한다.
- 기본 비트맵 이미지는 RGB. 이것을 YCbCr로 변환해준다. Y가 밝기를 나타내기 때문에 분리해주기 위해!
- RGB → YCbCr
2. Chrominance Downsampling
- 이제 Y값은 두고 CbCr을 압축해보자.
- Cb, Cr의 행렬에서 셀을 네개씩 묶어 평균을 구해 각각 1/4로 크기를 압축시켜줄 수 있다.
3. DCT (Discrete Cosine Transform)
- 이제 이미지를 8x8 블록으로 자르고, 각 블록에 DCT를 씌워보자.
- 숫자들이 중요한 정보(=굵직한 패턴=고주파=변화량이 큼) vs 덜 중요한 정보(=세밀한 패턴=저주파=변화량이 작음)로 나뉘게 된다.
4. Quantization - 양자화
- 덜 중요한 정보는 0으로 다 바꿔버린다.
5. Run Length and Huffman Encoding
Run Length Encoding (RLE)
ex. AAABBBCCDA → A3B3C2D1A1
허프만 알고리즘?
빈도수에 따라 이진 트리로 머지 후, 루트에서부터 아래로 내려가면서 숫자 부여.
자주 나오는 문자는 → 짧은 비트 코드 (숫자가 작아짐)
덜 나오는 문자는 → 긴 비트 코드 (숫자가 길어짐)
- 양자화의 결과값은 왼쪽 위에 유의미한 값이 몰려있고 오른쪽 아래로 갈수록 0이 많다.
- 왼쪽 위 → 오른쪽 아래 순서로 RLE를 적용해주어서 압축한다.
- 그 후 허프만 인코딩을 적용해서 다시 한 번 데이터 크기를 줄인다.
PNG 비손실 압축의 원리
1. Filtering
반복되는 패턴이 최대한 많아지도록 Filtering으로 전처리를 해준다.
숫자값을 그대로 사용하는 것보다, 차이를 이용하면 반복되는 값이 많아지기 때문이라고 한다.
어느 쪽 픽셀들의 차이를 구할 것인지는 행마다 더 좋은 것을 휴리스틱으로 판별해서 선택한다.
2. LZSS + 슬라이딩 윈도우
LZSS 알고리즘?
압축 대상 데이터를 왼쪽부터 오른쪽으로 훑어보면서,
지금까지 본 내용 중에서 현재 위치 이후에 다시 나오는 패턴이 있는지 찾음.
그 패턴이 있다면, 이전 위치(거리)와 패턴의 길이로 나타냄.
없다면 그냥 문자 그대로 출력.
슬라이딩 윈도우로 범위를 제한하여 LZSS 알고리즘을 적용한다.
3. Huffman Encoding
최종적으로 압축된 LZSS의 결과물을 인코딩(부호화)로 한 번 더 압축한다.
WebP 압축의 원리
뭘 했길래 WebP는 용량이 작을까?
손실 압축은 JPEG와 유사하지만, 맨 처음 PNG의 필터링 과정을 거쳐 데이터들을 비슷한 숫자로 가공한 후 진행한다고 한다. 비손실 압축은 PNG와 유사하지만, LZSS 대신 LZ77 알고리즘을 사용하며, 독자적인 예측 알고리즘을 사용한다고 한다.
Sharp?
Next.js에서 프로덕션 환경에서의 사용을 권장하고 있다. (디폴트로 설치된 것보다 빠름)
libvips 라는 라이브러리를 노드에서 쓸 수 있게 래핑한 라이브러리라고 한다.
그런데 찾아보니 libvips에서는 또 webp 처리를 위해 libwebp라는 구글에서 만든 라이브러리를 사용하고 있었다. 아마 여기에 위에서 배운 내용들로 압축 알고리즘이 구현되어 있을 것이다.
예시
sharp 라이브러리를 활용하여, ${path}-${width}w.webp 요 형식으로 이미지를 압축하는 동시에 여러 크기로 생성해주는 스크립트를 작성하였다. 위의 복잡한 과정을 직접 구현하지 않아도 되어 감사할 따름이다...
// 요렇게 내가 만들어줄 이미지 크기를 먼저 지정한다. suffix는 그에 따라 이미지에 붙일 접미사!
// 지금 보니 이것도 하드코딩하지 않고 따로 빼줄 수 있을듯.
const BREAKPOINTS = [
{ width: 480, suffix: '-480w' },
{ width: 768, suffix: '-768w' },
{ width: 1280, suffix: '-1280w' },
];
const convertImagesToWebP = async () => {
try {
if (!fs.existsSync(IMAGES_DIR)) {
fs.mkdirSync(IMAGES_DIR);
}
const folders = fs.readdirSync(IMAGES_DIR);
for (const folder of folders) {
const FOLDER_PATH = path.join(IMAGES_DIR, folder);
if (fs.statSync(FOLDER_PATH).isDirectory()) {
const files = fs.readdirSync(FOLDER_PATH);
console.log(`🔄 ${files.length}개의 이미지를 변환 중...`);
for (const file of files) {
const FILE_PATH = path.join(FOLDER_PATH, file);
if (fs.statSync(FILE_PATH).isFile() && /\.(png|jpg|jpeg)$/i.test(file)) {
const OUTPUT_FILE_PATH = path.join(FOLDER_PATH, file.replace(/\.(png|jpg|jpeg)$/i, ".webp"));
// 여기서 변환 로직이 실행된다. 이미지 폴더의 파일들을 모두 webp로 변환!
await sharp(FILE_PATH)
.toFormat("webp")
.webp({ quality: 80 })
.toFile(OUTPUT_FILE_PATH);
console.log(`✅ 변환 완료: ${OUTPUT_FILE_PATH}`);
fs.unlinkSync(FILE_PATH);
}
// 이후 변환된 파일을 리사이징
for (const { width, suffix } of BREAKPOINTS) {
const RESIZED_FILE_PATH = path.join(FOLDER_PATH, file.replace(/\.webp$/i, `${suffix}.webp`));
await sharp(FILE_PATH)
.resize(width)
.toFile(RESIZED_FILE_PATH);
console.log(`✅ 리사이징 완료: ${RESIZED_FILE_PATH}`);
}
}
}
};
console.log("🎉 모든 이미지 변환이 완료되었습니다!");
} catch (error) {
console.error("❌ 변환 중 오류 발생:", error);
}
};
convertImagesToWebP();
짠!
반응형 이미지 제공하기
<img>의 속성 이용! 위에서 브레이크포인트마다 다른 크기로 만들어준 이미지들을 사용해볼 차례이다.
- srcset
- 제공할 이미지들을 적는다.
- "이미지이름 크기w, 이미지이름 크기w, ..." 형식으로 적어주면 된다.
- sizes
- 이미지 출력 시 어느 정도의 너비를 가져야 하는지 알려주기 (% 사용불가)
- "(미디어 조건문) 이미지가 가질 너비 ..." 형식으로 적어주면 된다.
<img src="ham-l.png" //fallback
srcset="ham-s.png 640w, ham-m.png 1080w, ham-l.png 2048w"
sizes="(max-width: 991px) 80vw, 100vw" />
예시
위에서 만든 ${path}-${width}w.webp 요 이미지들을 클라이언트 화면 크기에 따라 띄워준다.
interface ImageProps {
src: string;
alt: string;
isLazy?: boolean;
className?: string;
}
const BREAKPOINTS = [480, 768, 1280];
const SIZES = `
(max-width: 480px) 480px,
(max-width: 768px) 768px,
(max-width: 1280px) 1280px,
(min-width: 1281px) 1920px
`;
const createSrcSet = (path: string) =>
BREAKPOINTS
.map((width) => `${path}-${width}w.webp ${width}w`)
.join(', ');
export const Image = ({ src, alt, isLazy = false, className }: ImageProps) => {
const BASE_PATH = src.split('.')[0];
return (
<img
alt={alt}
className={className}
loading={isLazy ? 'lazy' : 'eager'}
sizes={SIZES}
src={src}
srcSet={`${createSrcSet(BASE_PATH)}, ${src} 1920w`}
/>
);
};
이미지 스프라이트
[240115] 이미지 스프라이트와 자동화
이미지 스프라이트란? 웹사이트들을 개발자도구로 뜯어보면 네트워크 탭에서 가끔 저런 이미지의 모음들을 볼 수 있다. 이게 이미지 스프라이트이다! 이미지 스프라이트란, 여러 개의 배경 이
hamo0.tistory.com
next/image 활용하기
next/image는 위에서 제안한 최적화 방법들을 대부분 제공하고 있다.
- 이미지 포맷 선택 가능 - WebP 포맷이 디폴트
// next.config.js
module.exports = {
images: {
formats: ['image/avif', 'image/webp'],
},
}
- fill 속성을 이용하여 자동으로 반응형 이미지 제공
- Lazy Loading 디폴트
참고
JPG, PNG, WebP에 대해 알아보자
JPG, PNG, WebP에 대해 알아보자
seholee.com
'TIL > Web' 카테고리의 다른 글
[240203] CSR, SSG, SSR (0) | 2024.02.03 |
---|---|
브라우저 렌더링, 리플로우, 리페인팅 (1) | 2024.01.17 |
[240112] 스크린리더 UX 개선하기 (1) | 2024.01.13 |
[240111] 웹 표준, 웹 접근성 (0) | 2024.01.11 |