기본 콘텐츠로 건너뛰기

아마존웹서비스 안드로이드, ec2, mysql, nodejs, 탄력적 ip (예약관리 앱)

아마존웹서비스 안드로이드, ec2, mysql, nodejs, 탄력적 ip (예약관리 앱)

aws, mysql, android, nodejs 를 사용해서 간단한 어플리케이션 만들어보기

AWS를 사용해보고 싶어서 안드로이드와 접목하면서 했던것들을 기록할려고 합니다.

나중에 다시 AWS를 사용하게 된다면 기억을 떠올리고자 부분부분을 간단한 메모형식으로 작성하는글이라 보기 힘들 수 있습니다. (순서는 관계없이 덕지덕지 작성하겠습니다. ㅠ)

aws 가입

ec2, rds 인스턴스 생성하는데 돈들거아니면 모두 프리티어로 하자.

난 EC2에서 리눅스로 생성하였다. 그 후는 프리티어로 그냥 다 설정해주면 된다. (밑 사이트를 참고하면된다.. 아주 자세히작성하셨습니다.!! RDS의 경우에도 마찬가지이다. mysql 프리티어 선택)

이거 외에도 보안그룹이라든가 키페어 생성등도 잘 작성되어있다. (난 aws키는 aws폴더를 따로만들어 잘 보관중이다.)

https://blog.naver.com/PostView.nhn?blogId=zion830&logNo;=221353335126&parentCategoryNo;=&categoryNo;=&viewDate;=&isShowPopularPosts;=false&from;=postView

putty를 통해 리눅스서버에 원격으로 접속이 가능하다.

PuTTY는 SSH를 사용하기 때문에 리눅스 서버에 접속하기 위해 이전 글에서 만들었던 개인키 파일이 필요하다. 이거에 대해서도 다음 블로그에서 자세히 설명이 되어있습니다.

https://blog.naver.com/zion830/221353353266

밑 글을 참고하여 RDS인스턴스를 생성한다.

https://blog.naver.com/zion830/221354722615

참고로 나는 포트번호를 3308로 하고 mysql버전을 8로하였다. 과금안되게 프리티어로 잘 설정하도록하자. 그리고 디비이름과 사용자이름 암호 다 나중에 접속하는데 사용하므로 잘 설정하도록 하자.

여기서 연결&보안에 퍼블릭 엑세스 가능성을 '예'로 해놔야하는다. (내컴터같은 다른곳에서 aws컴터로 접근할려면) 이건 처음 rds생성할때 옵션이 안보여서 RDS생성 후 위에 사진의 수정버튼을 통해 수정해주었다.

RDS를 생성하고 mysql workbench로 테이블을 생성하고 관리하자. (cmd창으로도 할수있지만 매우 불편하므로)

이것도 밑 블로그에 자세히 작성되어있습니다. 주의해야할 점은 username과 암호를 내가 RDS인스턴스를 생성할때에 작성한거와 맞게 작성해주어야한다. 또한 호스트네임은 당연히 ec2가 아닌 rds 엔드포인트를 작성해야한다.

마지막으로 생성 후 데이터베이스를 생성할 때 내가 RDS를 생성했을때의 스키마 이름으로 해야한다. (example)

밑은 내가 처음 RDS생성할떄 example로 했던 캡처사진이다.

아니다 지금보니깐 나도 3306으로했다. ......

데이터베이스 스키마 생성

USE example

https://blog.naver.com/zion830/221354730576

nodejs 세팅 및 작성하는법과 파일질라로 올리는건 다음을 참고하면된다.

https://blog.naver.com/zion830/221396511803

https://blog.naver.com/zion830/221407674520

파일질라에서 호스트는 ec2 엔드포인트를 작성해주면 된다. (퍼블렉DNS)

웹스톰으로 편하게 코드를 작성 후 깃헙에 올려서 putty를 통해 aws에서 클론하거나

아니면 코드작성한 프로젝트를 파일질라를 통해 aws로 옮겨주면된다.

그리고 참고한 블로그에서의 코드가 저한테 안먹어서 바디파서를 추가하고 다음과 같이 살짝 바꿧습니다. 또한 listen에 가운데에 매개변수에 localhost가 원래있었는데 해당부분을 없에주었습니다.

그리고 간단하게 만든거라 따로 컨트롤러로 안나누고 app.js에 다 처리하게 하였습니다.

var mysql = require('mysql'); var express = require('express'); var bodyParser = require('body-parser'); var app = express(); app.use(bodyParser.json({extended: true})); //사용자가 웹사이트로 전달하는 정보들을 검사하는 미들웨어 app.use(bodyParser.urlencoded({extended: true})); //json이 아닌 post형식으로올때 파서 app.listen(3000, function () { console.log('서버 실행'); }); //디비연결 var connection = mysql.createConnection({ host: "dmazonaws.com", user: "w", database: "e", password: "t", port: 3306 }); //회원가입 app.post('/user/join', function (req, res) { var userId = req.body.userId; var userPassword = req.body.userPassword; var userRestaurant = req.body.userRestaurant; var restaurantLocation = req.body.restaurantLocation; var restaurantTel = req.body.restaurantTel; var fcm = req.body.fcm; var sql = 'INSERT INTO Users (userId, userPassword, userRestaurant, restaurantLocation, restaurantTel, fcm ) VALUES (?, ?, ?, ?, ?, ?)'; var params = [userId, userPassword, userRestaurant, restaurantLocation, restaurantTel, fcm]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { res.json({ result: true, msg: '회원가입에 성공했습니다.' }) } }); }); //아이디 중복확인 app.post('/user/checkId', function (req, res) { var userId = req.body.userId; var sql = 'select * from Users where userId = ?'; connection.query(sql, userId, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { res.json({ result: false, msg: '사용가능한 아이디입니다.' }); } else { res.json({ result: true, msg: '중복된 아이디가 존재합니다' }); } } }) }); //로그인 app.post('/user/login', function (req, res) { var userId = req.body.userId; var userPassword = req.body.userPassword; var sql = 'select * from Users where userId = ? AND userPassword = ?'; var params = [userId, userPassword]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { res.json({ result: false, msg: '존재하지 않는 계정입니다!' }); } else if (userPassword !== result[0].userPassword) { res.json({ result: false, msg: '비밀번호가 틀렸습니다!' }); } else { res.json({ result: true, msg: '로그인 성공!', id : result[0].id, userId: userId, userPassword: result[0].userPassword, userRestaurant: result[0].userRestaurant, restaurantLocation : result[0].restaurantLocation, restaurantTel : result[0].restaurantTel }); } } }) }); //레스토랑검색 app.post('/restaurant/search', function (req, res) { var userRestaurant = req.body.userRestaurant; var sql = "select * from Users where userRestaurant LIKE " +connection.escape('%'+req.body.userRestaurant+'%'); connection.query(sql, userRestaurant, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { } else { res.json({ result }); } } }) }); //레스토랑 예약 app.post('/restaurant/insert', function (req, res) { var restaurant_id = req.body.restaurant_id; var restaurant_name = req.body.restaurant_name; var apply_id = req.body.apply_id; var apply_date = req.body.apply_date; var reserve_date = req.body.reserve_date; var user_tel = req.body.user_tel; var user_pw = req.body.user_pw; var accept = req.body.accept; var fcm = req.body.fcm; var sql = 'INSERT INTO Apply (restaurant_id, restaurant_name, apply_id, apply_date, reserve_date, user_tel,user_pw, accept, fcm ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)'; var params = [restaurant_id, restaurant_name, apply_id, apply_date, reserve_date, user_tel,user_pw, accept, fcm]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { res.json({ result: true, msg: '예약이 성공했습니다' }) } }); }); //특정 레스토랑 전체 예약리스트(전체날짜) app.post('/restaurant/admin', function (req, res) { var restaurant_id = req.body.restaurant_id; console.log(restaurant_id, typeof(restaurant_id)); var sql = "select * from Apply where restaurant_id = ? ORDER BY reserve_date"; connection.query(sql, restaurant_id, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { } else { res.json({ result }); } } }) }); //특정 레스토랑 예약리스트(특정날짜) app.post('/restaurant/admin/certain', function (req, res) { var restaurant_id = req.body.restaurant_id; var reserve_date = req.body.reserve_date; var sql = "select * from Apply where restaurant_id = ? AND reserve_date LIKE " + connection.escape('%'+req.body.reserve_date+'%') + "order by reserve_date"; var params = [restaurant_id, reserve_date]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { } else { res.json({ result }); } } }) }); //예약 수락 app.post('/restaurant/accept', function (req, res) { var id = req.body.id; var sql = "update Apply set accept = 'yes' where id = ?"; connection.query(sql, id, function (err, result) { if (err) console.log(err); else { if (err) console.log(err); else { res.json({ result: true, msg: '예약이 수락되었습니다.' }) } } }) }); //예약 수락, 거절 알림 app.post('/restaurant/accept/alarm', function (req, res) { var id = req.body.id; var sql = "select * from Apply where id = ?"; connection.query(sql, id, function (err, result) { if (err) console.log(err); else { if (err) console.log(err); else { res.json({ result: true, fcm: result[0].fcm, restaurant_name: result[0].restaurant_name }) } } }) }); //예약신청 했다고 상대방에게 알림 app.post('/restaurant/reserve/alarm', function (req, res) { var id = req.body.id; var sql = "select * from Users where id = ?"; connection.query(sql, id, function (err, result) { if (err) console.log(err); else { if (err) console.log(err); else { res.json({ result: true, fcm: result[0].fcm, userId: result[0].userId }) } } }) }); //예약수락한거 취소 app.post('/restaurant/cancel', function (req, res) { var id = req.body.id; var sql = "update Apply set accept = 'no' where id = ?"; connection.query(sql, id, function (err, result) { if (err) console.log(err); else { if (err) console.log(err); else { res.json({ result: true, msg: '예약이 취소되었습니다.' }) } } }) }); //내가 예약한 레스토랑 app.post('/user/reserve', function (req, res) { var apply_id = req.body.apply_id; var user_tel = req.body.user_tel; var user_pw = req.body.user_pw; console.log("내가 예약한 가게검색"); var sql = "select * from Apply where apply_id = ? AND user_tel = ? AND user_pw = ? order by reserve_date"; var params = [apply_id, user_tel, user_pw]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { if (result.length === 0) { } else { res.json({ result }); } } }) }); //음식점(유저) fcm 변경 app.post('/user/fcm', function (req, res) { var id = req.body.id; var fcm = req.body.fcm; var sql = "update Users set fcm = ? where id = ?"; var params = [id, fcm]; connection.query(sql, params, function (err, result) { if (err) console.log(err); else { if (err) console.log(err); else { res.json({ result: true, msg: 'FCM 변경 성공' }) } } }) });

ec2 주소 유출위험성 때문에 잘라서 붙였습니다.(포스트맨) 포트는 3000으로 했으므로 ec2주소 뒤에 3000포트를 작성해준다.

처음 포스트맨으로 테스트한 결과입니다. 그 이후 변수명(컬럼명)을 변경하였습니다. 안붙여도 되긴한다.

잘 통신이되고 디비에 값이 삽입되는것을 확인할 수 있었습니다. 이렇게 통신이 된걸 확인한 후에는 안드로이드 프로젝트를 실행하고 안드로이드에서 레트로핏2를 사용해서 통신해주면 됩니다.

만약 putty에서 node app.js 이렇게 실행을 할텐데

그럼 putty를 종료시키면 서버가 죽어서 post방식으로 요청을해도 응답이 오질 않습니다.

그래서 pm2를 설치해주고

pm2 명령어로 노드를 실행시켜주면 됩니다.

http://pm2.keymetrics.io/

명령어 정리잘해논사이트

https://massivcode.com/5

그럼 실행시키고 푸티를 꺼도 죽지않습니다.

인스턴스를 중지시키고 다시실행시키면 IP가 바뀐다. 그래서 탄력적 IP주소를 활용해서 고정아이피로 박아놓으면 좋다. 사용법은 아래사이트에서 볼 수 있다.

https://docs.aws.amazon.com/ko_kr/AWSEC2/latest/UserGuide/elastic-ip-addresses-eip.html

안드로이드 관련 코드 (레트로핏 사용, 디비삽입까지 잘 됨을 확인)

import retrofit2.Retrofit; import retrofit2.converter.gson.GsonConverterFactory; public class ApiClient { private static final String BASE_URL = "ec2주소:3000/"; private static Retrofit retrofit; public static Retrofit getApiClient(){ if(retrofit == null){ retrofit = new Retrofit.Builder() .baseUrl(BASE_URL) .addConverterFactory(GsonConverterFactory.create()) .build(); } return retrofit; } }

import com.mtjin.aws_number_ticket.api.ApiClient; import com.mtjin.aws_number_ticket.api.ApiInterface; import com.mtjin.aws_number_ticket.model.User; import retrofit2.Call; import retrofit2.Callback; import retrofit2.Response; public class SignupPresenter implements SignupContract.Presenter{ private SignupContract.View view; public SignupPresenter(SignupContract.View view) { this.view = view; } @Override public void requestCheckId(String userId) { view.showProgress(); ApiInterface apiInterface = ApiClient.getApiClient().create(ApiInterface.class); Call call = apiInterface.checkUser(userId); call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { view.hideProgress(); if(response.isSuccessful() && response.body() != null){ Boolean isDuplicate = response.body().getResult(); //중복된아이디인지 if(isDuplicate){ //중복 view.onToastMessage("중복된 아이디가 이미 존재합니다."); }else{ view.onToastMessage("사용가능한 아이디입니다."); view.blockUserId(); } } } @Override public void onFailure(Call call, Throwable t) { view.hideProgress(); view.onToastMessage(t.getLocalizedMessage()); } }); } @Override public void requestSignup(String userId, String userPassword, String userRestaurant) { view.showProgress(); ApiInterface apiInterface = ApiClient.getApiClient().create(ApiInterface.class); Call call = apiInterface.saveUser(userId, userPassword, userRestaurant); call.enqueue(new Callback() { @Override public void onResponse(Call call, Response response) { view.hideProgress(); if(response.isSuccessful() && response.body() != null){ Boolean isSuccess = response.body().getResult(); if(isSuccess){ view.onToastMessage("회원가입 되었습니다."); } } } @Override public void onFailure(Call call, Throwable t) { view.hideProgress(); view.onToastMessage(t.getLocalizedMessage()); } }); } }

import android.content.Context; public interface SignupContract { interface View { void onToastMessage(String message); //토스트메세지 void showProgress(); void hideProgress(); void blockUserId(); //중복확인 후 아이디 변경 못하게막기 } interface Presenter{ void requestCheckId(String userId); //아이디 중복확인 요청 void requestSignup(String userId, String userPassword, String userRestaurant, String restaurantLocation , String restaurantTel, String fcm); //회원가입 요청 } }

import android.app.ProgressDialog; import android.content.Intent; import android.os.Bundle; import android.util.Log; import android.view.View; import android.widget.Button; import android.widget.EditText; import android.widget.Toast; import androidx.appcompat.app.AppCompatActivity; import com.mtjin.aws_number_ticket.R; import com.mtjin.aws_number_ticket.activity.login.LoginActivity; public class SignupActivity extends AppCompatActivity implements SignupContract.View{ EditText idEdit; EditText pwEdit; EditText pwConfirmEdit; EditText restaurantEdit; Button duplicateIdCheckEdit; Button okButton; ProgressDialog progressDialog; //value SignupPresenter presenter; boolean isChecked = false; //아이디 중복체크여부 Button.OnClickListener onClickListener; String userId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_signup); idEdit = findViewById(R.id.signup_et_id); pwEdit = findViewById(R.id.signup_et_pw); pwConfirmEdit = findViewById(R.id.signup_et_pwconfrim); restaurantEdit = findViewById(R.id.signup_et_restaurant); duplicateIdCheckEdit = findViewById(R.id.signup_btn_check); okButton = findViewById(R.id.signup_btn_ok); setOnClickListener(); duplicateIdCheckEdit.setOnClickListener(onClickListener); okButton.setOnClickListener(onClickListener); //다이얼로그 progressDialog = new ProgressDialog(this); progressDialog.setMessage("잠시만 기다려주세요 :)"); //프레젠터 presenter = new SignupPresenter(this); } private void setOnClickListener(){ onClickListener = new Button.OnClickListener() { @Override public void onClick(View view) { switch (view.getId()){ case R.id.signup_btn_check: userId = idEdit.getText().toString().trim(); Log.d("FFFF", userId); if(userId.length() <= 0){ idEdit.setError("아이디를 입력해주세요"); }else{ presenter.requestCheckId(userId); } break; case R.id.signup_btn_ok: String pwd = pwEdit.getText().toString().trim(); String pwd2 = pwConfirmEdit.getText().toString().trim(); String restaurant = restaurantEdit.getText().toString().trim(); if(!isChecked){ onToastMessage("아이디 중복체크를 먼저 해주세요."); }else if(pwd.length() <= 0){ onToastMessage("비밀번호를 작성해주세요."); }else if(!pwd.equals(pwd2)){ onToastMessage("비밀번호가 서로 다릅니다."); }else if(restaurant.length() <= 0){ onToastMessage("음식점 이름 작성해주세요."); } else{ presenter.requestSignup(userId, pwd, restaurant); finish(); } break; } } }; } @Override public void onToastMessage(String message) { Toast.makeText(this, message, Toast.LENGTH_SHORT).show(); } @Override public void showProgress() { progressDialog.show(); } @Override public void hideProgress() { progressDialog.hide(); } @Override public void blockUserId() { idEdit.setFocusable(false); idEdit.setEnabled(false); duplicateIdCheckEdit.setEnabled(false); isChecked = true; } }

import com.google.gson.annotations.Expose; import com.google.gson.annotations.SerializedName; public class User { @Expose @SerializedName("userId") private int userId; @Expose @SerializedName("userPassword") private String userPassword; @Expose @SerializedName("userRestaurant") private String userRestaurant; @Expose @SerializedName("result") private Boolean result; @Expose @SerializedName("msg") private String msg; public int getUserId() { return userId; } public void setUserId(int userId) { this.userId = userId; } public String getUserPassword() { return userPassword; } public void setUserPassword(String userPassword) { this.userPassword = userPassword; } public String getUserRestaurant() { return userRestaurant; } public void setUserRestaurant(String userRestaurant) { this.userRestaurant = userRestaurant; } public Boolean getResult() { return result; } public void setResult(Boolean result) { this.result = result; } public String getMsg() { return msg; } public void setMsg(String msg) { this.msg = msg; } }

참고한 사이트 : (코알라일락님 블로그를 참고해서 포스팅을 할 수 있도록 허락 해주셔서 감사합니다.)

https://docs.aws.amazon.com/ko_kr/elasticbeanstalk/latest/dg/create-deploy-nodejs.rds.html

https://blog.naver.com/zion830/221354730576

from http://youngest-programming.tistory.com/123 by ccl(S)

댓글

이 블로그의 인기 게시물

카카오 오픈빌더와 외부 API 연동(feat.Nodejs)

카카오 오픈빌더와 외부 API 연동(feat.Nodejs) 이전에 플러스 친구와 외부 API 연동에 관한 글을 작성한 적 있습니다. 하지만 지난 2년동안 플러스 친구에 많은 변화가 생겼는데요. 카카오 플러스 친구의 명칭이 카카오 채널로 바뀌고, 챗봇 세팅 방식이 기존 [카카오 플러스 친구 - 외부 API 연동] 구조에서 오픈빌더가 추가되어 [카카오 채널(구 플러스 친구) - 카카오 i 오픈빌더 - 외부 API 연동] 구조로 바뀌었습니다. 이번 글에서는 오픈빌더의 챗봇 시나리오 관리 기능을 간단히 소개하고 외부 API를 연동하는 예제를 다뤄보겠습니다. (연동파트는 5번 항목부터 보시면 됩니다.) 1. 블록 블록은 오픈빌더에서 질의/응답을 관리하는 최소 단위로, 사용자의 발화와 챗봇의 대답을 입력할 수 있습니다. 예를들어 인사라는 블록을 만들고 인사에 해당하는 사용자 발화 패턴들을 입력해두면, 실제 채널 톡방에서 그에 해당하는 발화가 들어왔을때 입력해둔 응답이 나오는 형식입니다. 예전에는 패턴과 발화 키워드가 1:1 매칭, 즉 입력해둔 패턴과 사용자 발화의 string이 정확히 일치할때만 블록이 실행됐었는데, 발화 패턴을 20개 이상 등록하면 머신러닝 기능을 이용할 수 있도록 기능이 생겼습니다. 아마 유사도 분석 개념이 기본으로 들어가있을 것이기 때문에 블록의 주제와 벗어나는 너무 뜬금없는 발화패턴들을 많이 넣지 않도록 하는걸 권장하겠습니다. 2. 시나리오 시나리오는 '블록'들을 묶어서 관리할 수 있는 단위로, 일종의 폴더 구조라고 생각하면 쉽습니다. 오픈빌더에서 좌측 상단에 파란 버튼을 클릭하여 시나리오를 생성할 수 있습니다. 하나의 시나리오에서 모든 블록을 관리하면 챗봇 도메인이 커질수록 관리가 어려워지니 아래 같은식으로 시나리오를 사용하여 블록을 구조화하면 운영 측면에서 수월해집니다. 3. 컨텍스트 컨텍스트는 맥락이라는 뜻 입니다. 오픈빌더에 존재하는 컨텍스트는 자연어 분석을 통해서 맥...

20.03.24 ShareBook TIL

20.03.24 ShareBook TIL Project/TIL 20.03.24 ShareBook TIL 중간 배포를 위해 EC2, RDS를 다시 설정하였다. EC2에 git에서 clone을 하고 서버를 작동시켜보니 ts로 돌려서 그런지 작동하지 않고 대기 상태로 있다가 timeout같은 시간 초과 에러가 났다. 그리고 갑자기 EC2 자체가 느려져서 nodejs를 삭제하고 다시 nvm으로 높은 버전의 node를 설치하였다. 그리고 나서 혹시 js로 돌리면 될까 해서 tsc로 js로 변환한뒤 돌려보니 RDS와 연결이 되지 않는 에러가 생겼다. workbench로 RDS를 연결했을 때는 정상적으로 작동해서 EC2에서 잘 못 설정한게 있다고 생각했다. 그래서 local에서 한번 config.json을 수정하고 연결하여도 똑같은 에러가 발생했다. 그럼 보안 설정에서 문제인가 싶어서 EC2, RDS 보안 그룹에서 설정을 막 만져보다 RDS에서 Custom TCP에 처음 RDS에서 설정한 포트를 넣어주었더니 연결되었다. config.json내용을 EC2에도 똑같이 적용시켜보려고 json파일을 vim으로 작성해서 넣어 주었지만 여전히 같은 에러를 반복하였다. 그럼 json 파일을 못 읽어내는게 아닌가 싶어서 그냥 module에 index.js에서 sequelize를 생성하는 부분에 직접 넣어 주었더니 마침내 연결이 되었다. 해결하고 난 뒤 생각의 흐름을 적어보니 매우 짧지만 정작 오늘 아침 10시 반부터 시작해서 저녁 10시 반까지 12시간을 고민하고나서야 해결되었다. from http://three-five.tistory.com/46 by ccl(A) rewrite - 2020-03-25 00:54:05

AWS instance로 Nodejs 구현하기

AWS instance로 Nodejs 구현하기 서버와 데이터베이스 관리 차원에서 효율적으로 관리하기 위해선 로컬보다는 서버를 호스팅해서 하는 것이 좋다. 우리는 Nodejs를 구동하기 위해 AWS에서 인스턴스를 할당받을 계획이다. 인스턴스의 pem키를 발급받아 nodejs와 npm까지는 설치를 완료한 상태이다. $ sudo npm install -g express 다음의 명령어를 입력하면 글로벌 옵션으로 어느 path에서든 express를 사용할 수 있게 설치한다. 다음과 같이 실행이 된다면 성공이다. 이후 Express generator를 설치한다. $ sudo npm install -g express-generator@4 버전은 4.x이며 이 역시 글로벌 옵션으로 설치해 준다. 이제 Node monitoring을 위해 nodemon을 설치해 준다. $ sudo npm install -g nodemon 모든 설치가 끝났다. 이제 nodejs를 실행시킬 프로젝트용 directory를 만든다. 이렇게 만들어 주고 express를 실행시키면 된다. $ express -e 다음과 같은 결과가 나오면 된다. 이제 node package를 설치하는 명령어를 입력하자. $ sudo npm install 이제 vi를 통해 포트번호를 정의해보자. app.set의 마지막에 한줄을 추가하면 된다. app.set('port', process.env.PORT || 9000); 이로써 우리는 9000번 포트를 사용하게 되었다. 또한 마지막줄에 서버를 생성하기 위한 코드를 작성하자. module.exports = app; var server = app.listen(app.get('port'), function() { console.log('Express server listening on port ' + server.address().port); }); 이...