웹 개발 실전 프로젝트 - 4주차
Yelp Camp 활용한 프로젝트
404 page Setting
지금까지 app.use 는 모든 요청에 상관없이 함수를 실행하고 req 를 전달하는데 사용했다.
하지만 use 는 문자열이나 경로, regex로 라우트를 설정할 수 있음.
app.use('/dogs', (req, res, next) => {
console.log("I love dogs");
next();
})
app.get('/dogs', (req, res) => {
console.log(req.requstTime);
res.send('WOOF WOOF');
})

여기서 POST /dogs 로 요청했으면
404 error
가 발생하지만 I love dogs 는 출력 된다.특정 경로에만 미들웨어를 실행할 때 이 방법을 사용할 수 있음 아무것도 매칭되지 않았을 때 404 로 이동하게 될 때, 맨 아래에 정의할 수 있음
//index.js의 라우터 핸들러 메소드들 맨 아래에...
app.use((req, res)=>{
res.status(404).send("NOT FOUND");
})
Authentication Demo
실제로는 인증을 만드는 건 많은 노력이 필요하지만 이번에는 간단하게 만들어본다. /secret 라는 경로에 인증 기능을 추가해본다.
쿼리 문자열로 인증을 넣어보기 위해 아래와 같은 코드를 생성한다.
app.use((req, res, next)=>{
console.log(req.query);
next();
})
http://localhost:3000/secret?food=chicken 실행시

이제 쿼리문자열에 password 를 받도록 해본다.
app.use((req, res, next)=>{
const { password } = req.query;
if(password === "chickennugget"){
next();
}
res.send("SORRY YOU NEED A PASSWORD!!")
})
이 코드는 모든 라우트 핸들러에 대해 실행을 하기 때문에 queryString 으로 항상 패스워드를 전달 해야 한다. 비밀번호가 틀렸다면 여기서 종료되고 맞다면, next 가 실행된다.
기존처럼 http://localhost:3000/dogs 를 실행하면 SORRY YOU NEED A PASSWORD!! 가 출력 된다. 여기서 http://localhost:3000/dogs?password=chickennugget 로 접근해야 next 가 출력되서 WOOF WOOF 가 출력 되었다.
Credential only Secret page
위의 경우는 모든 페이지에 대해서 패스워드를 요구하는데, 실제로는 특정한 라우트 에서만 비밀번호를 요구하도록 만들어야 한다.
req.path 가 특정 경로일 때 하도록 if 문을 추가하는 방법이 있지만 최선의 방법은 아니다. 특정한 경로일 때만 미들웨어를 사용하도록 설정하는 방법을 쓰면 된다.
const verifyPassword = (req, res, next)=>{
const { password } = req.query;
if(password === "chickennugget"){
next();
}
res.send("SORRY YOU NEED A PASSWORD!!")
}
기존 use 미들웨어를 해당 함수로 선언하고
app.get('/secret', verifyPassword, (req, res)=> {
res.send('MY SECRET');
})
이렇게 설정하면 secret에 접근할 때 verifyPassword 가 먼저 실행되고 다음 매개변수의 함수가 Callback으로 실행된다. 이런 식으로 secret 에만 인증 기능이 적용되었다.
41. YelpCamp: 기본 스타일 추가하기
421. Layouts를 위한 새로운 EJS 툴
- ‣ 패키지로 여러 기능을 설치할 수 있는데 그 중 layout을 사용할 것이다.
- boilerplate 를 통해 콘텐츠 사이에 짧은 변수 사용으로 코드를 삽입하는 것이 가능 (head, footer 파일 분할보다 간편)
- 설치
npm i ejs-mate
const ejsMate = require('ejs-mate');
// ...
app.engine('ejs', ejsMate);
- ejs 파일을 실행하거나 파싱할 때 쓰이는 Express의 디폴트 엔진 대신 ejsMate를 설정
- views 디렉터리에 layouts 폴더 생성 후 boilerplate.ejs 파일 추가
- 모든 페이지의 기본 상용구 역할
<!DOCTYPE html>
<html lang="ko">
<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>BOILERPLATE!!!</title>
</head>
<body>
<main>
<%- body %>
</main>
</body>
</html>
<% layout('layouts/boilerplate') %>
<h1>All Campgrounds</h1>
<div>
<a href="/campgrounds/new">Add Campground</a>
</div>
<ul>
<% for (let campground of campgrounds) { %>
<li><a href="/campgrounds/<%= campground._id %>"><%= campground.title %></a></li>
<% } %>
</ul>
- ejs에 작성한 콘텐츠들을 layout이 찾아내 boilerplate에 body로 전달한다.
- 스타일 시트나 JavaScript 등을 삽입해 common layout을 만들 수 있다.
- navbar를 삽입한다면 boilerplate.ejs 에만 추가하면 된다.
422. 부트스트랩5! 상용구 코드
423. 내비게이션 바 부분
<nav class="navbar navbar-expand-lg bg-dark navbar-dark sticky-top">
<div class="container-fluid">
<a class="navbar-brand" href="#">YelpCamp</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNavAltMarkup"
aria-controls="navbarNavAltMarkup" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarNavAltMarkup">
<div class="navbar-nav">
<a class="nav-link" href="/">Home</a>
<a class="nav-link" href="/campgrounds">Campgrounds</a>
<a class="nav-link" href="/campgrounds/new">New Campground</a>
</div>
</div>
</div>
</nav>
<%- include('../partials/navbar') %>
<main class="container mt-5">
<%- body %>
</main>
424. Footer 부분
- body
vh-100
view height 설정 후 footermt-auto
425. 이미지 추가하기
- Unsplash Source API 사용
- source.unsplash.com/collection/
컬렉션ID
로 요청을 전송하면 된다. - https://source.unsplash.com/collection/483251
- 페이지를 로딩할 때마다 컬렉션 내의 사진이 랜덤으로 나온다.
- Campground의 시드 파일 수정
const seedDB = async () => {
await Campground.deleteMany({});
for (let i = 0; i < 50; i++) {
const random1000 = Math.floor(Math.random() * 1000);
const price = Math.floor(Math.random() * 20) + 10;
const camp = new Campground({
location: `${cities[random1000].city}, ${cities[random1000].state}`,
title: `${sample(descriptors)} ${sample(places)}`,
image: 'https://source.unsplash.com/collection/483251',
description:
'Lorem ipsum dolor sit amet consectetur adipisicing elit. Earum a asperiores rerum iusto dolor ipsam iure totam laboriosam reiciendis impedit voluptatibus molestiae, nisi obcaecati officiis molestias, unde in, dolorem tempora?',
price,
});
await camp.save();
}
};
seedDB().then(() => {
mongoose.connection.close();
});
node seeds/index.js

426. 캠프그라운드 인덱스 스타일링
427. 새로운 (폼)양식 스타일링
438. YelpCamp의 다음 목표는?
- 오류 처리
- 클라이언트 측 유효성 검사
- 서버 측 유효성 검사
- 들어오는 req.body의 유효성을 검사해 패턴과 맞는지 확인
- Joi
- 오류 템플릿 설정
439. 클라이언트 양식 유효성 검사
- 폼에 클라이언트 측 유효성 검사 추가하기
- input에
required
어트리뷰트를 추가해 브라우저에서 처리시키는 방법 - 브라우저마다 구현 방법이 다르고 표준화되어 있지 않다.
부트스트랩으로 폼 유효성 검사하기
<form action="/campgrounds" method="POST" novalidate class="validated-form">
- 유효성 검사가 필요한 input들에
required
를 추가한 후, 부모 form에novalidate
어트리뷰트를 추가하면 브라우저 대신 부트스트랩이 유효성 검사를 수행할 수 있다.
- 유효성 검사가 필요한 폼에 클래스를 추가한다.
validated-form
const forms = document.querySelectorAll('.validated-form');
// Array.prototype.slice.call(forms)
Array.from(forms).forEach(function (form) {
form.addEventListener('submit', (event) => {
if (!form.checkValidity()) {
event.preventDefault();
event.stopPropagation();
}
form.classList.add('was-validated')
}, false)
})
- 이벤트리스너를 추가하고 submit 시
form.checkValidity
호출

valid feedback 추가하기
- 유효성 검사를 수행할 input 마다 밑에
valid-feedback
클래스를 가진 div를 추가한다.
<div class="valid-feedback">Looks good!</div>

440. 기본 오류 처리기
- Price에 숫자가 아닌 값을 입력했을 때 유효성 오류 처리 및 Promise 처리
- async 함수에서 try…catch문 사용
- 시도 및 저장할 때 오류가 있으면 검출하고, 해당 오류로 next를 호출하면 기본 오류 핸들러를 실행시켜 오류 메시지를 출력한다.
app.post('/campgrounds', async (req, res, next) => {
try {
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
} catch (e) {
next(e);
}
});
// ...
app.use((err, req, res, next) => {
res.send('Oh boy, something went wrong!');
});


441. ExpressError Class 정의하기
- 의도적으로 발생시킬 수 있는 표준화된 오류들 알아보기
- ExpressError 클래스 생성
class ExpressError extends Error {
constructor(message, statusCode) {
super();
this.message = message;
this.statusCode = statusCode;
}
}
module.exports = ExpressError;
- async 오류 검출을 위해 래퍼(wrapper) 함수 추가하기
module.exports = (func) => {
return (req, res, next) => {
func(req, res, next).catch(next);
};
};
- 앱 라우트에 추가하기
const catchAsync = require('./utils/catchAsync');
// ...
app.post('/campgrounds', catchAsync(async (req, res, next) => {
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
}));
442. 오류 더 알아보기
- 404 오류 추가
- 상단의 모든 코드에 요청이 닿지 않는 경우 실행되어야 함 ⇒ 하단에 위치
app.all('*', (req, res, next) => {
res.send('404!!!!');
});
- ExpressError 클래스 사용하기
const ExpressError = require('./utils/ExpressError');
// ...
app.all('*', (req, res, next) => {
res.status();
next(new ExpressError('Page Not Found', 404));
});
- ⭐ app.use로 메시지와 상태 코드 가져오기
- new ExpressError를 next로 전달하기 때문에, 오류 핸들러가 실행되면 ExpressError가 err가 된다. (혹은 다른 코드의 오류가 될 수도 있다.)
app.use((err, req, res, next) => {
const { statusCode = 500, message = 'Something went wrong' } = err;
res.status(statusCode).send(message);
});
- 유효하지 않은 URL을 입력하면 Page Not Found가 뜨고, 오류를 발생시킬 만한 페이지에서 유효하지 않은 ID를 입력하면 오류 메시지가 출력된다.




- 어떤 라우트에서든 new ExpressError를 작성해서 next로 전달하면, 그 코드에 있는 상태 코드가 적용된다.
- 입력된 캠프 정보가 없는 상황을 가정하고 오류 처리하기
- 데이터를 잘못 기입하는 등의 클라이언트 오류 상태 코드인 400으로 지정
app.post('/campgrounds', catchAsync(async (req, res, next) => {
if (!req.body.campground) throw new ExpressError('Invaild Campground Data', 400);
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
})
);
- 폼에는 현재 유효성 검사가 적용되어 있어 제출할 수 없으므로, Postman으로 데이터를 누락한 채 POST 요청을 보내면 다음과 같이 의도대로 메시지와 상태 코드가 출력된다.

443. 오류 템플릿 정의하기
<% layout('layouts/boilerplate')%>
<div class="row">
<div class="col-6 offset-3">
<div class="alert alert-danger" role="alert">
<h4 class="alert-heading">Error!</h4>
<p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Exercitationem itaque, dolores vero molestias quis
doloremque amet consequatur quisquam ipsum, rem sapiente quae!</p>
<hr>
<p class="mb-0">Whenever you need to, be sure to use margin utilities to keep things nice and tidy.</p>
</div>
</div>
</div>
app.use((err, req, res, next) => {
const { statusCode = 500, message = 'Something went wrong' } = err;
res.status(statusCode).render('error');
});

- 오류 객체 error.ejs로 전달하기
- err 구조분해 시 statusCode를 꺼내는 건 상관없지만, 설정한 message는 오류 객체에 업데이트 되지 않기 때문에 디폴트 값 설정을 바꿔야 한다.
app.use((err, req, res, next) => {
const { statusCode = 500 } = err;
if(!err.message) err.message = 'Oh No, Something Went Wrong!';
res.status(statusCode).render('error', { err });
});
<h4 class="alert-heading"><%= err.message %></h4>

- 개발 목적이라면
<%= err.stack %>
을 적용할 수도 있다.
444. JOI 스키마 유효성 검사
JavaScript 유효성 검사 도구 Joi
- 스키마를 간단히 설정하고, 스키마를 통해 데이터 유효성 검사를 할 수 있어 직접 작성하지 않아도 된다.
- 설치
npm i joi
const Joi = require('joi');
// ...
app.post('/campgrounds', catchAsync(async (req, res, next) => {
// if (!req.body.campground) throw new ExpressError('Invaild Campground Data', 400);
const campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required(),
price: Joi.number().required().min(0),
image: Joi.string().required(),
location: Joi.string().required(),
description: Joi.string().required(),
}).required(),
// const result = campgroundSchema.validate(req.body);
// if (result.error) {
// throw new ExpressError(result.error.details, 400);
// }
const { error } = campgroundSchema.validate(req.body);
if (error) {
const msg = error.details.map(el => el.message).join(',');
throw new ExpressError(msg, 400);
}
const campground = new Campground(req.body.campground);
await campground.save();
res.redirect(`/campgrounds/${campground._id}`);
})
);
- 이제 Postman이나 AJAX 등을 활용하려 해도 서버 측에서 데이터를 검증할 수 있다.

445. JOI 유효성 검사 미들웨어
- 스키마 유효성을 검사하는 재사용 가능한 미들웨어 만들기
const validateCampground = (req, res, next) => {
const campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required(),
price: Joi.number().required().min(0),
image: Joi.string().required(),
location: Joi.string().required(),
description: Joi.string().required(),
}).required(),
})
const { error } = campgroundSchema.validate(req.body);
if (error) {
const msg = error.details.map(el => el.message).join(',');
throw new ExpressError(msg, 400);
} else {
next();
}
}
// ...
app.post('/campgrounds', validateCampground, catchAsync(async (req, res, next) => {
// ...
- 라우트 자체, 라우트 핸들러에는 유효성 검사 로직이 없고, 인수로 validateCampground를 전달해 호출하면 유효성 검사를 할 수 있다.

- validateCampground의 스키마를 파일로 분리한다.
const Joi = require('joi');
module.exports.campgroundSchema = Joi.object({
campground: Joi.object({
title: Joi.string().required(),
price: Joi.number().required().min(0),
image: Joi.string().required(),
location: Joi.string().required(),
description: Joi.string().required(),
}).required(),
});
- 리뷰나 사용자 등 원하면 다른 스키마들을 schemas 파일에 추가할 수 있다.
- 다수의 스키마를 내보내기 할 예정이므로 불러올 때도 구조분해한다.
const { campgroundSchema } = require('./schemas');
// ...
const validateCampground = (req, res, next) => {
const { error } = campgroundSchema.validate(req.body);
if (error) {
const msg = error.details.map((el) => el.message).join(',');
throw new ExpressError(msg, 400);
} else {
next();
}
};
// ...
app.post('/campgrounds', validateCampground, catchAsync(async (req, res, next) => {
// ...
app.put('/campgrounds/:id', validateCampground, catchAsync(async (req, res) => {
무언가를 create 하거나 update 하기 전에 유효성 검사를 위해 사용하므로 model, mongoose Schema와는 다르다.
Joi 메서드를 사용하는 스키마 파일에 정의한 JavaScript 객체용 패턴.
446. 섹션 주제
- 데이터베이스를 구조화 및 저장하는 방법과 관계(relationships) 이해하기
- 데이터는 독립적으로 존재하지 않고 다양한 엔티티 사이에서 서로 연결됨
- SQL
- 세 가지 유형의 관계와 모델링을 위한 접근법
- One to Few
- One to Many
- One to Bajillions
- Mongoose 메서드
populate
- Mongo 스키마 디자인
447. Mongo의 관계 개요
- Mongo의 다양한 데이터간 관계 중 몇 가지 유형을 모델링하는 방법과 어떤 패턴을 사용할 수 있는지 알아보자.
448. SQL 관계 개요
- 관계형 데이터베이스
- MySQL, PostgreSQL Lite, Microsoft SQL Server 등의 SQL 데이터베이스들은 모두 테이블에 정보를 저장한다.
- 테이블은 엄격한 스키마를 설정해야 하는 견고한 구조
- 정보 중복의 비효율성 때문에 한 테이블에서 다른 테이블을 참조하고, 정보를 합칠 수 있다.
- One to Many의 예: 사용자 ID와 여러 게시물(또는 댓글) 관계가
- Many to Many의 예: 영화와 영화배우의 관계
Mongo가 Node, Express와 잘 어울린다고 해서, NoSQL 데이터베이스가 절대적인 정답이라고는 할 수 없다. SQL도 배워야 한다. 하지만 Mongo의 데이터 구조화에는 유연성과 자율성이라는 강점이 있다.
449. One to Few 관계

- 웹 상에서 사용자(users)와 그들이 만든 것(게시물, 댓글, …)과 연관이 깊고, 흔한 형태
- One to Many에서 ‘Many’가 얼마나 많은가에 따라 세 가지 유형으로 나뉜다.
- 그 중 One to ‘Few’는 사용자와 주소를 생각하면 이해하기 쉽다.
- 사용자가 두 가지나 그 이상의 여러 항목을 가질 때 좋은 방법
const mongoose = require('mongoose');
mongoose
.connect('mongodb://localhost:27017/relationshipDemo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('MONGO CONNECTION OPEN!!!');
})
.catch((err) => {
console.log('OH NO MONGO CONNECTION ERROR!!!!');
console.log(err);
});
const userSchema = new mongoose.Schema({
first: String,
last: String,
addresses: [
{
_id: { id: false },
// address에 id가 생성되지 않게 옵션으로 끌 수 있다.
street: String,
city: String,
state: String,
country: String,
},
],
});
const User = mongoose.model('User', userSchema);
const makeUser = async () => {
const u = new User({
first: 'Harry',
last: 'Potter',
});
u.addresses.push({
street: '123 Sesame St.',
city: 'New York',
state: 'NY',
country: 'USA',
});
const res = await u.save();
console.log(res);
};
const addAddress = async (id) => {
const user = await User.findById(id);
user.addresses.push({
street: '99 3rd St.',
city: 'New York',
state: 'NY',
country: 'USA',
});
const res = await user.save();
console.log(res);
};
- 하위 문서나 정보를 부모 문서 안에 직접 임베드하는 구조는 임베드하려는 정보 집합의 크기가 작을 때 더 적합하다.
450. One to Many 관계

- One to Few의 데이터 크기가 small, One to Bajillions의 데이터 크기가 large라면, One to Many의 데이터 크기는 medium에 해당된다고 볼 수 있다.
- One to Many 방식은 부모 문서에 정보를 직접 임베드하는 것이 아니라, 다른 곳에 정의되어 있는 참조(reference)를 임베드하는 방식이다.
- 보통 객체 ID를 활용하고, SQL 방식과 매우 유사하다.
Product 스키마 정의 및 모델 생성
const mongoose = require('mongoose');
const { Schema } = mongoose; // 구조 분해 해두면 편리
mongoose
.connect('mongodb://localhost:27017/relationshipDemo', {
useNewUrlParser: true,
useUnifiedTopology: true,
})
.then(() => {
console.log('MONGO CONNECTION OPEN!!!');
})
.catch((err) => {
console.log('OH NO MONGO CONNECTION ERROR!!!!');
console.log(err);
});
const productSchema = new Schema({
name: String,
price: Number,
season: {
type: String,
enum: ['Spring', 'Summer', 'Fall', 'Winter'],
},
});
Product.insertMany([
{ name: 'Goddess Melon', price: 4.99, season: 'Summer' },
{ name: 'Sugar Baby Watermelon', price: 4.99, season: 'Summer' },
{ name: 'Asparagus', price: 3.99, season: 'Spring' },
])
const Product = mongoose.model('Product', productSchema);
Farm 모델 생성
- Mongoose 공식 문서 populate 항목을 보면, 레퍼런스를 문서에 넣기 위해 거쳐야 하는 작업 단계가 설명되어 있다.
- 제일 먼저 할 일은 각 상품의 타입을 객체 ID로 설정해 주는 것
Schema.Types.ObjectId
const farmSchema = new Schema({
name: String,
city: String,
products: [{ type: Schema.Types.ObjectId, ref: 'Product' }],
});
const Farm = mongoose.model('Farm', farmSchema);
ref
옵션을 통해서는 Mongoose에게 채우기 작업 시 사용할 모델을 알려줄 수 있다.- 모델 이름인 Product를 전달한다.
- 데이터베이스를 쿼리하고 지정된 농장에 다양한 상품들을 채워 넣을 수 있게 된다.
const makeFarm = async () => {
const farm = new Farm({ name: 'Full Belly Farms', city: 'Guinda, CA' });
const melon = await Product.findOne({ name: 'Goddess Melon' });
farm.products.push(melon);
await farm.save();
console.log(farm);
};
makeFarm();

const addProduct = async () => {
const farm = await Farm.findOne({ name: 'Full Belly Farms' });
const watermelon = await Product.findOne({ name: 'Sugar Baby Watermelon' });
farm.products.push(watermelon);
await farm.save();
console.log(farm);
};
addProduct();

- 위와 같이 상품이 ObjectId 형태로 farms의 products 배열에 들어가는 것을 볼 수 있다.
- farms는 객체 ID 말고는 상품 자체에 대한 정보를 가지고 있지 않다.
451. Mongoose의 Populate 명령어
Farm.findOne({ name: 'Full Belly Farms' })
.populate('products')
.then((farm) => console.log(farm));
452. One to “Bajillions” 관계

본 스터디는 Udemy의 <【한글자막】 The Web Developer 부트캠프 2022> 강의를 활용해 진행됐습니다. 강의에 대한 자세한 정보는 아래에서 확인하실 수 있습니다.
프밍 스터디는 Udemy Korea와 함께 합니다.