Express를 사용하는 이유
- 첫째, 쉽고 직관적인 메서드로 쉽게 라우팅을 할 수 있다.
- 둘째, 미들웨어를 사용해 클라이언트와 서버 간 필요한 처리를 해 줄 수 있다.
- 셋째, MongoDB와 같은 데이터베이스와 연동해 서버를 관리할 수 있다.
- Express 설치. 현재는 Express 5 버전으로 설치된다.
cd express-tutorial
npm init
npm i express
nodemon: 서버 코드를 수정했을 때, 기존 실행을 멈추고 자동으로 다시 실행하게 해 줌.
- 이걸 안 쓰면, 직접 서버 실행 종료하고 다시 실행시켜야 함.
npm i nodemon --save-dev --g
nodemon app
--save-dev: 앱 개발 중에서만 사용. 배포 시 사용하지 않음
--g: 한번 설치 시, 시스템의 다른 곳에서도 사용 가능.
- 이후
nodemon app으로 실행
const express = require("express");
const app = express();
app.listen(3000, () => {
console.log("서버 실행 중");
});
라우팅
- GET: 서버에서 정보를 읽는다
- POST: 서버로 정보를 보낸다
- PUT: 서버의 정보를 수정한다
- DELETE: 서버의 정보를 삭제한다
req, res
app.get("/", (req, res) => {
res.send("Hello, Node!");
});
app.get("/contacts", (req, res) => {
res.send("Contacts Page");
});
- 기본적으로
app.메서드(경로, (req, res) => {실행내용})과 같이 라우팅 설정.
req: 요청 객체로, 요청과 관련된 정보들이 담김.
res: 응답 객체로, 응답 시 전송 및 렌더링에 사용.
- json 형태로 응답할 땐
res.json(json형태)
- 일반적인 텍스트 형태로 응답할 땐
res.send(응답텍스트)를 사용하면 된다.
라우트 파라미터 (req.params)
app.get("/contacts/:id", (req, res) => {
res.send(`View Contact for ID: ${req.params.id}`);
});
- 요청 URL 뒤에
:를 붙인 후, 변수 작성
- e.g.,
id에 맞는 연락처만 가져올 때(GET)/수정할 때(PUT)/삭제할 때(DELETE)...
- e.g.,
/요청 URL/:id
- 이후
req.params로 값을 읽을 수 있음
미들웨어
- 클라이언트 요청(
req)와 서버 응답(res)간 중계자 역할을 하는 함수.
- 요청 전처리: 클라이언트 요청이 서버로 도착하기 전, form의 내용 검증, 사용자 인증 등 필요한 처리를 해 줌
- 응답 처리: 서버 응답이 클라이언트로 도착하기 전, 자료를 적절한 형태로 변환하거나 오류를 처리해 줌
- 그 외 라우팅을 보다 쉽게 해 주는 미들웨어도 존재
app.use로 사용 가능.
Router 미들웨어
- 같은 경로로 들어오는 다양한 요청 (GET, POST, PUT, DELETE...)을 묶어서 관리하기에 용이함
- 경로별로 파일을 분리하여, 코드 구조가 깔끔해짐
- 코드의 구조를 더 깔끔하게 관리할 수 있음
const express = require("express");
const app = express();
app.get("/", (req, res) => {
res.send("Hello, Node!");
});
app.use("/contacts", require("./routes/contactRoutes.js"));
app.listen(3000, () => {
console.log("서버 실행 중");
});
app.use("/contacts", require("./routes/contactRoutes.js"));로, /contacts 경로로 들어오는 모든 요청을, contactRoutes.js에 지정한 라우터로 위임
const express = require("express");
const router = express.Router();
router
.route("/")
.get((req, res) => {
res.send("Contacts Page");
})
.post((req, res) => {
res.send("Create Contacts");
});
router
.route("/:id")
.get((req, res) => {
res.send(`View Contact for ID: ${req.params.id}`);
})
.put((req, res) => {
res.send(`Update Contact for ID: ${req.params.id}`);
})
.delete((req, res) => {
res.send(`Delete Contact for ID: ${req.params.id}`);
});
module.exports = router;
router.route("경로") 이후 .get, .put, .delete, .post를 연달아 작성하는 식으로, 동일 경로로의 여러 request 유형 처리 가능.
바디파서 미들웨어
- 요청이 json이나 urlencoded 형태로 들어올 시, 이를 처리하기 전 파싱해줘야 함
- cf. urlencoded -> HTML form으로 요청을 보낼 때.
- cf. json -> ajax, fetch, axios 등으로 JSON 형태의 요청이 주어질 때.
- 이 역시 미들웨어의 역할. Express에 내장된
express.json(), express.urlencoded()를 사용하면 됨.
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use("/contacts", require("./routes/contactRoutes.js"));
요청에 포함된 json 등 데이터 확인 (req.body)
- POST 요청으로 json 형태 데이터가 주어진 경우,
req.body를 이용해서 값을 꺼내올 수 있음.
- 바디파서 미들웨어는, 라우터 미들웨어보다 무조건 먼저 선언되어야 함. 라우터는 파싱이 되었다는 전제 하에 실행되기 때문...
{
"name": "SSR",
"email": "lalalahoha@mandu.com",
"height": 184
}
router
.route("/")
.get((req, res) => {
res.send("Contacts Page");
})
.post((req, res) => {
const { name, email, height } = req.body;
if (!name || !email || !height) {
return res.send("필수 값이 입력되지 않았습니다.");
}
res.send("Create Contacts");
});
MongoDB / Mongoose
Mongoose: express.js에서 MongoDB를 쉽게 사용할 수 있게 돕는 패키지.
dotenv: 환경 변수 관리용.
npm i mongoose dotenv
DB_CONNECT=mongodb://username:password@host:port/dbname
.env 파일은 보안상 깃허브에 푸시하면 안됨! .gitignore 사용하기.
const mongoose = require("mongoose");
require("dotenv").config();
const dbConnect = async () => {
try {
const connect = await mongoose.connect(process.env.DB_CONNECT);
console.log("DB Connected");
} catch (err) {
console.log(err);
}
};
module.exports = dbConnect;
try, catch: 연결 실패 시, 바로 서버가 터지지 않고 에러를 확인할 수 있음.
async, await: await이 앞서 선언된 mongoose.connect가 실행 완료될 때까지, 다음 코드인 console.log("DB Connected")가 실행되지 않음.
const express = require("express");
const dbConnect = require("./config/dbConnect.js");
const app = express();
dbConnect();
스키마와 모델
- 스키마: MongoDB 컬렉션에 들어갈 도큐먼트의 형태
- 컬렉션은 "전화부록부", 도큐먼트는 "각 전화번호",
- 스키마는 "이름, 이메일, 전화번호..." 등 전화부록부에 들어가는 정보 유형이라고 생각하면 된다.
mongoose.Schema로 스키마를 만든 뒤, mongoose.Model로 모델로 바꾸어준다.
- 모델은 Mongoose에서 컬렉션에 접근하기 위해 만든 JS 객체.
- 모델 이름을
Contact로 지으면, 컬렉션은 자동으로 Contacts(복수형)으로 이름이 지어짐에 유의.
const mongoose = require("mongoose");
const contactSchema = new mongoose.Schema(
{
name: {
type: String,
required: true,
},
email: {
type: String,
},
height: {
type: Number,
required: [true, "키는 꼭 기입해 주세요."],
},
},
{ timestamps: true }
);
const Contact = mongoose.model("Contact", contactSchema);
module.exports = Contact;
- 스키마는 이처럼
mongoose.Schema에 JSON형태로 작성하면 된다.
name, email, height는 각 도큐먼트의 필드명
type으로 자료형을 지정하고 (String, Number 등)
required로 값이 필요한지 지정 가능.
CRUD 구현
POST: Create. 자원을 새로 만든다. -> 모델명.create()
GET: Read. 자원을 가져온다. -> 모델명.find()(있는 대로), 모델명.findOne()(첫 번째만)
PUT: Update. 자원을 수정한다. -> 모델명.updateMany()(있는 대로), 모델명.updateOne()(첫 번째만)
DELETE: Delete. 자원을 삭제한다. -> 모델명.deleteMany()(있는 대로), 모델명.deleteOne()(첫 번째만)
모델명.findById, 모델명.findByIdAndUpdate, 모델명.findByIdAndDelete도 존재
- 하는 역할은 대충 이름 보면 짐작이 되실 거고...
const express = require("express");
const router = express.Router();
const {
getAllContacts,
createContact,
getOneContact,
editOneContact,
deleteOneContact,
} = require("../controllers/contactController.js");
router.route("/").get(getAllContacts).post(createContact);
router
.route("/:id")
.get(getOneContact)
.put(editOneContact)
.delete(deleteOneContact);
module.exports = router;
- 일단 앞선
contactRoutes.js 파일에 작성된 콜백 함수들을, 별도 함수로 contactController.js라는 다른 파일에 저장해 둘 거임.
const asyncHandler = require("express-async-handler");
const Contact = require("../models/contactModel");
const getAllContacts = asyncHandler(async (req, res) => {
const contact = await Contact.find();
res.status(200).json(contact);
});
const createContact = asyncHandler(async (req, res) => {
const { name, email, height } = req.body;
if (!name || !email || !height) {
return res.status(400).send("필수 값이 입력되지 않았습니다.");
}
const contact = await Contact.create({ name, email, height });
res.status(201).json(contact);
});
const getOneContact = asyncHandler(async (req, res) => {
const contact = await Contact.findById(req.params.id);
if (!contact) {
return res.status(404).send("Contact not found");
}
res.status(200).json(contact);
});
const editOneContact = asyncHandler(async (req, res) => {
const id = req.params.id;
const { name, email, height } = req.body;
const contact = await Contact.findByIdAndUpdate(
id,
{ name, email, height },
{ new: true }
);
if (!contact) {
return res.status(404).send("Contact not found");
}
res.status(200).json(contact);
});
const deleteOneContact = asyncHandler(async (req, res) => {
const contact = await Contact.findByIdAndDelete(req.params.id);
if (!contact) {
return res.status(404).send("Contact not found");
}
res.status(204).send();
});
module.exports = {
getAllContacts,
createContact,
getOneContact,
editOneContact,
deleteOneContact,
};
- 여기서는
res.status(status번호)로 응답 코드도 보내는데, 클라이언트 쪽에서 응답 코드를 보고 성공/실패 여부를 확인해 적절한 처리를 해 줄 수 있음.
- 보통
2XX는 성공, 나머지 숫자는 실패로 처리.
asyncHandler: 별도 설치해야함. 설치하면 try~catch 문 없이도, 요청 실패 시 자동으로 예외 처리 가능.
- 명시적으로 json 형식으로 클라이언트에게 반환 시
res.json을, 아닌 경우(간단한 문자열 등) res.send 사용하면 됨.
이메일 만두가 눈에 띄는군요🥟