개발일지

개발일지 35일차

index.ys 2023. 4. 20. 12:52

미들웨어

  • 웹 서버에서 요청이나 응답에 대해 공통적인 관리가 가능
  • ex)모든 요청에 대해 로그를 남겨 확인하고 싶은 경우

- 모든 로깅을 관리해주는 미들웨워

app.use(express.urlencoded({ extended: false }));
app.use(express.json());

유명한 웹서버

  • Apache
  • nginx

미들웨어 기본

- 위에서 부터 순차적으로 미들웨어를 통과함

- next()가 무조건 있어야 다음 미들웨어 실행

app.use((req, res, next) => {
    console.log('첫번째 미들웨어');
    next();
});

app.use((req, res, next) => {
    console.log('두번째 미들웨어');
    next();
});

app.use((req, res, next) => {
    console.log('세번째 미들웨어');
    next();
});

// print: 첫번째 미들웨어
// print: 두번째 미들웨어
// print: 세번째 미들웨어

쇼핑몰 구현

app.js

  • 라우터 및 미들웨어 추가
const express = require('express');
const app = express();
const port = 3000;

const goodsRouter = require("./routes/goods");
const cartsRouter = require("./routes/carts.js");

const connect = require("./schemas");
connect();


app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(express.static("assets"));
app.use("/api", [goodsRouter, cartsRouter]);

app.get('/', (req, res) => {
  res.send('Hello World!');
});

app.listen(port, () => {
  console.log(port, '포트로 서버가 열렸어요!');
});

user 모델

  • email, nickname, password 필드 추가
  • 몽고db _id값을 가상의 값을 할당함 virtual("적용할key")
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);

회원가입 API users.js

  • 클라이언트로부터 email, nickname, password, confirmPassword  4개의 값을 받음
  • password와 confirmPassword 값이 같은지 확인하는 조건문 작성
  • db에서 email 또는 ($or) nickname이 일치하는 데이터가 있는지 확인
  • email 또는 nickname이 사용중일때 return으로 아래코드가 더 이상 수행하지 않고 코드종료
  • 패스워드 저장시 암호화 하여 저장, 복호화 불가능, crypto라는 라이브러리 사용
  • user.saver()로 db에 저장후 201상태 반환
// routes/users.js

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

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

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

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

  // email 또는 nickname이 동일한 데이터가 있는지 확인하기 위해 가져온다.
  const existsUsers = await User.findOne({
    $or: [{ email }, { nickname }],
  });
  if (existsUsers) {
    // NOTE: 보안을 위해 인증 메세지는 자세히 설명하지 않습니다.
    res.status(400).json({
      errorMessage: "이메일 또는 닉네임이 이미 사용중입니다.",
    });
    return;
  }

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

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


module.exports = router;

로그인 API auth.js

  • 입력받은 이메일, 패스워드 할당
  • 이메일이 일치하는 유저를 db에서 findOne메서드으로 하나만 찾음
  • 이메일이 일치하는 유저가 존재하지 않거나 일치하는 유저를 찾았지만 유저의 비밀번호와 입력한 비밀번호가 다를때를 구분하는 조건문작성
  • 비밀번호가 일치 할때 jwt생성 jwt.sign( { userId : userid } , "customized secret key")
  • Bearer 타입으로 "Authorization"이라는 값 jwt전달
  • jwt를 쿠키로 할당 res.cookie( "Authorization" ,`Bearer ${token}` )
  • jwt를 body로 할당 res.status(200).json({ token });
// routes/auth.js

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

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

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

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

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

  const token = jwt.sign(
    { userId: user.userId },
    "custom-secret-key",
  );

	res.cookie("Authorization", `Bearer ${token}`); // JWT를 Cookie로 할당합니다!
  res.status(200).json({ token }); // JWT를 Body로 할당합니다!
});

module.exports = router;

app.js 에 authRouter 추가

const authRouter = require("./routes/auth.js");

app.use("/api", [goodsRouter, cartsRouter, usersRouter, authRouter]);

사용자 인증 미들웨어

- 구현된 프론트엔드는 로그인이 성공했을때 받아온 토큰을 HTTP header에 넣어서 보내고 있음

Authorization : Bearer JWT토큰내용

- "Authorization" 헤더로 전달받는 토큰이 유효한지 검사, 유효하다면 토큰안에 있는 userId 데이터로 해당 사용자가 db에 존재하는지 체크.

cookie-parser 추가

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

app.use(cookieParser());

사용자 인증 미들웨어 auth-middleware.js

  • 전달받은 Autorization 이 쿠키정보를 가지고옴
  • authType , authToken이 Authorization 변수가 undefined이거나 null 값일 경우 빈 문자열("")로 변경,  null 병합 연산자를 사용해 " "값을 왼쪽에 넣어줌(오류가 나지 않게 하기 위함)
  •  try catch문으로 jwt.verify(authToken, "customized-secret-key") 토큰이 일치하는지 확인
  • 미들웨어를 사용하는 라우터에서는 굳이 데이터베이스에서 사용자 정보를 가져오지 않게 할 수 있도록 express가 제공하는 안전한 변수인 res.locals에 담아두고, 언제나 꺼내서 사용할 수 있게 작성
// middlewares/auth-middleware.js

const jwt = require("jsonwebtoken");
const User = require("../schemas/user");

// 사용자 인증 미들웨어
module.exports = async (req, res, next) => {
  const { Authorization } = req.cookies;
  const [authType, authToken] = (Authorization ?? "").split(" ");

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

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

내 정보 조회 API routes/users.js

  • 사용자 인증 미들웨어추가.
  •  res.locals 에 저장된 email, nickname을 가져옴.
  • 예시와 같이 출력
// routes/users.js

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

...

// 내 정보 조회 API
router.get("/users/me", authMiddleware, async (req, res) => {
  const { email, nickname } = res.locals.user;
  
  res.status(200).json({
    user: { email, nickname }
  });
});

...

예시

{
	"user": {
		"email": "test@email.com",
		"nickname": "test-nickname",
	}
}

상품 API 수정 routes/goods.js

상품 목록조회 API

  • find(category ? { category } : {}) query param으로 category가 전달 되었을경우 해당하는 category로 조회하고 category가 존재하지 않는 경우 모든 상품을 조회하는 코드 
// routes/goods.js


// 상품 목록 조회 API
router.get("/goods", async (req, res) => {
  const { category } = req.query;

  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

  • 입력된 query param과 일치하는 goodsId를 1개만 가져옴. (데이터를 전체 조회할때는 [{ }], 1개만 조회할때는 { })
  • 입력된 query param이 없을때는 404에러 반환
// routes/goods.js

// 상품 상세 조회 API
router.get("/goods/:goodsId", async (req, res) => {
  const { goodsId } = req.params;

  const goods = await Goods.findOne({ goodsId: goodsId }).exec();

  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 });
});

Cart 모델 수정

  • userId필드 추가하여 어느 사용자의 장바구니 인지 구분
const mongoose = require("mongoose");

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

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

장바구니 등록 API routes/goods.js

  • authmiddleware로 인증 미들웨어 추가
  • 저장된 jwt userId에 할당
// routes/goods.js

const Goods = require("../schemas/goods.js");
const Cart = require("../schemas/cart.js");
const authMiddleware = require("../middlewares/auth-middleware");

// 장바구니 등록 API
router.post("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user;
  const { goodsId } = req.params;
  const { quantity } = req.body;

  const existsCarts = await Cart.find({ userId, goodsId }).exec();
  if (existsCarts.length) {
    return res.status(400).json({
      success: false,
      errorMessage: "이미 장바구니에 해당하는 상품이 존재합니다.",
    });
  }

  await Cart.create({ userId, goodsId, quantity });

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

장바구니 조회 API routes/goods.js

  • 인증 미들웨어 추가
  • jwt 가져오기
// routes/goods.js

const Goods = require("../schemas/goods.js");
const Cart = require("../schemas/cart.js");
const authMiddleware = require("../middlewares/auth-middleware");

// 장바구니 조회 API
router.get("/goods/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user;

  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 routes/goods.js

  • 인증 미들웨어 추가
  • jwt 가져오기
// routes/goods.js

const Goods = require("../schemas/goods.js");
const Cart = require("../schemas/cart.js");
const authMiddleware = require("../middlewares/auth-middleware");

// 장바구니 상품 수정 API
router.put("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user;
  const { goodsId } = req.params;
  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 routes/goods.js

  • 인증 미들웨어 추가
  • jwt 가져오기
// routes/goods.js

const Goods = require("../schemas/goods.js");
const Cart = require("../schemas/cart.js");
const authMiddleware = require("../middlewares/auth-middleware");

// 장바구니 상품 삭제 API
router.delete("/goods/:goodsId/cart", authMiddleware, async (req, res) => {
  const { userId } = res.locals.user;
  const { goodsId } = req.params;

  const existsCarts = await Cart.find({ userId, goodsId });
  if (existsCarts.length) {
    await Cart.deleteOne({ userId, goodsId });
  }

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

 

회원가입 payload

유저가 입력한 정보를 기반을 생성된 쿠키

db에 저장된 email과 password

저장된 email과 password의 값이 똑같을때

생성된 토큰 반환 

토큰의 정보

  res.cookie("Authorization", `Beare ${token}`)

name:authorization value:Beare%20eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOiI2NDQwYzUyNGU1MjFkNWU0ODJkN2Q0OWQiLCJpYXQiOjE2ODE5NjY0NTZ9.iAoDC-vQXWGKd6mBWCbgu0MUolKnqn6sQdHglunq_pg

내일 할일: 오늘 너무 많이 놀아서 내일은 일어나자마자 lv2 과제 api명세 정리하고 바로 과제 수행해야겠다.