[웹개발] Express.js를 쓰는 이유?

방법이있지·2025년 8월 16일

웹개발

목록 보기
2/19
post-thumbnail

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으로 실행
// index.js
// express 모듈 불러오고, 서버 app 실행하기
const express = require("express");
const app = express();

// 3000번 포트에서 실행
app.listen(3000, () => {
  console.log("서버 실행 중");
});

라우팅

  • GET: 서버에서 정보를 읽는다
  • POST: 서버로 정보를 보낸다
  • PUT: 서버의 정보를 수정한다
  • DELETE: 서버의 정보를 삭제한다

req, res

// GET: 서버에서 정보를 읽는다
app.get("/", (req, res) => {
  // 요청 경로가 "/"인 경우, 응답에 "Hello, Node!" 전송.
  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)

// 연락처 1개 가져오기
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...)을 묶어서 관리하기에 용이함
  • 경로별로 파일을 분리하여, 코드 구조가 깔끔해짐
  • 코드의 구조를 더 깔끔하게 관리할 수 있음
// index.js
// express 모듈 불러오고, 서버 app 실행하기
const express = require("express");
const app = express();

// GET: 서버에서 정보를 읽는다
app.get("/", (req, res) => {
  // 요청 경로가 "/"인 경우, 응답에 "Hello, Node!" 전송.
  res.send("Hello, Node!");
});

// POST도 app.post 형태로 비슷하게 할 수 있음.

// contacts 경로의 요청이 발생한 경우
// 라우터 미들웨어를 가져와 사용
app.use("/contacts", require("./routes/contactRoutes.js"));

// 3000번 포트에서 실행
app.listen(3000, () => {
  console.log("서버 실행 중");
});
  • app.use("/contacts", require("./routes/contactRoutes.js"));로, /contacts 경로로 들어오는 모든 요청을, contactRoutes.js에 지정한 라우터로 위임
// routes/contactRoutes.js
const express = require("express");
const router = express.Router();

// 라우터 미들웨어
// 같은 경로 내 다양한 메서드가 존재할 시, 이를 묶어서 관리할 수 있음

// /contacts로 들어오는 GET, POST 요청에 대해...
router
  .route("/")
  .get((req, res) => {
    res.send("Contacts Page");
  })
  .post((req, res) => {
    res.send("Create Contacts");
  });

// /contacts:id로 들어오는 GET, POST 요청에 대해...
router
  .route("/:id")
  // 연락처 1개 가져오기
  .get((req, res) => {
    res.send(`View Contact for ID: ${req.params.id}`);
  })
  // 연락처 1개 수정하기
  .put((req, res) => {
    res.send(`Update Contact for ID: ${req.params.id}`);
  })
  // 연락처 1개 삭제하기
  .delete((req, res) => {
    res.send(`Delete Contact for ID: ${req.params.id}`);
  });

// 다른 파일에서 import할 수 있게 설정
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()를 사용하면 됨.
// index.js
// json, url 형식의 요청이 들어올 시 파싱
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// contacts 경로의 요청 처리하는 라우터 미들웨어
app.use("/contacts", require("./routes/contactRoutes.js"));

요청에 포함된 json 등 데이터 확인 (req.body)

  • POST 요청으로 json 형태 데이터가 주어진 경우, req.body를 이용해서 값을 꺼내올 수 있음.
  • 바디파서 미들웨어는, 라우터 미들웨어보다 무조건 먼저 선언되어야 함. 라우터는 파싱이 되었다는 전제 하에 실행되기 때문...
{
  "name": "SSR",
  "email": "lalalahoha@mandu.com",
  "height": 184
}
// /routes/contactRoutes.js
// 모든 연락처에 대해
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
# .env
DB_CONNECT=mongodb://username:password@host:port/dbname
# 여기에 몽고DB Connection String을 복사 붙여넣기.
  • .env 파일은 보안상 깃허브에 푸시하면 안됨! .gitignore 사용하기.
// config/dbConnect.js
// mongoose: express에서 mongodb 사용할 수 있게 해줌
// dotenv: .env, 중 환경변수 관리용
const mongoose = require("mongoose");
require("dotenv").config();

// async: 비동기 처리
const dbConnect = async () => {
  try {
    // .env 파일에 DB_CONNECT 변수에 주소가 작성되어 있어야 함
    // await: 일단 DB에 접속 완료 후, DB Connected가 뜨게끔 처리
    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")가 실행되지 않음.
// index.js
// express 모듈 불러오고, 서버 app 실행하기
const express = require("express");
const dbConnect = require("./config/dbConnect.js");
const app = express();

dbConnect();

// 이하 서버 실행 코드...

스키마와 모델

  • 스키마: MongoDB 컬렉션에 들어갈 도큐먼트의 형태
    • 컬렉션은 "전화부록부", 도큐먼트는 "각 전화번호",
    • 스키마는 "이름, 이메일, 전화번호..." 등 전화부록부에 들어가는 정보 유형이라고 생각하면 된다.
  • mongoose.Schema로 스키마를 만든 뒤, mongoose.Model로 모델로 바꾸어준다.
    • 모델은 Mongoose에서 컬렉션에 접근하기 위해 만든 JS 객체.
    • 모델 이름을 Contact로 지으면, 컬렉션은 자동으로 Contacts(복수형)으로 이름이 지어짐에 유의.
// models/contactModel.js
const mongoose = require("mongoose");
const contactSchema = new mongoose.Schema(
  {
    name: {
      type: String,
      required: true,
    },
    email: {
      type: String,
    },
    height: {
      type: Number,
      required: [true, "키는 꼭 기입해 주세요."],
      // required 필드: 값이 없으면 뜰 오류 메시지 커스텀 가능
    },
  },
  { timestamps: true } // createdAt, updatedAt 등 정보 자동 저장
);

// 스키마를 모델로 변환해야 자료를 저장할 수 있다.
// mongoose.model(모델명, 스키마변수)
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도 존재
    • 하는 역할은 대충 이름 보면 짐작이 되실 거고...
// routes/contactRoutes.js
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")
  // 연락처 1개 가져오기
  .get(getOneContact)
  // 연락처 1개 수정하기
  .put(editOneContact)
  // 연락처 1개 삭제하기
  .delete(deleteOneContact);

module.exports = router;
  • 일단 앞선 contactRoutes.js 파일에 작성된 콜백 함수들을, 별도 함수로 contactController.js라는 다른 파일에 저장해 둘 거임.
// /controllers/contactController.js

const asyncHandler = require("express-async-handler");
const Contact = require("../models/contactModel");

// 모든 연락처를 가져온다.
// GET /contacts

const getAllContacts = asyncHandler(async (req, res) => {
  const contact = await Contact.find();
  res.status(200).json(contact);
});

// 연락처를 생성한다.
// POST /contacts
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);
});

// id를 기반으로 연락처 1개를 가져온다.
// GET /contacts/:id
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);
});

// id를 기반으로 연락처 1개를 수정한다.
// PUT /contacts/:id
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);
});

// 연락처 1개를 삭제한다.
// DELETE /contacts/:id
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(); // 삭제 성공, 내용 없음
});

// 다른 파일에서 쓸 수 있게 export
module.exports = {
  getAllContacts,
  createContact,
  getOneContact,
  editOneContact,
  deleteOneContact,
};
  • 여기서는 res.status(status번호)로 응답 코드도 보내는데, 클라이언트 쪽에서 응답 코드를 보고 성공/실패 여부를 확인해 적절한 처리를 해 줄 수 있음.
    • 보통 2XX는 성공, 나머지 숫자는 실패로 처리.
  • asyncHandler: 별도 설치해야함. 설치하면 try~catch 문 없이도, 요청 실패 시 자동으로 예외 처리 가능.
  • 명시적으로 json 형식으로 클라이언트에게 반환 시 res.json을, 아닌 경우(간단한 문자열 등) res.send 사용하면 됨.
profile
뭔가 만드는 걸 좋아하는 개발자 지망생입니다. 프로야구단 LG 트윈스를 응원하고 있습니다.

2개의 댓글

comment-user-thumbnail
2025년 8월 16일

이메일 만두가 눈에 띄는군요🥟

1개의 답글