...
Sharp 모듈
노드 진영에는 많은 이미지 리사이징 패키지들이 있었지만, 끝까지 살아남은 모듈이 shap 이다.
이미지 리사이징 동작 자체가 cpu와 메모리를 잡아먹는 주범이라, 가끔 out of memory로 node가 죽는 경우가 있기 때문이다.
그런 관점에서 sharp 모듈은 리사이징 속도도 빠르며 메모리도 다른 동종 모듈 대비 많이 잡아 먹지 않는다.
Sharp 사용법
아래 공식 문서 홈페이지에 들어가보면 옵션에 대해 아주 잘 정리되어있다.
> npm install sharp
sharp 모듈 설치시 주의사항 ⚠️
sharp는 OS의 바이너리를 이용하기 때문에 윈도우, 맥, 리눅스 OS에 따라 설치되는 모듈이 다르게 된다.
단순히 npm install sharp 명령어를 실행할 경우 현재 내 컴퓨터의 OS와 CPU 아키텍쳐 사양에 맞게 전용 바이너리가 다운되게 된다.
현 OS에서 쭉 사용할 것이면 문제는 없지만 만일 node_modules를 그대로 리눅스 서버로 들고가서 올려 사용할때 문제가 생기게 된다. 왜냐하면 윈도우 바이너리로 돌아가는 shap 패키지를 리눅에서 돌리려고 하니 돌아갈리가 없기 때문이다.
이러한 문제는 윈도우 OS에서 개발하고 AWS Lambda 함수로 업로드 할때(람다는 아마존 리눅스로 돌아간다) 가장 빈번히 발생하는 문제이다.
따라서 만일 리눅스 환경에서 사용할 예정이라면 다음과 같이 미리 옵션으로 플랫폼을 지정해서 사용해야 한다.
> npm install --platform=linux sharp
[옵션]
--platform: 중 linux하나 darwin또는 win32.
--arch: x64, ia32, arm또는 arm64.
--arm-version: 6, 7또는 8( arm기본값은 6, arm64기본값은 8) 중 하나입니다.
--libc: glibc또는 musl. 이 옵션은 플랫폼에서만 작동하며 linux기본값은glibc
--sharp-install-force: 버전 호환성 및 하위 리소스 무결성 검사를 건너뜁니다.
기본 이미지 리사이징
기본적인 이미지 리사이징 사용법은 다음고 같다.
각 메서드들이 체이닝 형태로 이루어져 있으며, 처음 로컬 이미지 파일을 불러올때는 원래라면 fs 모듈로 파일을 불러와야 되지만, 다음과 같이 바로 편리하게 불러올수도 있다.
.resize 메서드의 인수인 width, height 중 한 값만 주어질 경우 나머지 값은 적절하게 조정된다.
const sharp = require('sharp');
(async () => {
const image = await sharp('./my-image.jpg')
.resize(500, 500, { fit: 'contain' }) // fit : contain 가로 세로 비율을 강제 유지
.withMetadata() // 원본 이미지의 메타데이터 포함
.toFormat('jpeg', { quality : 100 }) // 포맷, 퀄리티 지정
.toFile('resizeIMG.jpeg', (err, info) => { // 리사이징된 이미지를 로컬에 저장
console.log(`리사이징 이미지 info : ${JSON.stringify(info, null, 2)}`);
})
.toBuffer(); // 리사이징된 이미지를 노드에서 읽을수 있게 buffer로 변환
})();
Node.js에서 파일이나 이미지(blob)와 같은 이진 데이터를 읽고 다루기 위해서는 마지막에 toBuffer()을 통해 buffer로 변환해야 한다.
이미지 크기 제한
만일 리사이징 가로 길이(500픽셀)보다 작은 이미지를 업로드하면 어떻게 할까?
원본보다 큰 500픽셀에 맞추면 이미지가 깨질 것이고, 리사이징 의미가 없다.
따라서 이미지의 크기를 미리 알 수 있다면 이런 상황을 미연에 방지할 수 있다.
(async() => {
const maxWidth = 500;
const image = await sharp('./my-image.jpg');
const { width } = await image.metadata(); // 원본이미지 크기 얻기
if (width > maxWidth) {
const resized = await image.resize({ width: maxWidth });
}
})();
ajax 이미지 다루기
로컬 이미지가 아닌, ajax로 이미지를 불러올때는 바이너리 작업을 해주어야 한다.
이미지를 불러오고 노드에서 이진 데이터를 다룰 수 있게 버퍼로 변환해주어야 한다.
/* fetch로 이미지 불러오기 */
const fetch = require('node-fetch'); // > npm i node-fetch
(async() => {
const img = await fetch('이미지URL') // 이미지 url을 fetch
.then((res) => res.buffer()) // 반환 이미지를 blob으로 변환
const resized = await sharp(img).resize(200, 200);
})();
/* axios로 이미지 불러오기 */
const axios = require('axios'); // > npm i axios
(async() => {
const img = await axios
.get('이미지URL', { responseType: 'arraybuffer' })
.then((response) => Buffer.from(response.data)); // 이미지를 서버단으로 ajax로 가져오기
const resized = await sharp(img).resize(200, 200);
})();
multer와 함께 사용
multer로 이미지 업로드를 하면 미들웨어를 통해 req.file 객체가 들어올 것이고, 이를 바탕으로 sharp로 리사이징 해주는 코드이다.
const sharp = require('sharp');
const fs = require('fs');
const multer = require('multer');
const storage = multer.diskStorage({
destination: (req, file, cb) => {
cb(null, 'uploads/');
},
filename: (req, file, cb) => {
let newFileName = new Date().valueOf() + path.extname(file.originalname);
cb(null, newFileName);
},
});
router.post('/upload', upload.single('img'), (req, res, next) => {
try {
sharp(req.file.path) // 리사이징할 파일의 경로
.resize({ width: 640 }) // 원본 비율 유지하면서 width 크기만 설정
.withMetadata()
.toFile(`${req.file.path}/resize.png`, (err, info) => {
if (err) throw err;
console.log(`info : ${info}`);
fs.unlink(`${req.file.path}/resize.png`, (err) => {
// 원본파일은 삭제해줍니다
// 원본파일을 삭제하지 않을거면 생략해줍니다
if (err) throw err;
});
});
} catch (err) {
console.log(err);
}
});
워터마크 넣기
sharp는 이미지 리사이징 뿐만 아니라 워터마크 넣는 기능도 지원해준다.
워터마크 라고 봣자 이미지 위에 이미지를 올려놓는 형식이라 복잡한 작업이 아니다.
이미지를 다른 이미지에 추가하기 위해서는 추가할 이미지를 Buffer 객체로 바꾸어 주어야 한다.
(async() => {
const image = await sharp('./my-image.png');
const watermark = await sharp('./watermark.png').toBuffer();
const watermarked = await image
.composite([{
input: watermark,
gravity: 'southeast',
}])
.toFile(`watermarkIMG.${requiredFormat}`, (err, info) => {
// 리사이징된 이미지를 로컬에 저장
console.log(`워터마크된 이미지 info : ${JSON.stringify(info, null, 2)}`);
})
})();
이미지를 포함한 컨텐츠 삽입 시에는 composite 이라는 메소드를 사용한다.
입력으로는 배열 객체를 받으며, 배열 뒤에 삽입한 컨텐츠일수록 위쪽(ppt의 맨 앞에 두기와 같은 기능)에 나타난다.
컨텐츠는 특정 위치를 지정하여 삽입할 수도 있지만, gravity 옵션으로 정중앙, 오른쪽 등 적절한 위치에 알아서 배치되게 만들 수도 있다. (https://sharp.pixelplumbing.com/api-composite)
이미지 리사이징 api 서비스 만들기
티스토리(다음) 블로그에서는 이미지를 리사이징 해주는 api 서비스를 공짜로 제공한다.
아래의 링크를 브라우저에 띄우면 실제로 리사이즈 된 이미지가 화면에 출력된다.
//i1.daumcdn.net/thumb/C100x100/?fname=[이미지url]
이 이미지 리사이징 api 서비스를 직접 Node(express)로 구현해보자 (정말 간단하다!!)
우선 다음 패키지들을 설치해준다.
> npm i express sharp axios
const express = require('express');
const sharp = require('sharp');
const axios = require('axios');
const app = express();
// ... 라우팅 코드
app.listen(8081, () => {
console.log('8081번 포트에서 대기중');
});
이미지 리사이징 라우팅
http://localhost:8081/thumb/500x700?fname=[이미지url]
형태로 api 요청을 하게되면 이미지가 500x700 사이즈로 강제조정 되는 서비스를 구현해보자.
//? http://localhost:8081/thumb/500x700?fname=[이미지url]
app.get('/thumb/:size', async (req, res) => {
//* 리사이징 설정
const quality = 100; // 이미지 퀄리티 (기본 80)
const [size1, size2] = req.params.size.split('x'); // [500, 700]
const ext = req.query.fname.split('.')[req.query.fname.split('.').length - 1]; // 파일 포맷
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp 에서는 jpg 말고 jpeg를 사용하기에 조치
console.log('리사이징 크기 : ', req.params.size);
console.log('원본 이미지 포맷 : ', requiredFormat);
//* 이미지 가져오기
const img = await axios
.get(req.query.fname, { responseType: 'arraybuffer' })
.then((response) => Buffer.from(response.data));
//* 이미지 리사이징
const imgCompress = await sharp(img)
.resize(parseInt(size1), parseInt(size2), { fit: 'contain' }) // fit : contain 가로 세로 비율을 강제 유지
.withMetadata() // 원본 이미지의 메타데이터 포함
.toFormat(requiredFormat, { quality }) // 포맷 / 퀄리티 지정
.toFile(`resizeIMG.${requiredFormat}`, (err, info) => {
// 리사이징된 이미지를 로컬에 저장
console.log(`리사이징 이미지 info : ${JSON.stringify(info, null, 2)}`);
})
.toBuffer(); // 리사이징된 이미지를 노드에서 읽을수 있게 buffer로 변환
res.status(200).end(imgCompress); // 리사이징된 이미지를 브라우저에 뜨게 응답
});
resize({ fit 옵션 })
- cover: (기본값) 가로 세로 비율을 유지하고 이미지가 맞게 자르기/잘려 제공된 두 치수를 모두 포함하는지 확인
- contain: 가로 세로 비율을 유지하고 필요한 경우 "레터박스"를 사용하여 제공된 두 치수 모두에 포함
- fill: 입력의 종횡비를 무시하고 제공된 두 치수로 늘인다
- inside: 가로 세로 비율을 유지하면서 이미지의 크기를 지정된 크기보다 작거나 같게 유지하면서 가능한 한 크게 이미지 크기를 조정
- outside: 종횡비를 유지하면서 이미지의 크기가 지정된 두 가지보다 크거나 같도록 최대한 작게 이미지 크기를 조정
다음 이미지 url을 리사이징 서비스를 이용해 조절해 해보자.
http://localhost:8081/thumb/500x700?fname=https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg
이미지 워터마크 라우트
다음 워터마크 이미지 파일을 서버 폴더에 저장해둔다.
//? http://localhost:8081/watermark?filename=[이미지url]
app.get('/watermark', async (req, res) => {
//* 워터 마크 생성하기
const ext = req.query.fname.split('.')[req.query.fname.split('.').length - 1]; // 파일 포맷
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp 에서는 jpg 말고 jpeg를 사용하기에 조치
console.log('원본 이미지 포맷 : ', requiredFormat);
// 이미지를 서버단으로 ajax로 가져오기
const img = await axios
.get(req.query.fname, { responseType: 'arraybuffer' })
.then((response) => Buffer.from(response.data));
const original_img = await sharp(img);
const watermark = await sharp('./watermark.png').toBuffer(); // 로컬에서 워터마크 이미지 불러오기
const watermarked = await original_img
.composite([
{
input: watermark,
gravity: 'southeast', // 워터마크 위치
},
])
.toFile(`watermarkIMG.${requiredFormat}`, (err, info) => {
// 리사이징된 이미지를 로컬에 저장
console.log(`워터마크된 이미지 info : ${JSON.stringify(info, null, 2)}`);
})
.toBuffer(); // 리사이징된 이미지를 노드에서 읽을수 있게 buffer로 변환
res.status(200).end(watermarked); // 리사이징된 이미지를 브라우저에 뜨게 응답
});
다음과 같이 라우터로 요청해보면, 원본 이미지에 반디캠 워터마크가 잘 삽입됨을 확인할 수 있다.
http://localhost:8081/watermark?fname=https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg
이렇게 간단한 이미지 리사이징 api 서버를 구현해 보았다.
하지만 이용자들이 24시간 내내 이미지 리사이징 기능을 사용하는 것도 아니니, 서비스 서버를 24시간 내내 기동시키는 것은 자원 낭비이다.
따라서 보통 이러한 서비스들은 서버리스로 구현한다.
만일 서버리스에 대해 아직 잘 모르신 독자분들은 이번 기회에 한번 공부해보자!
이 글이 좋으셨다면 구독 & 좋아요
여러분의 구독과 좋아요는
저자에게 큰 힘이 됩니다.