웹 개발 실전 프로젝트 - 4주차
Yelp Camp 활용한 프로젝트
39 : YelpCamp : 캠프인들의 CRUD
YelpCamp 에 엑세스 하는 방법
Init Express App
npm i express mongoose ejs
그 후 해당 폴더에
app.js
생성const express = require('express');
const app = express();
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'))
app.get('/', (req, res) => {
res.send('HELLO FROM YELP CAMP!')
})
app.listen(3000, () => {
console.log('Serving on port 3000')
})
참고로 ejs 는 동적 HTML 파일이다.
모델 기초
Mongoose 를 이용해 캠핑장 모델을 생성한다. 이름과 가격 설명,위치들이 들어가는 모델이다. 간단한것으로 시작함
models/campground.js
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const CampgroundSchema = new Schema({
title : String,
price : String,
description : String,
location : String,
});
module.exports = mongoose.model('Campground', CampgroundSchema);
mongoose 에 들어갈 모델 객체는 이와같이 생성한다.
객체를 만든 후 이제 앱과 mongoose 를 연결한다.
app.js
const express = require('express');
const app = express();
const mongoose = require('mongoose');
const Campground = require('./models/campground');
mongoose.connect('mongodb://localhost:27017/yelp-camp', {
useNewurlParser : true,
useUnifiedTopology : true,
})
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open" , ()=>{
console.log("Database Connected");
});
또한 create, read 하는 부분을 작성한다.
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'))
app.get('/', (req, res) => {
res.send('HELLO FROM YELP CAMP!')
})
app.get('/makecampground', async (req, res) => {
const camp = new Campground({title : 'My Backyard', description : 'cheap camping!' })
await camp.save();
res.send(camp);
});
app.listen(3000, () => {
console.log('Serving on port 3000')
})
localhost:3000//makecampground/
접속시 새 정보가 추가되었다.캠프 그라운드에 시드
Index 라우트를 생성 및 편집할 때 간단히 시드데이터만 꺼내서 사용할 수 있음.
과제에 들어있는 cities.js 랑 seedHelper.js 라는 파일을 받아서 사용한다. 별개의 기능이므로 index.js 를 따로 생성한다.
index.js
const mongoose = require('mongoose');
const Campground = require('../models/campground');
mongoose.connect('mongodb://localhost:27017/yelp-camp', {
useNewurlParser : true,
useUnifiedTopology : true,
})
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));
db.once("open" , ()=>{
console.log("Database Connected");
});
실제로 사용 가능한지 그리고 기존에 들어있던 데이터를 모두 초기화 하기 위해 동일 파일에 아래의 코드를 넣어서 실행해본다.
const seedDB = async() => {
await Campground.deleteMany({});
const c = new Campground({title: 'purple filed'})
await c.save();
}
seedDB();
실행 후
{title: 'purple filed'}
만 남아있게된다. 이제 seedDB 의 내용을 루프로 샘플 데이터를 넣도록 변경한다.
const sample = array => array[Math.floor(Math.random() * array.length)];
const seedDB = async() => {
await Campground.deleteMany({});
for(let i = 0 ; i < 50 ; i ++){
const random1000 = Math.floor(Math.random() * 1000);
const {places, descriptors} = require('./seedHelper');
const camp = new Campground({
location : `${cities[random1000].city}, ${cities[random1000].state}`,
title : `${sample(descriptors)} ${sample(places)}`,
})
await camp.save();
}
}
seedDB();
실행 후 50개가 저장되었다.

캠프 그라운드 index
Express Router 를 사용해보기 전에 각 캠핑장에 대해서 경로를 등록해본다.
app.get('/campgrounds', async (req, res) => {
const campgrounds = await Campground.find({});
res.render('campgrounds/index', {campgrounds}); // campgrounds 란 내용으로 rend한다.
});
render 경로로
campgrounds/index.ejs
로 정했으므로 해당 내용을 편집한다 <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Campgrounds</title>
</head>
<body>
<h1>All Campgrounds</h1>
<ul>
<% for (let campground of campgrounds){%>
<li><a href="./<%= campground._id%>"><%= campground.title %></a></li>
<% }%>
</ul>
</body>
</html>
localhost:3000//campgrounds/
접속시
Show라우트
이제 캠프별 상세페이지를 볼 수 있도록 라우트를 지정한다.
app.get('/campgrounds/:id', async (req, res) => {
const campground = await Campground.findById(req.params.id);
res.render('campgrounds/show', {campground});
});
campground/ 하위에 id 를 받고 해당 id 에 맞는 내용을 select 하여 show 에 랜딩 해주는 구조다.
views/campgrounds/show.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Show</title>
</head>
<body>
<h1><%= campground.title %></h1>
<h2><%= campground.location %></h2>
</body>
</html>
New & Create
이제 신규 내용을 넣는 폼을 만든다. 단 중요한게 있는데 방금 전
app.get('/campgrounds/:id')
요청이 id = new 로 받지 않도록 해당 메소드의 위에 배치를 해야한다 app.get('/campgrounds/new', async (req, res) => {
res.render('campgrounds/new');
});
views/campgrounds/new.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>New Campground</title>
</head>
<body>
<form action="/campgrounds" method="POST">
<div>
<label for="title">Title</label>
<input type="text" id="title" name="campground[title]">
</div>
<div>
<label for="location">Location</label>
<input type="text" id="location" name="campground[location]">
</div>
<button>Add Campground</button>
</form>
<a href="/campgrounds">All Campgrounds</a>
</body>
</html>
폼 input 의 name을 campground이란 상위 내용으로 구룹핑 시켜주었다. 이제 폼을 제출하는 종착점인 POST 요청을 받을 수 있도록 생성한다.
app.use(express.urlencoded({extended: true}));
// express 는 req 가 비어있는게 기본 설정이라 이 설정을 추가해야만 한다.
app.post('/campgrounds', async (req, res) => {
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
});
폼 생성이 완료되면 생성된 캠핑장의 상세페이지로 이동하도록 구현되었다.


Edit & Update
이번엔 편집폼을 생성한다.
app.get('/campgrounds/:id/edit', async (req, res) => {
res.render('campgrounds/edit');
});
views/playgrounds/edit.ejs
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Edit Campground</title>
</head>
<body>
<h1>Edit Campground</h1>
<form action="/campgrounds/<%=campground._id%>?_method=PUT" method="POST">
<div>
<label for="title">Title</label>
<input type="text" id="title" name="campground[title]" value="<%=campground.title %>">
</div>
<div>
<label for="location">Location</label>
<input type="text" id="location" name="campground[location]" value="<%= campground.location%>">
</div>
<button>Update Campground</button>
</form>
<a href="/campgrounds/<%= campground._id%>">Back To Campground</a>
</body>
</html>
조금 특이한건 form 요소의 method 는 POST 인데 action 주소값에
_method=PUT
으로 되어있다. ( 꼭 _method 일 필요는 없음 ) 요청은 POST 형식이지만 express 에서 받을땐 PUT 으로 받는단 의미이다. ( 메소드 재정의 ) npm i method-override
하여 사용할 수 있도록 라이브러리를 받고 아래처럼 app 을 설정한다.const methodOverride = require('method-override');
app.use(methodOverride("_method"));
app.put('/campgrounds/:id', async(req, res)=>{
const campground = await Campground.findById(req.params.id);
res.render('campgrounds/edit', {campground});
})
Delete
마지막으로 삭제를 구현한다. 메소드 타입은 delete 이다.
app.delete('/campgrounds/:id', async(req, res)=>{
const { id } = req.params;
await Campground.findByIdAndDelete(req.params.id);
res.redirect(`/campgrounds/`);
})
원래는 어떠한 내용을 지운다면 예컨데 캠핌장이면 관련 정보, 후기와 같은 해당 캠핑장과 관련 내용모두 지워야 원칙이지만 지금은 캠핑장을 지우기만 한다.
삭제버튼은 show.ejs 에 추가한다.
<form action="/campgrounds/<%=campground._id%>?_method=DELETE" method="POST">
<button>Delete</button>
</form>
보충
406. 캠프그라운드 모델 기초

407.
- 기본 시드 설정

코드
const mongoose = require('mongoose');
const cities = require('./cities');
const { places, descriptors } = require('./seedHelpers');
const Campground = require('../models/campground');
mongoose.connect('mongodb://localhost:27017/yelp-camp', {
useNewUrlParser: true,
useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
console.log('Database connected');
});
const sample = (array) => array[Math.floor(Math.random() * array.length)];
// 배열 길이만큼 곱해서 난수 인덱스 만들기
const seedDB = async () => {
await Campground.deleteMany({}); // 기존 데이터 전체 삭제
for (let i = 0; i < 50; i++) {
const random1000 = Math.floor(Math.random() * 1000);
const camp = new Campground({
location: `${cities[random1000].city}, ${cities[random1000].state}`,
title: `${sample(descriptors)} ${sample(places)}`,
// 임의의 title 생성
});
await camp.save();
}
};
seedDB().then(() => {
mongoose.connection.close(); // DB에 데이터 추가 후 연결 끊기
});
410.
중요한 내용
- 폼 input name 그룹핑 방법
- req.body 파싱 이유
- express에선 req.body의 디폴트 값이 undefined 이므로
411.
app.put('/campgrounds/:id', async (req, res) => {
const { id } = req.params;
const campground = await Campground.findByIdAndUpdate(id, { ...req.body.campground });
res.redirect(`/campgrounds/${campground._id}`);
});
412.
app.delete('/campgrounds/:id', async(req, res)=>{
const { id } = req.params;
await Campground.findByIdAndDelete(id);
res.redirect('/campgrounds');
})
delete 라우트
40 . 미들웨어 : Express 의 키 ( Key)
큰 Express 앱을 작성하기 위해 미들웨어는 핵심 개념이다. 단 Express의 미들웨어와 Mongoose 의 미들웨어는 다른 개념이나 마찬가지다. 쿠키와 세션을 다루거나 사람들이 로그인할 때 인증을 구할 때 미들웨어를 사용한다.
Concept
미들웨어는 Exrpess 내의 구성 요소 중 하나 인데, 요청-응답 라이프 사이클 내에서 실행되는 함수이다. 요청(req) 객체와 응답(res)객체에 접근/수정이 가능하다.
아무 정의된 미들웨어를 실행하면 그 미들웨어는 요청,응답 주기를 종료할 수 있음. 또한 다른 미들웨어를 연쇄적으로 호출할 수도 있음.
Express 공식에 의하면 Express 는 미들웨어의 호출의 연속이라고 표현한다.
Morgan-Logger
Morgan 은 HTTP 요청을 터미널에 로그로 남겨주는 미들웨어다.
신규 Express 프로젝트를 생성하고
index.js
를 아래처럼 만든다.const express = require('express');
const app = express()
app.get('/', (req, res) => {
res.send('HOME PAGE!');
})
app.get('/dogs', (req, res) => {
res.send('WOOF WOOF');
})
app.listen(3000, () => {
console.log('App is running on localhost:3000');
})
이제 margan 을 설치하고
npm i morgan
import 나 require 로 불러오고 실행하기만 하면 Moragn 을 사용 가능하다.const morgan = require('morgan');
morgan('tiny')
이제 Express 애플리케이션이 morgan 을 사용하도록 설정해준다. 그 전에 use 의 예를 설명해본다. 만약 use 내에 어떤 함수가 있다면 그 함수는 모든 요청에 항상 실행된다.
app.use(() => {
console.log("HEYYY!!!")
})
단 지금과 같은 예는 response 가 없어서 timeout 오류가 발생할 것이다. 이제 morgan 을 적용해본다.
app.use(morgan('tiny'))
//app.use(morgan('common'))
적용 후 접속해보면 아래와 같이 로그가 찍힌다. 해당 기능은 어떤 응답이 왔는가 등등의 내용이 있으며 로그의 내용을 설정할 수도 있다.


Define
정의하기
app.use 는 코드를 실행하는 방법이다. 이 곳에 원하는 함수를 넣을 수 있다. 하지만 이 방법으로는 use 종료 후 다음 미들웨어를 호출 할 수는 없다.

app 의 메소드는 자동으로 전달되는 세 번째 매개변수가 있다. 보통 next 라고 부른다 이름을 바꿔도 되지만 req, res와 같은 암묵적 공식이므로 이 이름을 쓰길 권장한다. next 는 다음 것을 참조하고 실행한다.
app.use((req, res, next) => {
console.log("THIS IS MY FIRST MIDDLEWARE!!!");
next();
//return next();
})

모든 동사의 요청에 실행되는 함수를 next 로서 설정되었다. get 이든 post 든 요청이든 실행 된다. 위처럼 use 로 설정했다면 항상 실행 된다. 그러면 해당 함수 다음에 use 가 있다면 그것도 실행된다.
또한
next()
는 다음에 실행될 함수를 참조하는 역할이므로, 이후에 실행된 코드도 실행된다. 보통 하는 경우는 없다. return next()
라고 정의하면 다음의 코드 실행을 막는다.Morgan 으로 생성된 로그는 항상 마지막에 로그에 기록되었는데, 해당 라이브러리는 응답이 완료된 후에 실행되도록 설정되어 있다.
Middleware 심화
Morgan 처럼 라우트 핸들러(
app.get()
, app.post()
)가 실행되기 전에 요청 정보에 접근해 수정하거나, 요청 객체에 데이터를 추가할 수 있는 미들웨어를 작성해볼 수 있음.특정 라우트에 접근 못하게 인증 받은 요청인지 확인해보거나, 현재 시각을 추가하거나 여러가지 유형이 있다.
간단히 미들웨어 이름을 지을 수도 있고 기명함수 표현을 쓰거나 함수를 미리 정의해볼 수도 있음
app.use((req, res, next) => {
console.log(req.method.toUpperCase(), req.path);
next();
})

이렇게 로그를 만들어볼 수도 있음
req객체에 변형을 줄 수도 있는데 아래 코드는 모든 요청 method를 GET 으로 바꾼다.
app.use((req, res, next) => {
req.method = 'GET'
console.log(req.method, req.path);
next();
})
아래의 코드는 요청 시간을 req 에 담아서 보내준다.
app.use((req, res, next) => {
req.requstTime = Date.now();
console.log(req.method, req.path);
next();
})
app.get('/', (req, res) => {
console.log(req.requstTime);
res.send('HOME PAGE!');
})
app.get('/dogs', (req, res) => {
console.log(req.requstTime);
res.send('WOOF WOOF');
})

코드는 위에서 아래로 실행되기 때문에 만약 use 가 라우트 핸들러 아래에 있다면 라우트 핸들러는 req 를 받을 수 없게 된다.
본 스터디는 Udemy의 <【한글자막】 The Web Developer 부트캠프 2022> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
프밍 스터디는 Udemy Korea와 함께 합니다.