[최적화] 이미지 압축/ 포맷 변환(to webP) 자동화

hsecode·2023년 6월 19일
3

최적화

목록 보기
6/7
post-thumbnail

이미지 압축 자동화에 이어.. 포맷도 자동으로 바꿔보자!

📌 기본 사항

기본적으로 설치해야하는 것들은 이미지 압축 자동화와 같다. (node.js/ npm 설치부터 imagemin-sharp 설치까지)

📎 node.js, npm 설치 / package json 생성

기본적으로 node js와 npm 설치가 필요하다.

node js

npm install
npm init -y

💡 -y : default 값으로 설정된 package.json 추가

📌 imagemin 설치하기

imagemin과 압축/포맷 변경에 사용할 imagemin-sharp, imagemin-webp를 설치한다.

npm install imagemin
npm install imagemin-sharp --save
npm install imagemin-webp

📌 이미지 압축과 포맷 변환을 하나의 js 파일로 컨트롤하기

지난번 이미지 압축 자동화때 만들었던 js파일을 개선했다. if문을 추가해 imagemin-sharp 와 imagemin-webp 플러그인을 한 파일에서 컨트롤 할 수 있도록 했다.

import imagemin from 'imagemin';
import imageminSharp from 'imagemin-sharp';
import imageminWebp from 'imagemin-webp';

async function optimizeImages(folderName, plugin) {
  const INPUT = `imgs/${folderName}/*.{jpg,png}`;
  const OUTPUT = `imgs/${folderName}`;
  let plugins;

  if (plugin === 'sharp') {
    plugins = [
      imageminSharp({
        chainSharp: async (sharp) => {
          return sharp;
        },
      }),
    ];
    console.log(`${folderName}의 이미지 압축이 완료되었습니다. ✨`);
  } else if (plugin === 'webp') {
    const webpOutput = `imgs/${folderName}/webp`;
    const convertedFiles = await imagemin([INPUT], {
      destination: webpOutput,
      plugins: [
        imageminWebp({
          quality: 75,
        }),
      ],
    });
    console.log(`💎 ${folderName}의 WebP 폴더 생성이 완료되었습니다. 💎`);
  } else {
    console.error(`🚨 Error: Invalid plugin specified.`);
    return;
  }
}

📎 폴더명으로 동작하도록 하며, 명령어가 올바르지 않을 시 경고

if ((process.argv[2] === 'build:sharp' || process.argv[2] === 'build:webp') && process.argv.length >= 4) {
  const folderName = process.argv[3];
  let plugin;

  if (process.argv[2] === 'build:sharp') {
    plugin = 'sharp';
  } else if (process.argv[2] === 'build:webp') {
    plugin = 'webp';
  }

  optimizeImages(folderName, plugin).catch((error) => {
    console.error(`🚨 Error optimizing images for ${folderName} using ${plugin} plugin:`, error);
  });
} else {
  console.error('🚧 Error: 폴더명이 누락되었습니다.');
}

⭐️ 개선하기

생각하지 못했던 부분에서 문제를 발견했다.
입력한 폴더의 하위 폴더에있는 이미지의 경우 압축과 포맷 변환이 이루어지지 않았다. path모듈을 추가해 경로를 변경했다.

그런데.. 이 부분이 생각보다 간단하게 수정되지 않았다.. 단순하게 경로만 변경해서 되는 것이 아니었다...

폴더 내 모든 하위 폴더와 파일을 탐색하기 위해서는 재귀함수를 써야한다고 한다.
재귀함수란..간단하게 자신을 재참조하는 함수를 말한다고 한다.
그러니까 대략..
1. 각 항목이 폴더인지 파일인지 확인하고
2. 파일인 경우는 sharp(또는 webp) 실행
3. 폴더일 경우 재귀함수 실행(해당 폴더 내 하위 폴더와 파일을 탐색하기 위해)
이런 과정을 거치는듯 하다..
아닐시 알려주세요 제발제발..

import imagemin from 'imagemin';
import imageminSharp from 'imagemin-sharp';
import imageminWebp from 'imagemin-webp';
import fs from 'fs';
import path from 'path';

async function optimizeImages(folderPath, plugin) {
  const files = fs.readdirSync(folderPath);

  for (const file of files) {
    const filePath = path.join(folderPath, file);
    const stat = fs.statSync(filePath);

    if (stat.isDirectory()) {
      await optimizeImages(filePath, plugin);
    } else {
      const ext = path.extname(file).toLowerCase();
      if (!['.jpg', '.png', '.jpeg'].includes(ext)) {
        console.log(`⛔️ ${filePath}은 지원하지 않는 파일 형식입니다.`);
        continue;
      }

      if (plugin === 'sharp') {
        const convertedFiles = await imagemin([filePath], {
          destination: folderPath,
          plugins: [
            imageminSharp({
              chainSharp: async (sharp) => {
                return sharp;
              },
            }),
          ],
        });
        console.log(`🪄 ${filePath}의 이미지 압축이 완료되었습니다.`);
      } else if (plugin === 'webp') {
        const webpOutput = path.join(folderPath, 'webp');
        fs.mkdirSync(webpOutput, { recursive: true });

        const webpFilePath = path.join(webpOutput, file);
        await imagemin([filePath], {
          destination: webpOutput,
          plugins: [
            imageminWebp({
              quality: 75,
            }),
          ],
        });
        console.log(`💎 ${filePath}의 WebP 변환 및 저장이 완료되었습니다.`);
      }
    }
  }
}

그리하여.. 조금 장황하게 .. 바뀌게 되었다 😓
잘못된 폴더명을 입력했을 경우 알림 문구를 띄워주는 부분을 추가하고, 폴더명이 누락됐을 경우 띄워주는 알림 부분도 수정했다.
(나름대로 여러번의 수정 작업을 거쳤다...험난쓰)

function optimizeFolder(folderName, plugin) {
  const folderPath = path.join('imgs', folderName);

  if (!fs.existsSync(folderPath)) {
    console.error(`🚧 Error: '${folderName}' 폴더가 존재하지 않습니다.`);
    return;
  }

  optimizeImages(folderPath, plugin)
    .then(() => {
      console.log(`✨✨✨ ${folderName}의 이미지 변환 및 저장이 완료되었습니다. ✨✨✨`);
    })
    .catch((error) => {
      console.error(`🚨 Error optimizing images in ${folderName} using ${plugin} plugin:`, error);
    });
}

if ((process.argv[2] === 'build:sharp' || process.argv[2] === 'build:webp') && process.argv.length >= 4) {
  const folderName = process.argv[3];
  let plugin;

  if (process.argv[2] === 'build:sharp') {
    plugin = 'sharp';
  } else if (process.argv[2] === 'build:webp') {
    plugin = 'webp';
  }

  optimizeFolder(folderName, plugin);
} else {
  console.error('🚧 Error: 폴더명이 누락되었습니다.');
}

📌 package.json 수정

{  
  "scripts": { // 실행 명령어
    "sharp": "node --experimental-modules imgopt.mjs sharp",
    "webp": "node --experimental-modules imgopt.mjs webp"
  },
  "dependencies": { // 패키지 추가
    "imagemin": "^8.0.1",
    "imagemin-sharp": "^1.0.6",
    "imagemin-webp": "^8.0.0"
  }
}

📌 +) glob의 경우 ...

댓글을 통해 glob/globby라는걸 알게 되었고, 추가적으로 적용해보았다. 결과적으로 기존보다 훨씬 빨라진 느낌.. 초반에 있던..MAX_SIZE 알림도 추가했다.

globby를 사용하기 위해서는 설치가 필요하다.

npm install globby

참고:
https://github.com/isaacs/node-glob
https://www.npmjs.com/package/glob
https://github.com/imagemin/imagemin
https://github.com/sindresorhus/globby#globbing-patterns

import imagemin from 'imagemin';
import imageminSharp from 'imagemin-sharp';
import imageminWebp from 'imagemin-webp';
import fs from 'fs';
import path from 'path';
import {globby} from 'globby';
const MAX_SIZE = 5000;

let warnedFiles = []; // 초과하는 이미지 파일 이름을 저장할 배열

async function optimizeImages(files, folderPath, plugin) {
  const promises = files.map(async (filePath) => {
    const ext = path.extname(filePath).toLowerCase();
    if (!['.jpg', '.png', '.jpeg'].includes(ext)) {
      console.log(`⛔️ ${filePath}은 지원하지 않는 파일 형식입니다.`);
      return;
    }

    if (plugin === 'sharp') {
      try {
        const result = await imagemin([filePath], {
          destination: folderPath,
          plugins: [
            imageminSharp({
              chainSharp: async (sharp) => {
                const meta = await sharp.metadata();
                if (meta.width > MAX_SIZE) {
                  const fileName = path.basename(filePath);
                  if (!warnedFiles.includes(fileName)) {
                    warnedFiles.push(fileName);
                  }
                }
                return sharp;
              },
            }),
          ],
        });
        console.log(`🪄 ${filePath}의 이미지 압축이 완료되었습니다.`);
        return result;
      } catch (error) {
        console.error(`🚨 Error optimizing image:`, error);
      }
    } else if (plugin === 'webp') {
      const webpOutput = path.join(folderPath, 'webp');
      fs.mkdirSync(webpOutput, { recursive: true });

      const webpFilePath = path.join(webpOutput, path.basename(filePath, ext) + '.webp');
      try {
        const result = await imagemin([filePath], {
          destination: webpOutput,
          plugins: [
            imageminWebp({
              quality: 75,
            }),
          ],
        });
        console.log(`💎 ${filePath}의 WebP 변환 및 저장이 완료되었습니다.`);
        return result;
      } catch (error) {
        console.error(`🚨 Error converting to WebP:`, error);
      }
    }
  });

  return Promise.all(promises);
}

function formatFileList(fileList) {
  return fileList.map((fileName) => `'${fileName}'`).join(', ');
}

async function optimizeFolder(folderName, plugin) {
  const folderPath = path.join('imgs', folderName);

  if (!fs.existsSync(folderPath)) {
    console.error(`🚧 Error: '${folderName}' 폴더가 존재하지 않습니다.`);
    return;
  }

  const files = await globby(path.join(folderPath, '**/*.{jpg,png,jpeg}'));

  try {
    await optimizeImages(files, folderPath, plugin);

    // 초과하는 이미지 파일 리스트 출력
    if (warnedFiles.length > 0) {
      const fileListString = formatFileList(warnedFiles);
      console.warn(`🚨 주의 : width 값이 ${MAX_SIZE}px을 초과하는 이미지가 있습니다. (${fileListString})`);
    }
  } catch (error) {
    console.error(`🚨 Error optimizing images:`, error);
  }
}

if ((process.argv[2] === 'build:sharp' || process.argv[2] === 'build:webp') && process.argv.length >= 4) {
  const folderName = process.argv[3];
  let plugin;

  if (process.argv[2] === 'build:sharp') {
    plugin = 'sharp';
  } else if (process.argv[2] === 'build:webp') {
    plugin = 'webp';
  }

  optimizeFolder(folderName, plugin);
} else {
  console.error('🚧 Error: 폴더명이 누락되었습니다.');
}

👏🏻 마무리

🐱: 최적화를 실무에 쉽고 빠르게 적용하기위해 몇가지 아이디어를 떠올려봤는데..
그 중 하나가 지난번 업데이트했던 이미지 용량 줄이기 자동화 이다.
비슷한 원리로 이미지 포맷 또한 (jpg/png -> webP) 자동으로 바꿀 수 있다면 편하겠다 싶었다. 사실.. 생각만 하고 막연히 그건 어렵겠지.. 싶었는데..
이게 되네...?
능력자님들은 이미 다 만들어둔 것이다.. 안 쓸 이유가 없지

🗂 참조한 문서

profile
Markup Developer 💫

6개의 댓글

comment-user-thumbnail
2023년 6월 19일

플러그인을 적재적소에 사용하는 것도 능력이라고 생각합니다.
이 커스텀 된 플러그인 또한 한 번 사용해 보도록 하겠습니다.

감사합니다. (띠용 이게되네?)

1개의 답글
comment-user-thumbnail
2023년 6월 19일

파일을 찾을때 재귀를 하는 방법도 있지만, globs 를 사용하면 더 편합니다.
https://github.com/isaacs/node-glob

1개의 답글