20230309_TIL

오늘 한 일(회고)
- 프로그래머 스쿨 알고리즘 코딩입문 Day4 공부(보류)
- 프로젝트 api 기능 구현
현재 내가 하는 프로젝트 기능 구현 중 doctor 테이블과 hospital 테이블에 여러 데이터를 담아야 하는데 그중 image 파일도 저장을 해야한다. 예전 프로젝트를 구현을 했을 때(내가 아니라 다른 조원분이 맡았었다.) sequelize 모델에 이미지를 저장하고 불러오는 방식으로 구현을 했었다.
나는 이번에 처음으로 구현하는 기능이다보니 일단 열심히 sequelize에 image를 upload 하는 방법을 서칭했다.
일단 조사한 결과로는 크게 3가지 방법이 있는 듯 하다..
-
Sequelize 모델에 BLOB(Binary Large Object) 데이터 타입으로 image 칼럼을 추가하고 multer를 사용해서 multipart/form-data를 처리할 수 있는 미들웨어를 사용하여 이미지를 업로드 하는 방법이 있는 것 같다.
-
이거는 전에 했던 조원이 사용한 방법인데 Sequelize 모델에 image 칼럼을 데이터 타입을 stirng으로 했었다. 작동방식이 mysql DB에는 실제 이미지가 저장 되는 것이 아니였고, 서버에 지정한 경로를 저장해서 서버에서 불러오거나 저장하는 방식을 사용했었다. multer의 storage 기능을 사용한 것으로 보였다.
-
그리고 마지막으로 찾은 것은 실제 서비스에서는 보안상의 이슈로 이미지를 서버에 저장하지 않고, 클라우드 스토리지 서비스를 이용해서 처리한다고 한다. AWSS3, Google Cloud Storage, Microsoft Azure Blob Storage 등의 클라우드 스토리지 서비스를 사용하는데, 이미지 업로드, 불러오기를 안전하고 빠르게 처리할 수 있다고 한다.
작동방식을 보니 일반적으로 서버에서 클라이언트가 업로드한 이미지를 임시 폴더에 저장한 뒤 클라우드 스토리지 서비스로 전송한다. 이 때 이미지를 전송하기 전에 이미지 리사이징(이미지 사이즈 조절이라는 뜻), 압축,포맷 변환 등의 작업을 수행하여 이미지 처리 성능을 향상 시킬 수 있다. 또 확장성, 유연성도 향상 시키고 클라우드 스토리지 서비스는 대부분 REST API를 제공하고 있어 서버가 아닌 클라이언트에서도 이미지를 직접 업로드하고 불러올 수 있다. 즉 장점이 많다~!!
나는 일단 처음 구현하기도 하니 첫번째부터 한번 테스트 해보기로 했다.
첫번째 방법
예전에 만들었던 간단한 게시판이다. 여기에 POST 테이블에 image column를 추가해보자.

- migrations 파일을 만들기
아래 명령어를 입력해보자!!
npx sequelize migration:generate --name add-column
migrations 폴더에 날짜-add-column.js 파일이 생성이 된다.

- migrate 하기
먼저 날짜-add-column.js 파일에 들어가서 아래와 같이 추가를 해준다.
'use strict';
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up (queryInterface, Sequelize) {
/**
* Add altering commands here.
*
* Example:
* await queryInterface.createTable('users', { id: Sequelize.INTEGER });
*/
queryInterface.addColumn(
'posts', // column을 추가할 테이블 이름
'image', // colunm 추가할 칼럼 이름
{
type: Sequelize.BLOB,
allowNull: true, // Null 값을 허용할지 말지 결정함, 만약 false로 지정하면 값이 Null인 경우 값을 DB에 저장하지 않는다.
}
)
},
async down (queryInterface, Sequelize) {
/**
* Add reverting commands here.
*
* Example:
* await queryInterface.dropTable('users');
*/
}
};
models 폴더에 post.js도 수정해주어야 한다.!! image 부분을 추가 해 줘야한다.
'use strict';
const {
Model
} = require('sequelize');
module.exports = (sequelize, DataTypes) => {
class Post extends Model {
/**
* Helper method for defining associations.
* This method is not a part of Sequelize lifecycle.
* The `models/index` file will call this method automatically.
*/
static associate({User, Comment, Like }) {
// define association here
// Userid => userId camelCase
this.belongsTo( User, { foreignKey: 'userId', as: 'user' });
this.hasMany(Comment, { foreignKey: 'postId', as: 'comments'});
this.hasMany(Like, { foreignKey: 'postId', as: 'likes' });
}
}
Post.init({
title:{
type: DataTypes.STRING,
allowNull: false,
},
content:{
type: DataTypes.STRING,
allowNull: false,
},
userId:{
type: DataTypes.INTEGER,
allowNull: false,
},
image:{
type: DataTypes.BLOB('long'),
allowNull: true
}
}, {
sequelize,
tableName: 'posts',
modelName: 'Post',
});
return Post;
};
이후 아래와 같이 명령어를 입력해준다.
npx sequelize db:migrate
그러면 아래와 같이 성공적으로 posts 테이블에 칼럼이 추가된 것을 알 수 있다.
posts 테이블


이제 테이블은 완성이 되었으니 이미지를 업로드하고 데이터베이스에 저장할 수 있는 라우터를 작성해야한다.
이 라우터는 ‘multer’ 라이브러리를 사용할 것이다.
npm i multer // multer 라이브러리를 설치해주자!!
routes/posts.js 코드
//posts.js 파일
//이미지 업로드
const multer = require('multer');
const upload = multer();
router.post('/:id/image', upload.single('image'), async (req, res) => {
const post = await Post.findByPk(req.params.id);
if (!posts) {
return res.status(404).send('Post not found');
}
post.image = req.file.buffer;
await post.save();
res.send(post.image);
});
에러1
이런 식으로 코드를 짠 후에 테스트는 ThunderClient 를 이용했다.
TypeError: Cannot read properties of undefined (reading 'buffer')
이런 에러가 계속해서 발생했다. 그래서 buffer 문제인가 싶어서 Nodejs buffer 공식 문서를 뒤져봤지만… 해결할 수 없었다. 구글링을 해보니 대충 buffer 문제가 아니라 req.file 객체가 ‘undefined’ 이므로 req.file.buffer 호출 할 수 없어 TypeError가 발생 했다는 것을 깨닫게 되었다.
그러면 왜 undefined일까??
일단 생각 했을 때 클라이언트가 데이터를 제대로 안 보냈기 때문에 이런 에러가 발생했을 거라고 생각을 했고, 중요한 사실을 알게되었다. 바로 올바르지 않은 헤더를 보냈기 때문에 발생한 문제였다. multer에 대해 다시 검색해보니 전송을 할 때 Content-Type 헤더를 multipart/form-data 넣어줘야 한다는 것을 찾았다…
에러2
Error: Multipart: Boundary not found
바로 다음 문제가 발생했다.
구글링을 해보니 Multipart 요청은 여러 개의 데이터 조각들을 하나의 요청으로 보내는 방식인데 이때 각각의 데이터 조각들은 boundary로 구분된다고 한다.
이 에러는 ‘boundary’ 값을 제대로 설정하지 않아서 생겨난 문제인데 즉, 각각의 데이터 조각을 구분해줘야 한다.
해결방법은 클라이언트에서 보내는 멀티파트 요청의 Boundary 값을 확인하고, 이 값을 서버에서 받아들일 수 있는 형식으로 설정해야한다.
thunder Client 에서는 body에 boundary 값을 넣는 부분이 없어서 어쩔 수 없이 REST Client를 사용하기로 했다. thunderclient 에서 boundary 넣는 부분을 아무리 찾아봐도 잘 모르겠다…
에러3
Error: Unexpected end of form at Multipart._final
이후 Thunder client 말고 REST client를 사용해서 boundary 값을 설정 해주었는데 이런 에러가 발생했다…
또 찾아보니 Content-Length 헤더가 없어서 서버가 요청의 길이를 알 수 없어 발생한 에러인 것 같다. 그러면 Content-Length 즉 데이터의 길이를 설정해서 같이 보내줘야하는데 대부분 HTTP 클라이언트 라이브러리는 요청한 본문의 크기를 자동으로 계산해서 보내준다고 한다.근데 나는 서버에서 multer를 사용해서 업로드를 처리 했기 때문에 Content-Length가 자동으로 계산되는게 맞다. 그렇지만 어찌된게 계속해서 같은 에러가 발생했고, 그 원인을 찾지는 못했다.
그래서 생각한게 html로 form 텍스트를 만들기로 결정했다.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>이미지 파일 업로드 테스트</title>
</head>
<body>
<form id="upload-form" enctype="multipart/form-data">
<input type="file" name="image">
<button type="submit">업로드</button>
</form>
<script>
const form = document.getElementById('upload-form');
form.addEventListener('submit', (event) => {
event.preventDefault();
const formData = new FormData(form);
const postId =5; // 실제 포스트 ID로 교체해야 함
fetch(`/posts/${postId}/image`, {
method: 'POST',
body: formData
}).then((response) => {
if (response.ok) {
alert('이미지가 업로드 되었습니다.');
} else {
alert('이미지 업로드에 실패했습니다.');
}
}).catch((error) => {
console.error(error);
});
});
</script>
</body>
</html>
이런식으로 클라이언트 부분을 코딩해주었고, 성공적으로 업로드가 된 것을 확인 할 수 있었다.
결과

이렇게 bird.jpg가 제대로 업로드 된 것을 db에서 확인할 수 있었다.
router.get('/:id/image', async (req, res) => {
const post = await Post.findByPk(req.params.id);
if (!post) {
return res.status(404).send('Post not found');
}
if (!post.image) {
return res.status(404).send('Post image not found');
}
res.set('Content-Type', 'image/jpeg');
res.send(post.image);
});
DB에서 다시 이미지를 불러올려면 위에 코드를 실행시켜보자.
REST client에서 테스트를 진행했고 결과는 성공적이었다.

BLOB (Binary Large Object) 타입 데이터 저장은 단점이 있는데,
-
용량제한 BLOB 타입은 대부분 데이터베이스에서 최대 크기를 제한한다. 만약 이미지 파일의 용량이 매우 큰 경우 문제가 된다. 또 대용량 이미지 파일을 저장할 경우 데이터베이스 용량이 불필요하게 증가하는 경우가 생긴다.
-
성능 저하 BLOB 타입으로 저장된 데이터는 일반적으로 디스크에서 읽어온다고 한다. DB의 성능에 영향을 미칠 수 있고 쿼리할 때도 성능이 저하될 수 있다
-
백업/복구 백업 및 복구 작업이 더 어려울 수 있다. DB의 백업 파일이 커지고, 복구 시간이 늘어날 수 있다.
-
이미지 처리 기능의 제한 이미지 리사이징, 썸네일 생성 등의 기능을 구현할 때 BLOB 타입으로 저장된 이미지 데이터를 사용할 경우 추가적인 작업이 필요하다.
결국 BLOB 타입으로 저장하는 간단하고 빠르지만, 성능면에서 특히 백업/복구등에서 문제가 생길 수 있다. 그래서 두번째 방법인 이미지 데이터를 파일 형태로 저장하고 파일 경로를 DB에 저장하는 방식을 사용하는게 일반적이다.
두번째 방법
두번째 방법을 사용하기 위해서는 multer의 storage 옵션을 이용하여 저장 경로를 지정을 할 수 있다.
const multer = require('multer');
const upload = multer({ storage: storage });
//업로드 기능
const storage = multer.diskStorage({
destination: function(req, file, cb) {
cb(null,'image/') // 실제 서버(폴더) 저장될 경로 지정
},
filename: function(req, file, cb) {
cb(null, file.fieldname + '-' + Date.now()) //저장될 이름 설정(현재시간)
}
});
router.post('/:id/image',upload.single('image'), async(req, res) => {
const post = await Post.findByPk(req.params.id);
if(!post) {
return res.status(404).send('Post no found');
}
post.image = req.file.path; //db에 경로가 저장될 수 있도록 지정
await post.save();
res.send('image uploaded');
})
코드는 이런식으로 구현을 하였고, 테스트를 해보자. 위의 코드를 나도 정확히는 이해하지는 못했지만 약간 느낌적인 느낌만 가지고 있다.

성공적으로 db에도 경로가 저장이 되었다.
그렇다면 이것을 다시 불러오는 api를 작성해보자.
const fs = require('fs');
router.get('/:id/image', async (req, res) => {
const post = await Post.findByPk(req.params.id);
if (!post) {
return res.status(404).send('Post not found');
}
// 저장된 경로를 사용하여 파일을 읽어옴
const imagePath = post.image;
fs.readFile(imagePath, (err, data) => {
if (err) {
console.error(err);
return res.status(500).send('Internal Server Error');
}
// 읽어온 데이터를 응답으로 보냄
res.writeHead(200, {'Content-Type': 'image/jpeg'});
res.end(data);
});
});
코드 해석을 간략하게 해석 해보면 fs 모듈의 readFile 함수를 이용해서 post.image에 저장된 이미지 파일을 읽어온다. 읽어온 데이터는 res.end함수로 응답하여 보내주는 방식이다.

이렇게 결과가 제대로 나오는 것을 REST client를 통해 테스트해서 성공하였다.
공부하면서 참고한 블로그
내일 할일
- 네이버 클라우드 스토리지 사용해보기
- 클라우드 스토리지 이용해서 이미지 저장 구현해보기