제로부터 쌓는 개발일지
article thumbnail
반응형

요약정리

[Node.JS] 미들웨어

 

전체 코드

동영상 강의 코드

app.js

더보기
const express = require("express");
const mongoose = require("mongoose");

mongoose.connect("mongodb://127.0.0.1/shopping-demo", {
  useNewUrlParser: true,
  useUnifiedTopology: true,
});
const db = mongoose.connection;
db.on("error", console.error.bind(console, "connection error:"));

const app = express();
const router = express.Router();

const User = require("./models/user");

// 회원가입 API
router.post("/users", async (req, res) => {
  const { email, nickname, password, confirmPassword } = req.body;

  if (password !== confirmPassword) {
    res.status(400).send({
      errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
    });
    return;
  }

  // email or nickname이 동일한게 이미 있는지 확인하기 위해 가져온다.
  const existsUsers = await User.findOne({
    $or: [{ email }, { nickname }],
  });
  if (existsUsers) {
    // NOTE: 보안을 위해 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다.
    //  https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
    res.status(400).send({
      errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
    });
    return;
  }

  const user = new User({ email, nickname, password });
  await user.save();

  res.status(201).send({});
});

const jwt = require("jsonwebtoken");

router.post("/auth", async (req, res) => {
  const { email, password } = req.body;

  const user = await User.findOne({ email });

  // NOTE: 인증 메세지는 자세히 설명하지 않는것을 원칙으로 한다: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html#authentication-responses
  if (!user || password !== user.password) {
    res.status(400).send({
      errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
    });
    return;
  }

  res.send({
    token: jwt.sign({ userId: user.userId }, "customized-secret-key"),
  });
});

const authMiddleware = require("./middlewares/auth-middleware");

router.get("/users/me", authMiddleware, async (req, res) => {
  res.send({ user: res.locals.user });
});

app.use("/api", express.urlencoded({ extended: false }), router);
app.use(express.static("assets"));

app.listen(8080, () => {
  console.log("서버가 요청을 받을 준비가 됐어요");
});

 

models/user.js

더보기
const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  email: String,
  nickname: String,
  password: String,
});
UserSchema.virtual("userId").get(function () {
  return this._id.toHexString();
});
UserSchema.set("toJSON", {
  virtuals: true,
});
module.exports = mongoose.model("User", UserSchema);

 

middlewares/auth-middlewares.js

더보기
const jwt = require("jsonwebtoken");
const User = require("../models/user");

module.exports = (req, res, next) => {
  const { authorization } = req.headers;
  const [authType, authToken] = (authorization || "").split(" ");

  if (!authToken || authType !== "Bearer") {
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
    return;
  }

  try {
    const { userId } = jwt.verify(authToken, "customized-secret-key");
    User.findById(userId).then((user) => {
      res.locals.user = user;
      next();
    });
  } catch (err) {
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
  }
};

 


 

노션 코드

app.js

더보기
// app.js

const express = require("express");
const cookieParser = require("cookie-parser");

const goodsRouter = require("./routes/goods.js");
// const cartsRouter = require("./routes/carts.js"); // 현재 사용하지 않는 장바구니 라우터
const usersRouter = require("./routes/users.js");
const authRouter = require("./routes/auth.js");

// MongoDB 연결을 위한 스키마 설정 모듈을 가져옴
const connect = require("./schemas");

const app = express(); // express 애플리케이션을 생성
const port = 3000; // 서버가 사용할 포트 번호

connect(); // mongoose를 이용해 MongoDB에 연결

// 미들웨어 설정
app.use(express.json()); // JSON 요청 본문을 파싱합니다.
app.use(express.urlencoded({ extended: false })); // URL 인코딩된 데이터를 파싱
app.use(cookieParser()); // 쿠키를 파싱
app.use(express.static("assets")); // 정적 파일을 제공하는 미들웨어

// 라우트 미들웨어 설정
app.use("/api", [goodsRouter, usersRouter, authRouter]); // '/api' 경로로 시작하는 요청을 각 라우터에 위임

// 루트 URL에 대한 요청 처리
app.get("/", (req, res) => {
  res.send("Hello World!"); // 루트 URL에 접근하면 'Hello World!'를 반환
});

// 서버 시작
app.listen(port, () => {
  console.log(port, "포트로 서버가 열렸어요!"); // 서버가 포트 3000에서 시작되었음을 콘솔에 출력
});

 

schemas/cart.js

더보기
const mongoose = require("mongoose");

const cartSchema = new mongoose.Schema({
  userId: {
    type: String,
    required: true,
  },
  goodsId: {
    type: String,
    required: true,
  },
  quantity: {
    type: Number,
    required: true,
  },
});

module.exports = mongoose.model("Cart", cartSchema);

 

schemas/goods.js

더보기
const mongoose = require("mongoose");

const goodsSchema = new mongoose.Schema({
  goodsId: {
    type: String,
    required: true,
    unique: true,
  },
  name: {
    type: String,
    required: true,
    unique: true,
  },
  thumbnailUrl: {
    type: String,
  },
  category: {
    type: String,
  },
  price: {
    type: Number,
  },
});

module.exports = mongoose.model("Goods", goodsSchema);

 

schemas/index.js

더보기
const mongoose = require("mongoose");

const connect = () => {
  mongoose
    .set("strictQuery", true)
    .connect("mongodb://127.0.0.1:27017/spa_mall")
    .catch((err) => console.log(err));
};

mongoose.connection.on("error", (err) => {
  console.error("몽고디비 연결 에러", err);
});

module.exports = connect;

 

schemas/user.js

더보기
const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  email: {
    // email 필드
    type: String,
    required: true,
    unique: true,
  },
  nickname: {
    // nickname 필드
    type: String,
    required: true,
    unique: true,
  },
  password: {
    // password 필드
    type: String,
    required: true,
  },
});

// 가상의 userId 값을 할당
UserSchema.virtual("userId").get(function () {
  return this._id.toHexString();
});

// user 정보를 JSON으로 형변환 할 때 virtual 값이 출력되도록 설정
UserSchema.set("toJSON", {
  virtuals: true,
});

module.exports = mongoose.model("User", UserSchema);

 

routes/auth.js

더보기
// routes/auth.js

// 필요한 모듈을 가져옴
const jwt = require("jsonwebtoken"); // JSON Web Tokens를 생성하고 검증하기 위한 모듈
const express = require("express");
const router = express.Router();

// MongoDB User 모델을 가져옴
const User = require("../schemas/user");

// 로그인 API
router.post("/auth", async (req, res) => {
  // 요청 본문에서 이메일과 패스워드를 추출
  const { email, password } = req.body;

  // 이메일로 사용자를 찾습니다.
  const user = await User.findOne({ email });

  // 사용자가 존재하지 않거나 패스워드가 일치하지 않으면 오류 메시지를 반환
  if (!user || password !== user.password) {
    res.status(400).json({
      errorMessage: "이메일 또는 패스워드가 틀렸습니다.",
    });
    return;
  }

  // 사용자를 위한 JWT를 생성
  const token = jwt.sign({ userId: user.userId }, "customized-secret-key");

  // 생성된 JWT를 쿠키에 저장
  res.cookie("Authorization", `Bearer ${token}`);
  // 생성된 JWT를 응답 본문에도 포함하여 반환
  res.status(200).json({ token });
});

// 라우터 모듈을 내보냄
module.exports = router;

 

routes/goods.js

더보기
// routes/goods.js

const express = require("express");
const router = express.Router();
const authMiddleware = require("../middlewares/auth-middleware");

// 썬더 클라이언트로 등록할 상품목록
// const goods = [
//   {
//     goodsId: 4,
//     name: "상품 4",
//     thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/02/11/frogs-1650657_1280.jpg",
//     category: "drink",
//     price: 0.1,
//   },
//   {
//     goodsId: 3,
//     name: "상품 3",
//     thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/02/12/frogs-1650658_1280.jpg",
//     category: "drink",
//     price: 2.2,
//   },
//   {
//     goodsId: 2,
//     name: "상품 2",
//     thumbnailUrl: "https://cdn.pixabay.com/photo/2014/08/26/19/19/wine-428316_1280.jpg",
//     category: "drink",
//     price: 0.11,
//   },
//   {
//     goodsId: 1,
//     name: "상품 1",
//     thumbnailUrl: "https://cdn.pixabay.com/photo/2016/09/07/19/54/wines-1652455_1280.jpg",
//     category: "drink",
//     price: 6.2,
//   },
// ];

// MongoDB의 Goods 스키마를 가져옴
const Goods = require("../schemas/goods.js");
// MongoDB의 Cart 스키마를 가져옴
const Cart = require("../schemas/cart.js");

// 상품 목록 조회 API
router.get("/goods", async (req, res) => {
  // 쿼리 파라미터에서 카테고리를 추출
  const { category } = req.query;

  // Goods 모델을 사용하여 데이터베이스에서 상품을 조회
  const goods = await Goods.find(category ? { category } : {})
    .sort("-date") // 최신순으로 정렬
    .exec();

  // 조회된 상품 데이터를 정리하여 응답
  const results = goods.map((item) => {
    return {
      goodsId: item.goodsId,
      name: item.name,
      price: item.price,
      thumbnailUrl: item.thumbnailUrl,
      category: item.category,
    };
  });

  res.json({ goods: results });
});

// 상품 등록 API
router.post("/goods", async (req, res) => {
  const { goodsId, name, thumbnailUrl, category, price } = req.body; // 요청 본문에서 상품 정보 추출

  // 동일한 goodsId를 가진 상품이 있는지 확인
  const goods = await Goods.find({ goodsId });

  if (goods.length) {
    // 이미 존재하면 오류 메시지와 함께 400 상태 코드를 반환
    return res.status(400).json({
      success: false,
      errorMessage: "이미 존재하는 GoodsId입니다.",
    });
  }

  // 새 상품을 생성하고 데이터베이스에 저장
  const createdGoods = await Goods.create({ goodsId, name, thumbnailUrl, category, price });

  res.json({ goods: createdGoods });
});

// 상품 상세 조회 API
router.get("/goods/:goodsId", async (req, res) => {
  // URL 파라미터에서 goodsId를 추출
  const { goodsId } = req.params;

  // 특정 상품 ID에 해당하는 상품을 조회
  const goods = await Goods.findOne({ goodsId: goodsId }).exec();

  // 해당 상품이 없으면 404 오류를 반환
  if (!goods) return res.status(404).json({});

  // 조회된 상품 정보를 응답
  const result = {
    goodsId: goods.goodsId,
    name: goods.name,
    price: goods.price,
    thumbnailUrl: goods.thumbnailUrl,
    category: goods.category,
  };

  res.json({ goods: result });
});

// ========== 장바구니 기능 미구현 ==========

// 장바구니 등록 API
router.post("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user; // 인증된 사용자의 ID
  const { goodsId } = req.params; // URL 파라미터에서 goodsId를 추출
  const { quantity } = req.body; // 요청 본문에서 수량을 추출

  // 동일한 상품이 장바구니에 있는지 확인
  const existsCarts = await Cart.find({ userId, goodsId }).exec();
  if (existsCarts.length) {
    // 이미 존재하면 오류 메시지와 함께 400 상태 코드를 반환
    return res.status(400).json({
      success: false,
      errorMessage: "이미 장바구니에 해당하는 상품이 존재합니다.",
    });
  }

  // 장바구니에 상품을 추가
  await Cart.create({ userId, goodsId, quantity });

  res.json({ result: "success" });
});

// 장바구니 조회 API
router.get("/goods/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user; // 인증된 사용자의 ID

  // 사용자의 장바구니 항목을 조회
  const carts = await Cart.find({ userId }).exec();
  const goodsIds = carts.map((cart) => cart.goodsId);

  // 장바구니에 있는 상품들의 정보를 조회
  const goods = await Goods.find({ goodsId: goodsIds });

  // 장바구니 항목과 상품 정보를 결합하여 응답
  const results = carts.map((cart) => {
    return {
      quantity: cart.quantity,
      goods: goods.find((item) => item.goodsId === cart.goodsId),
    };
  });

  res.json({
    carts: results,
  });
});

// 장바구니 상품 수정 API
router.put("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user; // 인증된 사용자의 ID
  const { goodsId } = req.params; // URL 파라미터에서 goodsId 추출
  const { quantity } = req.body; // 요청 본문에서 수량 추출

  // 장바구니 항목이 존재하는지 확인
  const existsCarts = await Cart.find({ userId, goodsId });
  if (existsCarts.length) {
    // 존재하면 해당 항목의 수량을 업데이트
    await Cart.updateOne({ userId, goodsId: goodsId }, { $set: { quantity: quantity } });
  }
  res.status(200).json({ success: true });
});

// 장바구니 상품 삭제 API
router.delete("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user; // 인증된 사용자의 ID
  const { goodsId } = req.params; // URL 파라미터에서 goodsId 추출

  // 장바구니 항목이 존재하는지 확인
  const existsCarts = await Cart.find({ userId, goodsId });
  if (existsCarts.length) {
    // 존재하면 해당 항목을 삭제
    await Cart.deleteOne({ userId, goodsId });
  }

  res.json({ result: "success" });
});

module.exports = router;

 

routes/users.js

더보기
// routes/users.js

const express = require("express");
const router = express.Router();

// MongoDB의 User 스키마를 가져옴
const User = require("../schemas/user");
// 사용자 인증을 위한 미들웨어
const authMiddleware = require("../middlewares/auth-middleware");

// 회원가입 API
router.post("/users", async (req, res) => {
  // 요청 본문에서 필요한 정보를 추출
  const { email, nickname, password, confirmPassword } = req.body;

  // 입력된 비밀번호와 확인 비밀번호가 일치하는지 검사
  if (password !== confirmPassword) {
    // 일치하지 않으면 오류 메시지와 함께 400 상태 코드를 반환
    res.status(400).json({
      errorMessage: "패스워드가 패스워드 확인란과 다릅니다.",
    });
    return;
  }

  // 이메일 또는 닉네임이 이미 사용 중인지 검사
  const existsUsers = await User.findOne({
    $or: [{ email }, { nickname }],
  });
  if (existsUsers) {
    // 이미 사용 중인 경우 오류 메시지와 함께 400 상태 코드를 반환
    res.status(400).json({
      errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
    });
    return;
  }

  // 새로운 사용자를 생성하고 데이터베이스에 저장
  const user = new User({ email, nickname, password });
  await user.save();

  // 생성이 완료되면 201 상태 코드를 반환
  res.status(201).json({});
});

// 내 정보 조회 API
router.get("/users/me", authMiddleware, async (req, res) => {
  // 인증 미들웨어를 통해 얻은 사용자 정보를 추출
  const { email, nickname } = res.locals.user;

  // 사용자 정보를 반환
  res.status(200).json({
    user: { email, nickname },
  });
});

// 라우터 모듈을 내보냄
module.exports = router;

 

models/user.js

더보기
const mongoose = require("mongoose");

const UserSchema = new mongoose.Schema({
  email: {
    // email 필드
    type: String,
    required: true,
    unique: true,
  },
  nickname: {
    // nickname 필드
    type: String,
    required: true,
    unique: true,
  },
  password: {
    // password 필드
    type: String,
    required: true,
  },
});

// 가상의 userId 값을 할당
UserSchema.virtual("userId").get(function () {
  return this._id.toHexString();
});

// user 정보를 JSON으로 형변환 할 때 virtual 값이 출력되도록 설정
UserSchema.set("toJSON", {
  virtuals: true,
});

module.exports = mongoose.model("User", UserSchema);

 

middlewares/auth-middleware.js

더보기
// middlewares/auth-middleware.js

const jwt = require("jsonwebtoken"); // JWT 처리를 위한 모듈
const User = require("../schemas/user"); // MongoDB User 모델

// 사용자 인증 미들웨어
module.exports = async (req, res, next) => {
  // 요청에서 쿠키를 가져옴
  const { Authorization } = req.cookies;

  // 쿠키에서 인증 타입과 토큰을 분리
  // 'Authorization' 쿠키가 없다면 빈 문자열을 기본값으로 사용
  const [authType, authToken] = (Authorization ?? "").split(" ");

  // 토큰이 없거나 인증 타입이 'Bearer'가 아니면 오류 메시지를 반환
  if (!authToken || authType !== "Bearer") {
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
    return;
  }

  try {
    // JWT를 검증하고, 토큰에 담긴 사용자 ID를 추출
    const { userId } = jwt.verify(authToken, "customized-secret-key");

    // 사용자 ID로 데이터베이스에서 사용자를 찾음
    const user = await User.findById(userId);

    // 찾은 사용자 정보를 응답 객체의 로컬 변수에 저장
    res.locals.user = user;

    // 다음 미들웨어를 실행합니다.
    next();
  } catch (err) {
    // JWT 검증에 실패하면 오류를 로깅하고 오류 메시지를 반환
    console.error(err);
    res.status(401).send({
      errorMessage: "로그인 후 이용 가능한 기능입니다.",
    });
  }
};

 


 

Error Handling?

Issue 1.

  • 강의 미들웨어까지 진행 후 로그인 테스트를 해보면 에러 발생

 

Solution 1.

  • 프론트에는 구현이 되어 있으나 서버엔 상품 CRUD가 존재하지 않음
    강의에선 회원가입, 로그인만 알려주며 그 외 코드는 노션 자료에 있음
더보기
// 프론트 코드

// const socket = io.connect("/");

// socket.on("BUY_GOODS", function (data) {
//   const { nickname, goodsId, goodsName, date } = data;
//   makeBuyNotification(nickname, goodsName, goodsId, date);
// });

function initAuthenticatePage() {
  // socket.emit("CHANGED_PAGE", `${location.pathname}${location.search}`);
}

function bindSamePageViewerCountEvent(callback) {
  // socket.on("SAME_PAGE_VIEWER_COUNT", callback);
}

function postOrder(user, order) {
  if (!order.length) {
    return;
  }

  // socket.emit("BUY", {
  //   nickname: user.nickname,
  //   goodsId: order[0].goods.goodsId,
  //   goodsName:
  //     order.length > 1
  //       ? `${order[0].goods.name} 외 ${order.length - 1}개의 상품`
  //       : order[0].goods.name,
  // });
}

function getSelf(callback) {
  $.ajax({
    type: "GET",
    url: "/api/users/me",
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    success: function (response) {
      callback(response.user);
    },
    error: function (xhr, status, error) {
      if (status == 401) {
        alert("로그인이 필요합니다.");
      } else {
        localStorage.clear();
        alert("알 수 없는 문제가 발생했습니다. 관리자에게 문의하세요.");
      }
      window.location.href = "/";
    },
  });
}

function getGoods(category, callback) {
  $("#goodsList").empty();
  $.ajax({
    type: "GET",
    url: `/api/goods${category ? "?category=" + category : ""}`,
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    success: function (response) {
      callback(response["goods"]);
    },
  });
}

function signOut() {
  localStorage.clear();
  window.location.href = "/";
}

function getGoodsDetail(goodsId, callback) {
  $.ajax({
    type: "GET",
    url: `/api/goods/${goodsId}`,
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    error: function (xhr, status, error) {
      if (status == 401) {
        alert("로그인이 필요합니다.");
      } else if (status == 404) {
        alert("존재하지 않는 상품입니다.");
      } else {
        alert("알 수 없는 문제가 발생했습니다. 관리자에게 문의하세요.");
      }
      window.location.href = "/goods";
    },
    success: function (response) {
      callback(response.goods);
    },
  });
}

function makeBuyNotification(targetNickname, goodsName, goodsId, date) {
  const messageHtml = `${targetNickname}님이 방금 <a href="/detail.html?goodsId=${goodsId}" class="alert-link">${goodsName}</a>을 구매했어요! <br /><small>(${date})</small>
    <button type="button" class="close" data-dismiss="alert" aria-label="Close">
        <span aria-hidden="true">&times;</span>
    </button>`;
  const alt = $("#customerAlert");
  if (alt.length) {
    alt.html(messageHtml);
  } else {
    const htmlTemp = `<div class="alert alert-sparta alert-dismissible show fade" role="alert" id="customerAlert">${messageHtml}</div>`;
    $("body").append(htmlTemp);
  }
}

function addToCart(goodsId, quantity, callback) {
  $.ajax({
    type: "PUT",
    url: `/api/goods/${goodsId}/cart`,
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    data: {
      quantity,
    },
    error: function (xhr, status, error) {
      if (status == 400) {
        alert("존재하지 않는 상품입니다.");
      }
      window.location.href = "/goods.html";
    },
    success: function () {
      callback();
    },
  });
}

function buyLocation(params) {
  sessionStorage.setItem("ordered", JSON.stringify(params));
  location.href = "order.html";
}

function getCarts(callback) {
  $.ajax({
    type: "GET",
    url: `/api/goods/cart`,
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    success: function (response) {
      callback(response.cart);
    },
  });
}

function deleteCart(goodsId, callback) {
  $.ajax({
    type: "DELETE",
    url: `/api/goods/${goodsId}/cart`,
    headers: {
      authorization: `Bearer ${localStorage.getItem("token")}`,
    },
    success: function () {
      callback();
    },
  });
}

 


 

Issue 2.

  • CastError: Cast to Number failed for value "cart" (type string) at path "goodsId" for model "Goods"
    스키마 컬럼 타입은 Number로 되어 있는데 왜 문자열을 집어넣냐고 출력되는 에러.. 상당히 길게 출력된다

 

Solution 2.

  • 무지성 컬럼 타입 변경
    에러가 출력되면 서버가 내려가서 임시로 변경해 줌

 


 

회고

정말.. 강의대로 했는데도 예상치 못한 에러들이 여기저기서 나왔어...

장바구니 기능도 구현해보고 싶었는데 새로운 개인 과제도 나와서 나중에 여유 시간 있을 때 한번 해봐야 될듯하다

 

세션은 이해가 되는데 JWT토큰은 아직 모호하다

좀 더 파봐야 될 듯

 

반응형
profile

제로부터 쌓는 개발일지

@PachyuChepe

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...