일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 |
29 | 30 | 31 |
- Sharp
- 이미지서버
- Bucket
- 노선
- nodejs
- ProudNet
- AWS
- 리사이즈
- multiparty
- 스트리밍서버
- Thumbnail
- lightsail
- 지하철역
- 이미지
- 게임서버
- 버킷
- 데이터
- Node
- 튜토리얼
- 샘플링
- 디지털영상
- 화소
- resize
- S3
- 시작하기
- streaming
- 이미지프로세싱
- 프라우드넷
- 아날로그영상
- 좌표
- Today
- Total
Deep Studying
Node js와 AWS로 이미지 서버 만들기 (3) 이미지 리사이즈, 썸네일 생성 본문
이전 포스트: Node.js와 AWS로 이미지 서버 만들기 (2) 이미지 로드와 업로드
지난 포스트에서는 AWS bucket에 이미지를 업로드하고 로드할 수 있는 라우터를 만들어보았습니다.
이번 포스트에서는 더 나아가 이미지를 리사이즈하거나 썸네일을 만들거나
혹은 이런 여러 작업을 동시에 처리하여 업로드하도록 코드를 개선해보겠습니다.
이번 내용을 이해하기 위해서는 Promise에 대한 이해가 필요합니다.
이전 포스트의 코드에서 sharp 패키지를 추가해야합니다.
npm install --save sharp
이제부터는 imageRouter.js의 내용을 차례로 개선하는 과정을 보이려합니다.
설명이 필요 없다면 바로 4번으로 넘어가 코드만 확인하실 수 있습니다.
0. AWS로 업로드하는 부분은 함수로
이미지를 리사이즈하고, 썸네일을 만들고 하게되면 업로드하는 코드가 계속 중복되어 보기가 불편해집니다.
function uploadToBucket(filename, Body){
const params = { Bucket:BUCKET_NAME, Key:filename, Body, ContentType: 'image' }
const upload = new AWS.S3.ManagedUpload({ params });
return upload.promise()
}
위와 같은 함수로 만들어줍니다.
그러면 part 이벤트핸들러의 내용은 아래와 같이 쓸 수 있습니다.
form.on('part', function(part){
if(!part.filename)
return part.resume()
const filename = part.filename // 버킷에 올라갈 디렉토리+파일의 이름
uploadToBucket(filename, part)
part.on('end', function(){
// 파일 업로드 후 실행할 추가 코드
// ...
// ...
})
part.on('error', function(err){
console.log(err)
})
})
1. 이미지 Stream을 Buffer로
이미지를 변형하기 위해서는 이미지의 모든 데이터를 가지고 있어야합니다.
따라서 이미지 스트림에서 정보를 가져올 필요가 있습니다.
function streamToBuffer(part, filename){
const chunks = [];
return new Promise((resolve, reject) => {
part.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
part.on('error', ( err ) => reject(err));
part.on('end', ( ) => resolve(Buffer.concat(chunks)));
part.resume()
})
}
data 이벤트 핸들러는 이미지 chunk를 받을 때 마다 호출됩니다. 따라서 chunks 라는 배열에 차곡차곡 chunk데이터를 누적한 뒤에 end 이벤트 핸들러가 호출되면 그동안 모았던 데이터를 합쳐 다음 Promise로 resolve해줍니다.
지난 포스트에서 언급한 part.resume( )을 실행하면 part stream에 데이터가 흐르기 시작하며 chunk를 받을 수 있게 됩니다.
여기서 잠깐, part.resume( )을 사용하면 원본 데이터가 chunk에 모두 담기긴 하지만 이를 다시 Bucket에 업로드 해주려고 하니 조금 비효율적인 것 같기도 합니다. 어차피 읽어야할 stream이라면 그대로 Bucket에 흐르게 두어 원본 업로드에 사용해도 괜찮습니다.
function streamToBufferUpload(part, filename){
const chunks = [];
return new Promise((resolve, reject) => {
part.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
part.on('error', ( err ) => reject(err));
part.on('end', ( ) => resolve(Buffer.concat(chunks)));
uploadToBucket(filename, part)
})
}
원본 이미지도 업로드해야한다면 streamToBufferUpload 함수를 사용하고 원본 이미지를 파기해야한다면 streamToBuffer 함수를 사용하면 됩니다. 두 함수 모두 resolve되는 값은 동일하니 용도에 맞게 선택하시면 됩니다.
2. sharp 사용하기
sharp는 javascript에서 간단하게 이미지를 변형할 수 있는 라이브러리입니다.
이 포스트에서는 이미지를 리사이즈하고, 정사각형으로 자르는 기능만 사용해보겠습니다.
https://github.com/lovell/sharp
그 외에 회전하기, 흑백으로 만들기, 배경색 넣기 등 기능의 사용법은 위 링크를 참고하시기 바랍니다.
2-1. 이미지 리사이즈
function makeSmallImage(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(320)
.jpeg({mozjpeg:true})
return uploadToBucket('small/'+filename, Image)
}
sharp는 파라미터로 이미지 버퍼를 받습니다. 위에서 작성한 streamToBuffer 함수로 얻은 버퍼를 data 파라미터에 넣어 주어야 합니다.
두 번째 인자에 { failOnError:false } 옵션은 넣는 이유는 삼성 핸드폰과의 호환때문입니다. 삼성 펌웨어가 validate된 jpeg 파일을 생성하지 못하게 막아 sharp 라이브러리와 충돌을 일으키는 경우가 간혹 발생합니다. 이를 해결하기 위한 옵션입니다. 아이폰이나 웹 환경에서는 안넣어도 큰 차이가 없는것으로 알고있습니다. ( 혹시 다른 이유가 더 있다면 알려주시기 바랍니다 )
해당 이슈: https://github.com/lovell/sharp/issues/1578
withMetadata( ) 함수는 원본 이미지에서 메타데이터도 함께 가져오기 위해 사용하는 이미지입니다. 이 함수를 넣지 않으면 핸드폰으로 찍은 사진들이 90도 회전되어 보이는 경우들이 발생할 수 있습니다. 특별한 이유가 있지 않다면 모두 넣어줍시다.
핸드폰 카메라로 사진을 찍을 때, 가로와 세로를 전환하면서 찍게 됩니다. 이 때, 핸드폰 카메라 앱마다 이미지를 적절한 방향으로 회전시켜 저장해주는 경우가 있고, 이미지는 그대로 두되 메타데이터에 ratation에 대한 정보를 추가하여 저장해주는 경우도 있습니다. 따라서 메타데이터를 지우면 사진이 90도 회전하는 경우가 종종 발생하기도 합니다.
resize( ) 함수는 파라미터가 한 개 일 때, 긴 변의 길이를 파라미터 숫자에 맞추어 리사이즈합니다.
위의 함수처럼 .resize( 320 )으로 변환하면
( 1000 x 500 ) 의 이미지는 ( 320 x 160 ) 이 되고
( 1000 x 2000 ) 의 이미지는 ( 160 x 320 ) 이 됩니다.
jpeg( ) 함수는 모든 promise chain을 거쳐 나온 결과를 jpeg 포맷으로 변환하여 stream으로 만들어주는 역할을 합니다. 따라서 위 함수에서는 Image 변수의 결과가 readable한 stream이 되는 셈입니다. mozjpeg 옵션은
png( ) 함수도 있습니다. jpeg( )와 동작은 같고 결과를 png 포맷으로 생성해줍니다.
2-2. Thumbnail 만들기
function makeThumbnail(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(170,170)
.jpeg({mozjpeg:true})
return uploadToBucket('thumb/'+filename, Image)
}
코드의 내용은 makeSmallImage( )함수와 거의 유사하고 resize( )의 파라미터가 두 개라는 점만 다릅니다.
resize( ) 함수는 파라미터가 두 개 일 때, 두 변의 길이를 각 파라미터 숫자에 맞춰 이미지를 리사이즈 및 크롭해줍니다.
2-3. 중간 점검
여기까지의 내용을 적용한 코드 중간의 모습은 아래와 같습니다.
router.post('/',function(req, res){
const form = new multiparty.Form()
// 에러 처리
form.on('error', function(err){
res.status(500).end()
})
// form 데이터 처리
form.on('part', function(part){
if(!part.filename)
return part.resume()
const filename = part.filename
streamToBufferUpload(part, filename)
.then( image => makeSmallImage(image, filename) )
})
form.on('close', function(){
// 모든 파일 업로드 후 실행할 추가 코드
// ...
// ...
res.end()
})
form.parse(req)
})
function makeSmallImage(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(320)
.jpeg({mozjpeg:true})
return uploadToBucket('small/'+filename, Image)
}
function streamToBufferUpload(part, filename){
const chunks = [];
return new Promise((resolve, reject) => {
part.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
part.on('error', ( err ) => reject(err));
part.on('end', ( ) => resolve(Buffer.concat(chunks)));
uploadToBucket(filename, part)
})
}
function makeThumbnail(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(170,170)
.jpeg({mozjpeg:true})
return uploadToBucket('thumb/'+filename, Image)
}
두 가지 변화가 눈에 띄실텐데요
첫 번째는 streamToBufferUpload( ) 함수 뒤에 promise Chain으로 방금 만든 makeSmallImage( )가 붙었습니다.
두 번재로, part의 이벤트 핸들러들이 사라졌습니다. 이 내용들은 streamToBuffer( ) / streamToBufferUpload( ) 함수 안에 part.on(' ... ')의 형태로 들어왔으므로 두 번 작성할 필요는 없기 때문에 삭제하는 것이 맞습니다.
다시 http://localhost:3000/upload를 켜신 뒤 이미지를 업로드해보겠습니다.
sample2.jpg라는 파일 한 개를 업로드 했습니다.
그 결과 sample2.jpg 파일이 업로드 되었으며 ( streamToBufferUpload 함수를 사용했으므로 ) sample 디렉토리가 생성되고 그 안에 리사이즈된 sample2.jpg가 업로드 되었습니다. ( 경로를 'small/<filename>'으로 지정했기 때문에 )
그 다음에 makeSmallImage를 makeThumbnail로 변경하면 이미지가 업로드되는 동시에 썸네일이 생성될 것입니다.
3. 다수의 이미지 프로세스 병렬 처리
이미지 업로드를 하면서 리사이즈 이미지도 같이 업로드 하는 것은 좋았습니다. 하지만 썸네일도 같이 만들어야 한다던가 여러 사이즈별로 이미지를 만들어야한다던가 몇 개의 작업이 추가될지 장담할 수 없습니다. 또한 이 모든 작업들을 Promise chain으로 연결해서 하나씩 처리하는 것은 확장성에서도 별로 좋지 않아보입니다. 이를 간단하게 개선해봅시다.
처리해야할 이미지 프로세스 작업을 일일히 코드로 적기보다 List에 넣어 한 번에 처리하면 편리할 것 같습니다.
function processImage(image, filename, functions=[]){
if(!functions.length) return
const promiseList = functions.map( item => item(image, filename))
return Promise.all(promiseList)
}
functions.map( )의 item은 각각이 promise를 리턴하는 함수여야 합니다.
각각의 함수들을 실행시켜놓고 Pending된 promise들을 promiseList에 담아둡니다.
Promise.all( )에 promiseList를 넣어줌으로써 이 모든 작업이 끝나야지만 processImage 함수가 resolve됩니다.
이를 적용한 라우터의 모습은 아래와 같습니다.
router.post('/',function(req, res){
const form = new multiparty.Form()
// 에러 처리
form.on('error', function(err){
res.status(500).end()
})
// form 데이터 처리
form.on('part', function(part){
if(!part.filename)
return part.resume()
const filename = part.filename
// 처리해야할 이미지 프로세스 작업 함수들
const processFunctions = [makeSmallImage, makeMiddleImage, makeThumbnail]
streamToBufferUpload(part, filename)
.then( image => processImage(image, filename, processFunctions) )
})
// form 종료
form.on('close', function(){
res.end()
})
form.parse(req)
})
function makeMiddleImage(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(640)
.jpeg({mozjpeg:true})
return uploadToBucket('middle/'+filename, Image)
}
makeSmallImage 와 makeThumbnail 함수는 위에서 만든 함수이며, makeMiddleImage는 makeSmallImage에서 resize( )함수의 파라미터와 파일 경로만 바꾼 함수입니다.
이로써 이미지 업로드와 동시에 원하는 만큼의 이미지 프로세스 작업을 처리할 수 있게 되었습니다. 원하는대로 함수를 만들어 붙이면 모두 병렬적으로 처리될 것입니다. 물론 함수를 만드는 함수를 선언해서 더욱 체계적으로 관리하는 것도 가능합니다. 하지만 포스트가 너무 길어진 관계로 여기에 적지는 않겠습니다.
4. 최종 정리
이전 포스트에서 이어서
npm install --save sharp
imageRouter.js
const router = require('express').Router()
const multiparty = require('multiparty')
const url = require('url')
const AWS = require('aws-sdk');
const sharp = require('sharp')
const BUCKET_NAME = 'bucket-h3v3bu' // Bucket 이름
const BUCKET_URL = 'bucket-h3v3bu.s3.ap-northeast-2.amazonaws.com' // Bucket 도메인
AWS.config.update({
region: 'ap-northeast-2',
accessKeyId: 'AKIATIIKUHVXRN7EW2MV', // 엑세스 키 ID
secretAccessKey:'0/00kRKGuHR16RHFljQJXgvgrzBUCh7FID7ZMupx' // 엑세스 키
})
router.get('/*', (req,res)=>{
const {pathname} = url.parse(req.url, true)
res.redirect(`https://${BUCKET_URL}${pathname}`)
})
router.post('/',function(req, res){
const form = new multiparty.Form()
// 에러 처리
form.on('error', function(err){
res.status(500).end()
})
// form 데이터 처리
form.on('part', function(part){
if(!part.filename)
return part.resume()
const filename = part.filename
const processFunctions = [makeSmallImage, makeMiddleImage, makeThumbnail]
// 두 함수중 한 가지 택 1할 수 있습니다.
//streamToBuffer(part, filename)
streamToBufferUpload(part, filename)
.then( image => processImage(image, filename, processFunctions) )
.then( ( )=>{ /* 이미지 프로세스가 모두 처리된 후 실행할 내용 */})
})
// form 종료
form.on('close', function(){
res.end()
})
form.parse(req)
})
function uploadToBucket(filename, Body){
const params = { Bucket:BUCKET_NAME, Key:filename, Body, ContentType: 'image' }
const upload = new AWS.S3.ManagedUpload({ params });
return upload.promise()
}
function streamToBuffer(part, filename){
const chunks = [];
return new Promise((resolve, reject) => {
part.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
part.on('error', ( err ) => reject(err));
part.on('end', ( ) => resolve(Buffer.concat(chunks)));
part.resume()
})
}
function streamToBufferUpload(part, filename){
const chunks = [];
return new Promise((resolve, reject) => {
part.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
part.on('error', ( err ) => reject(err));
part.on('end', ( ) => resolve(Buffer.concat(chunks)));
uploadToBucket(filename, part)
})
}
function processImage(image, filename, functions=[]){
if(!functions.length) return
const promiseList = functions.map( item => item(image, filename))
return Promise.all(promiseList)
}
function makeMiddleImage(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(640)
.jpeg({mozjpeg:true})
return uploadToBucket('middle/'+filename, Image)
}
function makeSmallImage(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(320)
.jpeg({mozjpeg:true})
return uploadToBucket('small/'+filename, Image)
}
function makeThumbnail(data, filename){
const Image =
sharp(data, {failOnError:false})
.withMetadata()
.resize(170,170)
.jpeg({mozjpeg:true})
return uploadToBucket('thumb/'+filename, Image)
}
module.exports = router
'웹서버' 카테고리의 다른 글
Node.js로 음악, 동영상 스트리밍 서버 만들기 (0) | 2021.09.04 |
---|---|
Node js와 AWS로 이미지 서버 만들기 (2) 이미지 로드와 업로드 (0) | 2021.08.28 |
Node js와 AWS로 이미지 서버 만들기 (1) AWS 버킷 세팅하기 (0) | 2021.08.19 |