티스토리 뷰

 

https://dev-dorydory.tistory.com/5

(요기에 이전 이야기가 적혀있습니다 ㅎㅎ)

 

저는 이미지가 수만개 저장되어있는 플레이스토어, 앱스토어에 배포되어 있는 서비스에서

앞으로 유저들이 업로드하는 이미지 로딩 속도를 줄여야 했습니다!

 

요번에는,

이미 Firebase Storage에 업로드 되어있던 수만개의 이미지들의 URL을 DB에서 조회하여

=> 서버 환경에서 다운받은 후

=> 리사이징하여

=> 썸네일 이미지를 업로드 해야 합니다!

=> 적절한 비동기 처리도 꼭 필요합니다!

 

0. URL들을 DB에서 조회하기

는 SELECT(SQL)문을 사용하여 배열에 담았습니다! (요부분은 매우 간단한 부분이니 PASS!!)

(처음에 코드를 돌릴 때에는 LIMIT문으로 페이징처리를 해서 소수의 이미지들만 가져와 테스트해보며 버그를 고쳤습니당!)

 

추가로, 수만개의 이미지들을 불러와서 다뤘기 때문에 반복문을 사용했으며,

URL에서 파일 이름과 경로를 뽑아왔습니다!

(제 경우에는 원본 이미지 경로 및 파일명과 썸네일 이미지 경로 및 파일명이 같아야 하기 때문입니다.)

async function uploadImage(rows, page) {
	for (const row of rows) {
		if(row.imageUrl === null || row.imageUrl === undefined || row.imageUrl === "" 
					|| row.imageUrl.length === 0){
			continue;
		}
		let path = row.imageUrl.split("?")[0].split("%2F");
		let fileName = path.pop();
		if (fileName.indexOf(".") === -1) {
			fileName = path[2]+"@"+fileName + "_400x400";
		} else {
			fileName = fileName.split(".");
			fileName = path[2]+"@"+fileName[0] + "_400x400." + fileName[1];
		}
		await download(row.imageUrl, fileName, row, path);
	}
	console.log(`finished`);
}

(코드적인 부분은 따로 설명하지 않겠습니다!)

 

다만, fileName에서

  • split("%2F")하여 뽑은 이유는? 
    • Firebase Storage에 저장된 이미지 URL에서는 "%2F"로 폴더를 구분하기 때문입니다.
  • fileName.indexOf(".")로 조건문을 걸어 처리한 이유는? 
    • 이 서비스에서 Firebase Storage에서 저장되어있는 파일 이름에 자료형(.jpeg)이 붙어있을 수도, 아닐 수도 있기 때문입니다.
  • fileName에 "@~~"를 붙여준 이유는?
    • 로컬에 임시 저장하는 파일 이름이 같은 경우 리사이징을 할 때 다른 게시물의 이미지를 리사이징 하거나, 이미지가 손상되는 버그가 있어 고유한 값을 파일 이름에 붙여주었습니다.

(Firebase Storage에 업로드 할 때에는 "@~~"부분을 지워주었습니다!)

 

1. 서버 환경에서 URL로 이미지 다운 받기

여기서는 fs와 request 모듈을 사용하여 이미지를 다운받기로 했습니다!

 

npm install fs
npm install request

npm으로 fs와 request 모듈을 설치합니다.

const fs = require('fs');
const request = require('request');

var download = function(uri, filename, callback){
  request.head(uri, function(err, res, body){
    request(uri).pipe(fs.createWriteStream(filename)).on('close', callback);
  });
};

download('(이미지의 URL)', '(저장할 이미지 파일 이름)', function(){
	// (저장할 이미지 파일 이름)으로 접근하여 이미지를 가져옵니다.
});

요런식으로 사용하는 방식을 채택했으며,

(이미지의 URL)에 저는 DB에서 뽑아온 URL을 넣었고 download 함수를 조금 변형하였습니당!

function download(uri, fileName, row, path) {
    request.head(uri, function (err, res, body) {
         request(uri).pipe(fs.createWriteStream(fileName)).on('close', () => {
             // 이미지가 다운받아진 후 로직을 수행합니다.
         });
    })
}

node.js 서버에서 URL로 이미지를 다운받는 참고 사이트 : 

https://stackoverflow.com/questions/12740659/downloading-images-with-node-js

 

Downloading images with node.js

I'm trying to write a script to download images using node.js. This is what I have so far: var maxLength = 10 // 10mb var download = function(uri, callback) { http.request(uri) .on('response',

stackoverflow.com

 

2. 이미지 리사이징 하기 

여기서는 sharp라는 리사이징 모듈을 사용하여 이미지 리사이징을 하기로 했습니다.

npm install sharp

npm으로 sharp 모듈을 설치해줍니다.

sharp('input.jpg')
  .resize(400, 400)
  .toFile('output.jpg', function(err) {
    // output.jpg is a 300 pixels wide and 200 pixels high image
    // containing a scaled and cropped version of input.jpg
});

요런식으로 사용하는 방식을 채택했으며,

height : 400, width : 400은 제가 임의로 설정한 부분입니다.

(저는 400*400의 썸네일을 쓸 예정이라서,,,)

sharp(fileName)
	.resize({height: 400, width: 400})
    	.toFile('output_image', (err, info) => {
	// 이미지를 리사이징한 후 로직을 수행합니다.
});

sharp module의 참고 레퍼런스 : 

https://sharp.pixelplumbing.com/api-constructor

 

sharp - High performance Node.js image processing

 

sharp.pixelplumbing.com

 

3. 썸네일 이미지를 Firebase Storage에 업로드 하기

많은 레퍼런스들이 gcloud를 사용하라고 하는데 알아보기로는 deprecated 되어 저는 firebase-admin 모듈을 사용하였습니다!

npm install firebase-admin

npm으로 firebase-admin 모듈을 설치합니다.

const admin = require("firebase-admin");
const uuid = require('uuid-v4');

var serviceAccount = require("(serviceAccountKey.json의 경로)");

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    storageBucket: "(버킷 이름).appspot.com"
});

const bucket = admin.storage().bucket();

var filename = "(파일의 경로 및 이름)"

async function uploadFile() {
  	const metadata = {
      metadata: {
        firebaseStorageDownloadTokens: uuid()
      },
      contentType: 'image/png',
      cacheControl: 'public, max-age=31536000',
  	};

  	await bucket.upload(filename, {	
      gzip: true,
      metadata: metadata,
  	});
	console.log(`${filename} uploaded.`);
}

uploadFile().catch(console.error);

썸네일 이미지의 경로를 따로 설정해줘야 하기 때문에 이 방식에서 path를 설정할 수 있는 구조로 조금 변경했습니다!

(저는 썸네일 폴더를 만들어서 그 안에 파일을 만들어주었습니다.)

const admin = require("firebase-admin");
const serviceAccount = require("(프로젝트설정>서비스계정>새비공개키생성 으로 다운받은 키의 경로)");

admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    storageBucket: "gs://(버킷 이름).appspot.com"
});
const bucket = admin.storage().bucket();

async function uploadFile() {
	const metadata = {
		metadata: {
			firebaseStorageDownloadTokens: uuid()
		},
		contentType: 'image/jpeg',
		cacheControl: 'no-cache',
	};
	// 옵션으로 경로 설정
	await bucket.upload('output_image', {
		gzip: true,
		metadata: metadata,
		destination: "post/" + paramPath[1] + "/" + paramPath[2] 
        			+ "/thumbnails/" + paramFileName.split("@")[1],
	});
	// 이미지 업로드 완료시 이곳으로 옵니다!
	console.log(`${paramRow.postIdx} ${paramRow.imageOrder} uploaded.`);
}

(뒤에서 @를 split해주는 이유는 0번에서 설명드렸듯이 @로 고유한 파일 이름을 만들어줬기 때문입니다!)

저는 수많은 이미지들을 다뤘기 때문에 console log로 게시물 번호와 이미지 순서를 찍어주었습니다!

이 과정 덕분에 중간중간에 에러가 났을 때 다시 처음부터 시작하는 것이 아니라 해당 에러난 부분부터 시작할 수 있었답니다~~! 호호

 

Firebase Storage에 이미지를 업로드하는 참고 사이트 : 

https://stackoverflow.com/questions/60922198/firebase-storage-upload-image-file-from-node-js

 

Firebase Storage upload image file from Node.js

Please help I receive images from the client and save it on my server in the file system and process this image, after which I need to upload it to firebase storage I try upload image file to fir...

stackoverflow.com

 

4. 비동기 처리하기

제일 어려웠던 이 부분!! 

(제가 노드나 자바스크립트를 깊게 판 적이 없어서 처음 다뤄보는 부분이었습니다ㅠㅠ)

 

제 경우에는 반복문인 for문 안에서 어떠한 기능을 하는 부분을 기다렸다가 그 다음으로 넘어가야 했습니다.

저는 async/await, Promise 함수를 사용하였습니다! 

async function uploadImage(rows, page) {
	...
	for (const row of rows) {
		...
		await download(row.imageUrl, fileName, row, path);
		...
	}
    ...
}

function download(uri, paramFileName, paramRow, paramPath) {
	return new Promise((resolve) => {
		...
		resolve('Success');
	});
}
  • for문이 포함된 함수에 async를 걸어주고,
  • for문 안에 있는 download라는 함수가 완료되어야만 다음으로 넘어갈 수 있어서 해당 함수를 호출하는 부분에 await를 걸어주고,
  • 완료까지 기다려야 하는 download 함수를 Promise로 감싸주어 완료되는 시점에 resolve를 걸어줍니다!

(resolve에 도달하는 때에 다음으로 넘어갑니다.)

 

이 부분 말고도 async와 await를 사용한 코드도 첨부합니다.

async function uploadFile() {
	...
	await bucket.upload('output_image', {
		...	
	});
}

 

자바스크립트 비동기 연산 참고 자료 : 

https://ebbnflow.tistory.com/245

 

[NodeJS] 자바스크립트 비동기 연산을 다루는 async/await

자바스크립트의 비동기를 다루는 async/await 콜백함수의 콜백지옥을 탈출하게 해주는 Promise, 그리고 또 Promise의 단점을 보완해주는 async/await. async/await은 Promise와 다른 개념이 아니고 Promise를 사..

ebbnflow.tistory.com

 

더보기

풀 코드 요깄습니댱!

const {pool} = require('../../../config/database');
const {logger} = require('../../../config/winston');
const admin = require("firebase-admin");
const fs = require('fs');
const request = require('request');
const sharp = require('sharp');
const uuid = require('uuid-v4');
const serviceAccount = require("(json파일의 경로)");

function download(uri, fileName, row, path) {
    return new Promise((resolve) => {
        request.head(uri, function (err, res, body) {
            request(uri).pipe(fs.createWriteStream(fileName)).on('close', () => {
                sharp(fileName).resize({height: 400, width: 400}).toFile('output_image', (err, info)=>{
                    const bucket = admin.storage().bucket();
                    async function uploadFile() {
                        const metadata = {
                            metadata: {
                                firebaseStorageDownloadTokens: uuid()
                            },
                            contentType: 'image/jpeg',
                            cacheControl: 'no-cache',
                        };
                        // 옵션으로 경로 설정
                        await bucket.upload('output_image', {
                            gzip: true,
                            metadata: metadata,
                            destination: "(저장하고자 하는 경로)/" + fileName.split("@")[1],
                        });
                        console.log(`${row.postIdx} ${row.imageOrder} uploaded.`);
                    }
                    uploadFile().catch(console.error);
                });
                resolve('success');
            });
        })
    });
}
admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
    storageBucket: "gs://(버킷 이름).appspot.com"
});

exports.uploadThumbnail = async function (req, res) {
    try {
        const connection = await pool.getConnection(async conn => conn);
        try {
            const [rows] = await connection.query(
                `
                    SELECT postIdx, imageOrder, imageUrl
                    FROM PostImage 
                        ORDER BY postIdx ASC, imageOrder ASC 
                    ;`
            );
            connection.release();
            uploadImage(rows);
            return res.json(rows);
        } catch (err) {
            logger.error(`example Query error\n: ${JSON.stringify(err)}`);
            connection.release();
            return false;
        }
    } catch (err) {
        logger.error(`example non transaction DB Connection error\n: ${JSON.stringify(err)}`);
        return false;
    }

}

async function uploadImage(rows, page) {
    for (const row of rows) {
        if(row.imageUrl == null || row.imageUrl == "" || row.imageUrl.length == 0){
            continue;
        }
        let path = row.imageUrl.split("?")[0].split("%2F");
        let fileName = path.pop();
        if (fileName.indexOf(".") === -1) {
            fileName = path[2]+"@"+fileName + "_400x400";
        } else {
            fileName = fileName.split(".");
            fileName = path[2]+"@"+fileName[0] + "_400x400." + fileName[1];
        }
        await download(row.imageUrl, fileName, row, path);
    }
    console.log(`finished`);
}
댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/02   »
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
글 보관함