express-serverless 구축(2)

aws-express-serverless 를 통해 express 를 통해 구축한 간단한 api 서버를 API GatewayLmabda를 이용하여 배포해 보았다.

API Gateway를 통해 Lambda 함수를 실행하기 위해서는 API Gateway에서 생성된 URL로 요청을 보내야한다. 요청을 보내기 위한 URL은 aws 콘솔 - API Gateway - API - Stage 창에서 확인이 가능하다.

URL 정보.


url 구조는 https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage}

https://{restapi_id}.execute-api.{region}.amazonaws.com/{stage}와 같이 구성된 url로 요청을 보내면 Lambda에 배포한 함수가 실행되며 응답을 해준다.

하지만 이렇게 길고 불편한 URL을 매번 기억하고 요청하기에는 어려움이 있다.

요청을 보내고 싶은 URL은 https://{domain}/api/v1/lambda/{proxy}의 구조를 가진 URL이다. API Gateway에서 생성해준 URL이 아닌 직접 지정한 도메인으로 요청을 보내기 위해서는 커스텀 도메인을 등록하는 작업이 필요하다.

해당 작업을 진행하기 위해서는

  1. AWS 계정 (Lambda에 함수를 배포했으니 계정은 당연히 존재한다고 가정..)
  2. API Gateway에 연결할 도메인

이렇게 두가지만 있으면 된다. 그럼 진행해보도록 하자!!!

자세히 보기

express serverless 구축(1)

Lambda Express API 서버 구축하기

람다와 express 를 이용하여 API 서버를 구축해보자.

- 프로젝트 생성

1
npm init

- 패키지 설치

1
2
npm install express aws-serverless-express
npm install serverless-offline -d

- 로컬에서도 express를 실행하며 테스트하고 배포는 람다로 하고싶다.

개발환경과 배포환경을 구분하여 개발을 진행해야한다. (나누어 진행하지 않는다면?? 매우 힘들다…)
serverless 는 serverless-offline 이라는 서비스를 지원하며 람다를 통해 배포한 환경과 동일한 환경에서 테스트를 로컬에서 실행이 가능하도록 해주는 패키지이다.

그래서 난 로컬환경에서 serverless-offline 을 통해 실행함과 더불어 일반적인 express 개발 환경처럼 직접 express 를 구동할 수 있도록 설정하기로 했다.

자세히 보기

Sequelize Migration

도커를 통해 데이터베이스를 띄우고 express를 실행시켜 연결을 해보았다. MySQL과 연결하여 데이터베이스를 사용하기 위해서 Sequelize를 사용하여 연결까지 성공적으로 연결을 진행하였다.

기존에 시퀄라이즈를 사용할때는 모델을 모두 정의한 후 데이터베이스를 생성하고 모델의 수정사항이 있을때마다 **Sync({ force: false || true })**옵션을 통해 데이터베이스를 수정하며 진행했다. 물론 혼자 사용하는 데이터베이스고 서비스를 하지 않는 디비여서 이런방식으로 사용해도 무방하지만 실무에서 사용하는 데이터베이스는 많은 데이터가 있고 구조의 변경이 일어날때 마다 데이터를 백업한다던지 새로 덤프 한다는 것은 현실적으로 어려움이 있다.

이러한 문제점을 보완하기 위해 ORM에서는 마이그레이션 기능을 지원한다. 마이그레이션이란 어떤 운영환경에서 다른 환경으로 환경의 변화를 위해 옮겨지는 작업을 의미한다.

데이터베이스 마이그레이션이란.

데이터베이스 마이그레이션이란 하나의 데이터베이스를 다른 종류의 데이터베이스로 데이터를 옮기는 경우 혹은 두개의 데이터베이스를 하나의 시스템으로 합치거나 분할 혹은 데이터베이스 모델의 구조적 변경을 진행 하는 모든 과정을 의미한다.
데이터 베이스 마이그레이션이란?

그렇다면 시퀄라이즈에서 제공하는 Migration 기능에는 어떤 기능이 있을까? 시퀄라이즈에서는 몇가지 명령어를 통해 마이그레이션 기능을 사용할 수 있다고 한다.

자세히 보기

Doker Mysql(sequelize)

Docker MySQL Express 연결하기

이전 도커로 express앱을 띄운 후 연결 확인 후 데이터 베이스를 도커로 실행 한 후 express와 연결하는 테스트를 진행해보도록 하자.

먼저 docker-compose.yml에 mysql이미지를 추가해준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mysql:
image: mysql:5.7
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
restart: always
environment:
MYSQL_DATABASE: # Database Name
MYSQL_USER: # User name
MYSQL_PASSWORD: # Password
MYSQL_ROOT_PASSWORD: # Root Password
ports:
- "3306:3306"
volumes:
- ./data:/var/lib/mysql
- ./mysql.conf:/etc/mysql/conf.d

docker-compose up 명령 실행 후 mysql이 실행중인 것을 확인하고 docker exec -it #{name} bash 를 통해 mysql로 접근이 가능하다.

자세히 보기

Docker Node.js nodemon

도커를 활용하여 Express를 구동하기 위해 이전 포스팅을 통해 기본적인 앱을 띄워 보았다. 도커를 통해 express를 실행하였지만 코드의 수정이 있을 때 마다 빌드를 다시 해주어야 하는 문제점이 있었다.
node.js에서는 nodemon이라는 모듈을 사용하여 개발환경에서 코드의 변경사항이 감지되었을때 자동으로 코드의 반영사항을 포함하기 위해 서버를 재시동 해주며 개발을 진행한다.

도커를 통해 띄운 express를 로컬에서 작업 후 변경사항을 반영해주기 위해서는 도커의 작업 디렉토리와 로컬 작업 디렉토리를 볼륨 연결 설정을 통해 연결해주고 nodemon으로 express를 구동해주면 된다.

  1. docker-compose.yml
    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
    version: "3.8"

    services:
    mysql:
    image: mysql:5.7
    command:
    --default-authentication-plugin=mysql_native_password
    --character-set-server=utf8mb4
    --collation-server=utf8mb4_unicode_ci
    restart: always
    environment:
    MYSQL_DATABASE: test
    MYSQL_USER: test
    MYSQL_PASSWORD: 1210ss
    MYSQL_ROOT_PASSWORD: 1210ss
    ports:
    - "3306:3306"
    volumes:
    - ./data:/var/lib/mysql
    - ./mysql.conf:/etc/mysql/conf.d

    docker-test:
    build:
    context: .
    args:
    PORT: "4000"
    image: docker-node-app
    container_name: docker-node-app
    volumes:
    - .:/usr/src/app
    - /usr/src/app/node_modules
    ports:
    - "1210:4000"
    docker-compose.yml 파일을 통해 여러 컨테이너를 한번에 관리해주는 방식으로 하나의 서비스를 위해 실행하거나 존재해야하는 이미지들을 한 파일을 통해 정의하는 방식이다.

mysql은 이 후 사용할 예정이므로 넘어가도록 하고 volumes옵션이 중요한 부분이다.

volumes은 다음과 같이 작성해 주면된다. <로컬 작업 디렉토리>:<도커 컨테이너 디렉토리> 이것은 로컬 디렉토리와 도커의 디렉토리를 연결하겠다는 의미로 코드의 변경이나 생성 삭제와 같은 작업의 결과를 공유해준다.

이 후 nodemon을 통해 express 를 실행하고 로컬에서 작업 후 변경사항이 생기게 되면 도커에서 실행중인 express가 재시동 되며 수정 사항을 반영하게 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
FROM node:12

ARG PORT
RUN mkdir -p /usr/src/app
WORKDIR /usr/src/app

# . 은 현재 디렉토리의 모든것을 /usr/src/app으로 복사한다는 의미다.
COPY . /usr/src/app
RUN npm install

RUN npm install -g nodemon
# Bundle app source

EXPOSE $PORT

# 도커에서 nodemon을 실행하기 위해 필요한 옵션
CMD ["nodemon", "-L", "app.js"]

컨테이너를 다시 빌드 후 접속하고 코드를 수정하면 변경사항이 자동으로 반영되는 모습을 확인 할 수 있다.

Docker 개발환경 세팅하기

Docker 개발 환경 구축

회사에서 처음 클론 받은 레포지토리의 개발 환경 구축을 위해 도커환경을 구축하며 사용하던 명령어들과 도커를 이용하여 프로젝트 환경을 세팅하는 기본적인 것들에 대해서 기록하고자 글을 남깁니다.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 5.

도시별 숙소 보여주기

도시별 숙소 라우터

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
router.get('/city', async (req, res, next) => {
try {

let hosts;
let count;
let city;
let category
...
hosts = await Host.findAll({
where: {
hostaddress: {
[Op.like]: "%" + req.query.city + "%"
},
},
include: {
model: Image,
attributes: ['src'],
},
order: [['id', 'DESC']],
offset: offset,
limit: 12,

});
count = await Host.count({
where: {
city: {
[Op.like]: "%" + req.query.city + "%"
},
},
});
if(req.query.city) {
city = req.query.city;
} else {
city = "전체"
}
res.render('hosts', {hosts, count, city});
}

} catch (error) {
console.error(error);
next(error);
}
});

숙소의 주소 중 도시 컬럼의 값을 비교하여 해당 도시의 숙소를 가져오는 방식이다.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 4.

local 계정 회원가입 구현

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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
// 프론트 코드
document.getElementById('join__form').addEventListener('submit', (e) => {
e.preventDefault();
const joinData = {
email: e.target.email.value,
password: e.target.password.value,
name: e.target.name.value,
nickname: e.target.nickname.value,
phone: e.target.firstNum.value + e.target.middleNum.value + e.target.lastNum.value,
}
console.log(joinData);
if(joinData.email === "" ) {
return alert('이메일을 입력해주세요.');
}
if(joinData.password < 4) {
return alert('비밀번호는 4글자 이상입니다.');
}
if(joinData.name === "" || joinData.nickname === "" || joinData.phone === "" || joinData.phone.length > 11) {
return alert('잘못된 정보입니다.');
}

axios.post('/auth/join', joinData)
.then((response) => {
if(response.data.joinsuccess){
alert(response.data.message);
return window.location = "/login";
}
alert(response.data.message);
return location.reload();
})
.catch((error) => {
console.log(error);
});


e.target.email.value = '';
e.target.password.value = '';
e.target.name.value = '';
e.target.nickname.value = '';
e.target.firstNum.value = '';
e.target.middleNum.value = '';
e.target.lastNum.value = '';
});
// 서버 코드
router.post('/join', isNotLoggedIn, async (req, res, next) => {
try {
const {
email,
password,
name,
nickname,
phone,
} = req.body;
console.log(req.body);
//const phone = firstNum + middleNum + lastNum;
const user = await User.findOne({ where: {email} });
if(user) {
return res.json({ joinsuccess: false, message: "입력하신 이메일로 가입한 계정이 존재합니다..." });
}
const hash = await bcrypt.hash(password, 12);
const newUser = await User.create({
email,
password: hash,
name,
nickname,
phone,
});
return res.json({ joinsuccess: true, message: "회원가입 성공입니다."});

} catch (error) {
console.error(error);
return res.json({ joinsuccess: false, message: "데이터 베이스 오류 입니다 관리자에게 문의 하세요..."});
}
});

회원 가입 페이지에서 가입 정보를 받아온 후 User모델에 새로운 데이터를 생성하여 회원을 생성한다.

가입 전 회원을 구분할 수 있는 이메일로 기존의 데이터와 일치한 정보가 있는지 확인을 하고 없을 때 생성하는 방식으로 진행했다…

결과

회원 가입을 할 정보를 입력한다.

서버에서 처리 후 json형식으로 보낸 데이터에 따라 성공 실패 여부를 확인 한다.

입력한 데이터가 없는 회원이기 때문에 성공이라는 메시지를 띄웠고 성공했다는 것을 알수있다..✌✌

users 테이블을 확인하면 아까 입력한 데이터가 성공적으로 생성 된 것을 확인 할 수 있다. 👍

문제점

로컬 계정 회원 가입 처리 코드 작성은 간단하게 구현해서 어렵게 느껴지지는 않았지만… 페이지에서 데이터를 보낼 때 검사하는 방법이 너무 많고 어려운게 많아서 간단하게 검사 후 서버로

데이터를 보내는 방식으로 진행했다…

자바 스크립트를 이용해서 데이터를 검사해서 보내야 하는 방법에 치여서 조금 어려움을 느꼈다 자바스크립트의 공부가 필요하다는 것을 느끼는 과정 이였다.

보완점

혼자 하는 프로젝트라서 많은 것을 축소해서 진행 하려 하다 보니 회원 가입 과정이 너무 단순하다는 생각이 들었다. 많은 사이트들을 참고해서 비슷한 과정으로 만들고 싶었지만

본인 인증 부분을 제외 시킨 것이 조금 아쉽다.

본인 인증 구현도 공부해서 사용해봐야겠다.

Nodejs express로 에어비앤비 클론코딩 해보기 3.

숙소 업로드 구현

숙소 업로드 라우터

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
46
47
48
49
50
51
try {
fs.readdirSync('uploads');
} catch (error) {
console.error('uploads 폴더가 없어 uploads 폴더를 생성 합니다.');
fs.mkdirSync('uploads');
}

const upload = multer({
storage: multer.diskStorage({
destination(req, file, cb) {
cb(null, 'uploads/');
},
filename(req, file, cb) {
const ext = path.extname(file.originalname);
cb(null, path.basename(file.originalname, ext) + Date.now() + ext);
},
}),
});

// 게시글 업로드
router.post('/host', isLoggedIn, upload.array('img'), async (req, res, next) => {
const hostaddress = req.body.firstCity.concat(" ", req.body.middleCity," ", req.body.hostaddress);

try{
const newHost = await Host.create({
title: req.body.title,
hostaddress,
city: req.body.firstCity,
person: req.body.person,
roominfo_room: req.body.room,
roominfo_bed: req.body.bed,
roominfo_cook: req.body.cook,
roominfo_bathroom: req.body.bathroom,
hostinfo: req.body.hostinfo,
hosttype: req.body.hosttype,
UserId: req.user.id,
});
for(i=0; i<req.files.length; i++){
let newImage = await Image.create({
src: "img/" + req.files[i].filename,
HostId: newHost.id,
});

}
console.log("success");
res.redirect('/');
} catch (error) {
console.log(error);
next(error);
}
});

숙소 업로드 라우터는 숙소 정보를 담는 Host모델과 숙소의 사진을 담는 Image모델을 사용하여 업로드를 진행한다.

사진 파일들은 multer 모델을 사용하여 업로드했고 이미지의 파일 이름을 Image모델에 담아 주었다.

이미지 업로드는 이미지의 갯수 만큼 생성하도록 반복문을 사용하여 진행했는데 더 좋은 방법이 있을 수 도 있지만 간단하게 구현 했다.

자세히 보기

Nodejs express로 에어비앤비 클론코딩 해보기 2.

Sequelize Model 구축

Mysql 데이터베이스를 사용하며 Sequelize를 통해 데이터 관리를 했다. 시쿼라이즈를 통한 데이터베이스 사용을 위해 모델을 구축하는 과정을 기록합니다.

DB 관계

User 1 : N Host
User 1 : N Review
User 1 : N Reservation
User 1 : N Favorite

Host 1 : N Image
Host 1 : N Favorite
Host 1 : N Reservation
Host 1 : N Review

많은 모델을 설계하지는 않았지만 각각 필요한 모델이라고 생각하는 모델을 구축했다.
기본적으로 유저와 게시글을 기준하여 예약 기능에 필요한 모델 찜 모델, 리뷰 모델 정도로 구성했다.

자세한 코드는 깃헙에 올렸으니 기본적인 관계 설정과 모델 구조는 깃에서 확인하면 될 것 같다.
Model file : github