기존 블로그에 작성했던 포스트를 이전한 글입니다.
해당 포스트는
NODEJS
를 학습하며 정리한 내용에 대한 포스트입니다.
🌈 서버리스 노드 개발
💻 서버리스 이해하기
서버리스(serverless, server+less)
서버가 없다는 뜻이지만 서버가 없는 것은 아니고, 서버를 직접 운영하지 않아도 된다는 뜻
개발자는 자신의 서비스 로직 작성에만 집중할 수 있음
단순히 코드를 업로드한 뒤, 사용량에 따라 요금을 지불하면 됨(함수처럼 호출할 때만 실행됨, FaaS(Function as a Service))
24시간 작동할 필요가 없는 서버인 경우, 서버리스 컴퓨팅을 사용하면 요금 절약
AWS는 Lambda, GCP에서는 Cloud Functions나 Firebase가 유명함
이를 활용해 NodeBird에서 업로드하는 이미지를 리사이징 및 저장할 것임
💻 AWS S3 사용하기
🍳 AWS S3 사용해보기
스토리지 섹션의 S3를 선택
🍳 버킷 만들기
버킷 만들기나 시작하기 버튼 클릭
🍳 버킷 리전 설정하기
버킷 이름은 고유해야 하므로 고유한 이름을 사용할 것
이름만 정하고 계속 다음 버튼을 눌러 넘어감
권한에서 모든 퍼블릭 액세스 차단 체크박스 해제(실무에서는 해제하지 않는 것이 좋음)
🍳 버킷 생성 확인하기
화면이 뜨면 nodebird 버킷 클릭
🍳 버킷 정책 수정하기
권한 – 버킷 정책 메뉴 선택
- 다음 코드를 입력 후 저장
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AddPerm",
"Effect": "Allow",
"Principal": "*",
"Action": [
"s3:GetObject",
"s3:PutObject"
],
"Resource": "arn:aws:s3:::버킷명/*"
}
]
}
s3:GetObject
는 S3로부터 데이터를 가져오는 권한 s3:PutObject
는 S3에 데이터를 넣는 권한
🍳 액세스 키 발급받기
상단 메뉴에서 계정 이름 클릭 후 내 보안 자격 증명 메뉴 선택
보안 자격 증명으로 계속 버튼 클릭
실 서비스에서는 IAM 사용자 시작하기 버튼 누를것
액세스 키 만들기 버튼 클릭
보안 액세스 키는 다시 볼 수 없으므로 키 파일 다운로드 버튼 눌러 저장
🍳 aws-sdk로 S3 도입하기
multer-s3와 aws-sdk 패키지를 설치한 후 .env에 발급받은 보안 키 기입
1
npm i multer-s3 aws-sdk
🔻.env
1
2
3
4
5
6
7
8
COOKIE_SECRET=nodebirdsecret
KAKAO_ID=5d4daf57becfd72fd9c919882552c4a6
SEQUELIZE_PASSWORD=nodejsbook
REDIS_HOST=redis-18954.c92.us-east-1-3.ec2.cloud.redislabs.com
REDIS_PORT=18954
REDIS_PASSWORD=JwTwGgKM4P0OFGStgQDgy2AcXvZjX4dc
S3_ACCESS_KEY_ID=AKIAID6RLNYHFCZEEODA
S3_SECRET_ACCESS_KEY=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
AWS.config.update로 AWS에 관한 설정을 함(ap-northeast-2는 서울 리전)
multer를 multerS3로 교체함(버킷은 여러분의 버킷명을 사용할 것)
req.file.location에 S3 버킷 이미지 주소가 담겨 있음
🔻 nodebird/routes/post.js
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const express = require('express');
const multer = require('multer');
const path = require('path');
const fs = require('fs');
const { S3Client } = require('@aws-sdk/client-s3');
const multerS3 = require('multer-s3');
const { afterUploadImage, uploadPost } = require('../controllers/post');
const { isLoggedIn } = require('../middlewares');
const router = express.Router();
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const s3 = new S3Client({
credentials: {
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
},
region: 'ap-northeast-2',
});
const upload = multer({
storage: multerS3({
s3,
bucket: 'nodebird03',
key(req, file, cb) {
cb(null, `original/${Date.now()}_${file.originalname}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
// POST /post/img
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);
// POST /post
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);
module.exports = router;
🔻 nodebird/controllers/post.js
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
const { Post, Hashtag } = require('../models');
exports.afterUploadImage = (req, res) => {
console.log(req.file);
res.json({ url: req.file.location });
};
exports.uploadPost = async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
🍳 이미지 업로드 시도하기
http://localhost:8001에 접속하여 로그인 후 이미지 업로드
S3 버킷에 이미지가 업로드된 것 확인
💻 AWS LAMBDA 사용하기
🍳 이미지 리사이징을 위해 람다 사용
이미지 리사이징은 CPU를 많이 사용하기 때문에 기존 서버로 작업하면 무리가 감
Lambda라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징
🍳 람다용 package.json 작성하기
aws-upload 폴더 만든 후 package.json 작성
Lambda라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징 🔻aws-upload/package.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"name": "aws-upload",
"version": "0.0.1",
"description": "Lambda 이미지 리사이징",
"main": "index.js",
"author": "leekoby",
"license": "ISC",
"dependencies": {
"aws-sdk": "^2.634.0",
"sharp": "^0.25.1"
}
}
🔻 aws-upload/.gitignore
1
node_modules
🍳 sharp로 리사이징하기
Sharp는 이미지 리사이징을 위한 라이브러리
exports.handler가 람다 실행 부분
event에 버킷과 데이터 정보가 들어 있음
s3.getObject로 이미지를 버킷에서 가져옴
sharp로 리사이징
resize(가로, 세로, 모드), 모드는 inside(비율 유지하면서 꽉 차게)
toFormat으로 확장자 지정, toBuffer로 버퍼로 변환
S3.putObject로 버킷에 이미지 데이터 저장
callback으로 람다 종료 및 응답 데이터 전달
🔻aws-upload/index.js
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
32
33
34
35
36
37
38
39
40
const sharp = require('sharp');
const { S3Client, GetObjectCommand, PutObjectCommand } = require('@aws-sdk/client-s3');
const s3 = new S3Client();
// 고양이.png
// %CD%AE%AW.png
exports.handler = async (event, context, callback) => {
const Bucket = event.Records[0].s3.bucket.name;
const Key = decodeURIComponent(event.Records[0].s3.object.key); // original/고양이.png
const filename = Key.split('/').at(-1);
const ext = Key.split('.').at(-1).toLowerCase();
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext;
// sharp에서는 jpg 대신 jpeg 사용합니다.
console.log('name', filename, 'ext', ext);
try {
const getObject = await s3.send(new GetObjectCommand({ Bucket, Key }));
const buffers = [];
for await (const data of getObject.Body) {
buffers.push(data);
}
const imageBuffer = Buffer.concat(buffers); // 버퍼로 가져오기
console.log('put', imageBuffer.length);
const resizedImage = await sharp(imageBuffer) // 리사이징
.resize(200, 200, { fit: 'inside' })
.toFormat(requiredFormat)
.toBuffer();
await s3.send(new PutObjectCommand({
Bucket,
Key: `thumb/${filename}`, // thumb/고양이.png
Body: resizedImage,
}))
console.log('put', resizedImage.length);
return callback(null, `thumb/${filename}`);
} catch (error) {
console.error(error);
return callback(error);
}
}
🍳 코드 깃허브로 전송하기
먼저 GitHub에 aws-upload 리파지토리로 올린 후 Lightsail 인스턴스에서 클론
압축 후 S3로 업로드
🍳 람다 서비스 설정하기
🍳 새 함수 만들기
함수 생성 버튼 클릭
함수명은 node-deploy로, 런타임은 Node.js 18.x으로
역할은 템플릿에서 새 역할 생성 선택, S3 객체 읽기 전용 권한 부여
🍳 zip 파일 업로드하기
함수 코드 섹션에서 S3에 올린 파일을 선택
https://버킷명.s3.지역명.amazonaws.com/파일명
🍳 트리거 설정하기
모든 객체 생성 이벤트를 선택하고 접두사에 original/ 누른 후 저장
🍳 NodeBird에 람다 연결하기
기존 original 폴더 부분을 thumb(리사이징 됨) 폴더로 교체
🔻 nodebird/controllers/post.js
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
32
33
const { Post, Hashtag } = require('../models');
exports.afterUploadImage = (req, res) => {
console.log(req.file);
const originalUrl = req.file.location;
const url = originalUrl.replace(/\/original\//, '/thumb/');
res.json({ url, originalUrl });
};
exports.uploadPost = async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
이미지 리사이징이 오래걸려서 리사이징된 이미지가 일정 기간 동안 표시되지 않는 경우를 대비해 img 태그에 onerror 속성을 붙여서 리사이징 이미지를 로딩하는 데 실패하면 원본 이미지를 사용하도록 수정
🔻 nodebird/views/main.html
🍳 이미지 업로드하기
람다를 통해 이미지 리사이징 된 것 확인하기
💻 Google Cloud Storage 사용하기
🍳 Cloud Storage 이용하기
좌측 메뉴에서 Storage를 선택한 후 버킷 만들기 버튼 클릭
🍳 Cloud Storage 버킷 만들기
버킷 이름은 고유해야 하므로 고유한 버킷 이름 정한 후 만들기 버튼 클릭
🍳 버킷 권한 수정하기
우측 버킷 메뉴에서 액세스 수정 선택
구성원 추가에 allUsers를 입력하고 저장소 개체 뷰어 선택 후 추가
- 위 단계에서 오류가 발생할 경우 공개 액세스 방지를 삭제해주면 됨
🍳 클라우드 스토리지 키 발급받기
https://console.cloud.google.com/apis/credentials 에 접속하기
사용자 인증 정보 화면에서 서비스 계정 키 선택
🍳 클라우드 스토리지 연결하기
multer-google-storage와 axios 설치
routes/post.js에 multer-google-storage를 multer 대신 연결
아까 다운받은 json 파일 이름을 keyFilename에 입력, projectId는 프로젝트의
아이디 입력(홈 메뉴의 프로젝트 정보 섹션에 있음)
1
npm i multer-google-storage
🔻 nodebird/routes/post.js
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
32
33
34
35
36
37
const express = require('express');
const multer = require('multer');
const fs = require('fs');
const multerGoogleStorage = require('multer-google-storage');
const { afterUploadImage, uploadPost } = require('../controllers/post');
const { isLoggedIn } = require('../middlewares');
const router = express.Router();
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성합니다.');
fs.mkdirSync('uploads');
}
const upload = multer({
storage: multerGoogleStorage.storageEngine({
bucket: 'nodebird3',
projectId: 'node-deploy-358509',
keyFilename: 'node-deploy-358509-a2917cd5849c.json',
filename: (req, file, cb) => {
cb(null, `original/${Date.now()}_${file.originalname}`);
},
}),
limits: { fileSize: 5 * 1024 * 1024 },
});
// POST /post/img
router.post('/img', isLoggedIn, upload.single('img'), afterUploadImage);
// POST /post
const upload2 = multer();
router.post('/', isLoggedIn, upload2.none(), uploadPost);
module.exports = router;
🔻 nodebird/controllers/post.js
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
const { Post, Hashtag } = require('../models');
exports.afterUploadImage = (req, res) => {
console.log(req.file);
res.json({ url: req.file.path });
};
exports.uploadPost = async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
🍳 이미지 업로드 시도하기
http://localhost:8001에 접속하여 로그인 후 이미지 업로드
클라우드 스토리지 버킷에 이미지가 업로드된 것 확인
💻 Google Cloud Functions 사용하기
🍳 이미지 리사이징을 위해 펑션 사용
이미지 리사이징은 CPU를 많이 사용하기 때문에 기존 서버로 작업하면 무리가 감
Cloud Functions라는 기능을 사용해 필요할 때만 서버를 실행해서 리사이징
🍳 펑션용 package.json 작성하기
gcp-upload 폴더 안에 package.json 작성하기
🔻 gcp-upload/package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
{
"name": "gcp-upload",
"version": "0.0.1",
"description": "Cloud Functions 이미지 리사이징",
"main": "index.js",
"author": "leekoby",
"license": "ISC",
"dependencies": {
"@google-cloud/storage": "^6.4.1",
"gm": "^1.23.1",
"sharp": "^0.30.5"
}
}
🔻 gcp-upload/.gitingnore
1
node_modules
🍳 펑션으로 이미지 리사이징하기
resizeAndUpload 메서드에 코드 작성
- storage.bucket(버킷명).file(파일명)
- readStream으로 파일 읽어들임
- sharp로 이미지 리사이징
- writeStream으로 파일 출력
- resolve로 응답 마무리
🔻 gcp-upload/index.js
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
const storage = require('@google-cloud/storage')();
const sharp = require('sharp');
exports.resizeAndUpload = (data, context) => {
const { bucket, name } = data;
const ext = name.split('.').at(-1);
const requiredFormat = ext === 'jpg' ? 'jpeg' : ext; // sharp에서는 jpg 대신 jpeg사용합니다
console.log('name', name, 'ext', ext);
const file = storage.bucket(bucket).file(name);
const readStream = file.createReadStream();
const newFile = storage.bucket(bucket).file(`thumb/${name}`);
const writeStream = newFile.createWriteStream();
sharp(readStream)
.resize(200, 200, { fit: 'inside' })
.toFormat(requiredFormat)
.pipe(writeStream);
return new Promise((resolve, reject) => {
writeStream.on('finish', () => {
resolve(`thumb/${name}`);
});
writeStream.on('error', reject);
});
};
🍳 코드 깃허브로 전송하기
먼저 GitHub에 gcp-upload 리포지터리로 올린 후 컴퓨트엔진 인스턴스에서 클론
압축 후 클라우드 스토리지로 업로드
🍳 코드 압축해서 클라우드 스토리지로 보내기
압축 후 S3로 업로드
🍳 펑션 이용하기
🍳 NodeBird에 펑션 연결하기
🔻 nodebird/controllers/post.js
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
32
33
34
const { Post, Hashtag } = require('../models');
exports.afterUploadImage = (req, res) => {
console.log(req.file);
const filePath = req.file.path.split('/').splice(0, 3).join('/');
const originalUrl = `${filePath}/${req.file.filename}`;
const url = originalUrl.replace(/\/original\//, '/thumb/');
res.json({ url, originalUrl });
};
exports.uploadPost = async (req, res, next) => {
try {
const post = await Post.create({
content: req.body.content,
img: req.body.url,
UserId: req.user.id,
});
const hashtags = req.body.content.match(/#[^\s#]*/g);
if (hashtags) {
const result = await Promise.all(
hashtags.map(tag => {
return Hashtag.findOrCreate({
where: { title: tag.slice(1).toLowerCase() },
})
}),
);
await post.addHashtags(result.map(r => r[0]));
}
res.redirect('/');
} catch (error) {
console.error(error);
next(error);
}
};
🔻 nodebird/views/main.html
🍳 이미지 업로드하기
펑션을 통해 이미지 리사이징 된 것 확인하기
💻 함께 보면 좋은 자료
📚 레퍼런스
조현영. Node.js 교과서 = Node.js Textbook / 조현영 지음 (2022). Print.