TIL

Node.js 입문 개인과제 (2) < 회고 >

황민도 2024. 5. 21. 14:49

내일배움캠프 스파르타 코딩클럽

 

저번 입문 개인과제(1) 에 이어서 TIL을 작성하도록 하겠다.

저번 내용은 패키지매니저 명령어들과 프로젝트에 설치한 패키지들 구성을 알아보았고

이번에는 app, schemas, router, middleware 4가지 를 살펴보도록 하겠다.

 

살펴 보기에 앞서 어떠한것 들을 구현 했는지 잘 작동이 되었는지는 피드백으로 아래 와같이 확인해 볼 수 있다.

총평

  • 대부분의 기능이 잘 작동합니다.
  • 처음 시작하셨을 떄보다 실력이 더 많이 향상된 것이 보입니다.
  • 추가적으로 시간에 대한 고민까지 하셨다니 멋집니다.
  • 다만 코드를 작성함에 있어 어떻게 효율적으로 작성할 수 있을지에 대한 고민은 필요합니다만 이는 시간이 흐르며 자연스레 나아질 부분입니다.
  • 수고하셨습니다.


 

제일 핵심이자 메인보드를 담당하는 app.js

import express from 'express';
import connect from './src/schemas/index.js';
import GoodsRouter from './src/routers/GoodsRouter.js';
import errorHandlerMiddleware from './src/middlewarmies/error.handler.middleware.js';

const app = express();
const PORT = 3300;

connect();

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

const router = express.Router();

router.get('/', (req, res, next) => {
  return res.json({ message: 'Welcome to MDshop' });
});

app.use('/api', [router, GoodsRouter]);

app.use(errorHandlerMiddleware);

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

제일 애용하는 3000 번 포트는 todolist 서버 포트로 사용하고 있으므로 위와같이 3300번으로 설정한 것을 볼 수 있다.

mongoDB 사용을 위해 connect 라는 이름으로 임포트 하여 connect() 로 실행을 시켜주었고

app.use('/api', [GoodRouter]) 를 사용하여 핵심적인 기능구현을한 파일을 실행 시켜주었다.  

app.use(errorHandlerMiddleware); 를 사용하여 미들웨어를 실행 시켜주고 있다.


< schemas > <index.js>

import mongoose from 'mongoose';
import dotenv from 'dotenv';

dotenv.config();

const connect = () => {
  mongoose
    .connect(process.env.MONGODB_URL, {
      dbName: process.env.MONGODB_NAME,
    })
    .catch((err) => console.log(`MongoDB 연결에 실패하였습니다. ${err}`))
    .then(() => console.log('MongoDB 연결에 성공하였습니다.'));
};

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

export default connect;

몽고디비에 연결하기 위한 작업 그리고 dotenv는 .env 파일을 통하여 key를 집어 넣어주기 위해 위와 같이 임포트 하였다.

 

< schemas > <GoodsSchema.js>

import mongoose from 'mongoose';

const GoodsSchema = new mongoose.Schema({
  goodsId: {
    // 상품ID
    type: Number,
    required: true,
  },
  goodsPw: {
    // 상품PW
    type: String,
    required: true,
  },
  person: {
    // 담당자
    type: String,
    required: true
  },
  goods: {
    // 상품명
    type: String,
    required: true
  },
  manual: {
    // 상품 설명
    type: String,
    required: true
  },
  condition: {
    // 상품 상태
    type: String,
    required: true
  },
  uploadAt: {
    // 생성 날짜
    type: Date,
    required: true,
  },
  updateAt: {
    // 수정 날짜
    type: Date,
    required: false, // updateAt 필드는 필수 요소가 아닙니다.
    // 완료가 되지않았다면 null 이기때문에 필수를 false로 설정
  },
});

// Goods 모델 생성
export default mongoose.model('Goods', GoodsSchema);

updateAt은 수정시에 타임값을 입력하므로 필수적 요소가 아니라 false로 설정하였으며

나머지 사항들은 등록시 입력을 받거나 내가 설정을 해줘야 하는 필수적 요건이므로 true로 설정해 주었다.


< routers > < GoodsRouter.js >

import express from 'express';
import joi from 'joi';
import Goods from '../schemas/GoodsSchema.js';

const router = express.Router();



//유효성 검사에 실패했을 때, 에러가 발생해야 한다.
//검증을 진행할때 비동기적으로 진행해야 한다. .validateAsync(req.body)
const createdGoodsSchema = joi.object({
  goods: joi.string().min(1).max(30).required(),
  manual: joi.string().min(1).max(50).required(),
  person: joi.string().min(1).max(10).required(),
  goodsPw: joi.string().min(1).max(10).required(),
});

// 상품등록 API
router.post('/goods', async (req, res, next) => {
  try {
    // 클라이언트로 부터 받아온 value 데이터를 가져온다.
    const validation = await createdGoodsSchema.validateAsync(req.body);
    const { goods, manual, person, goodsPw } = validation;
    // 만약, 클라이언트가 value 데이터를 전달하지 않았을 때, 클라이언트에게 에러 메시지를 전달한다.

    // 해당하는 마지막 goodsId 데이터를 조회한다.
    // findeOne = 1개의 데이터만 조회한다.
    // sort = 정령한다. -> 어떤 컬럼을?
    const goodsMaxId = await Goods.findOne().sort('-goodsId').exec();
    // order 만 하면 오름차순 -order 하면 내림차순
    // 몽구스로 조회할때는 exec 로 조회하면 좀더 정상적으로 조회할 수 있다.
    // exec()가 없으면 프로미스로 동작하지 않게 되며 프로미스로 동작하지 않는다는건
    // await 을 사용할 수 없다.

    // 3. 만약 존재한다면 ID +1 하고, 아니면 1로 할당한다.
    const goodsId = goodsMaxId ? goodsMaxId.goodsId + 1 : 1;
    const condition = 'FOR_SALE';
    const uploadAt = new Date().toLocaleString('en-US', {
      timeZone: 'Asia/Seoul',
    });
    const updateAt = null;

    // 4. 상품 등록
    const loadGoods = new Goods({
      goods,
      manual,
      person,
      goodsId,
      goodsPw,
      condition,
      uploadAt,
      updateAt,
    }); // 인스턴스 형식으로 만든것이고
    await loadGoods.save(); // 데이터베이스에 저장한다.

    // 5. 해야할 일을 클라이언트에게 반환한다.
    return res.status(201).json({ loadGoods: loadGoods }); // loadGoods 없애는거 고려해보기
  } catch (error) {
    // 서버가 중단되지 않기위해 위를 try로 묶어주고 아래 catch 구문을 하여 에러메세지를 리스폰스 함으로 써 서버를 유지할 수 있다.
    // Router 다음에 있는 에러 처리 미들웨어를 실행한다.
    next(error);
  }
});

// 상품 목록 조회 API
router.get('/goods', async (req, res, next) => {
  try {
    // 상품 목록 조회를 진행한다.
    const goodsMenu = await Goods.find({}, { goodsPw: 0 })
      .sort('-uploadAt')
      .exec();
    // mongoDB쿼리 언어규칙 find({}, { goodsPw: 0 }) 0을 넣어서 제외 1은 포함

    if (!goodsMenu) {
      return res.status(404).json([]);
    }

    // 상품 목록 조회 결과를 클라이언트에게 반환한다.
    return res.status(200).json({ goodsMenu });
  } catch (error) {
    next(error);
  }
});

// 상품 상세 조회 API
router.get('/goods/:goodsId', async (req, res, next) => {
  try {
    const { goodsId } = req.params;
    const goodsSearch = await Goods.findOne({ goodsId }, { goodsPw: 0 }).exec();
    // mongoDB쿼리 언어규칙 find({}, { goodsPw: 0 }) 0을 넣어서 제외 1은 포함

    if (!goodsSearch) {
      throw new Error('Goods not found');
    }

    // 상품 목록 조회 결과를 클라이언트에게 반환한다.
    return res.status(200).json({ goodsSearch });
  } catch (error) {
    next(error);
  }
});

const updateGoodsSchema = joi.object({
  goods: joi.string().min(1).max(30),
  manual: joi.string().min(1).max(50),
  person: joi.string().min(1).max(10),
  goodsPw: joi.string().min(1).max(10).required(),
  condition: joi.string().valid('FOR_SALE', 'SOLD_OUT'),
});

// 상품 수정 비밀번호 입력 API
router.patch('/goods/:goodsId', async (req, res, next) => {
  try {
    const { goodsId } = req.params;
    const validation = await updateGoodsSchema.validateAsync(req.body);
    const { goods, manual, person, goodsPw, condition } = validation;

    const updateGoods = await Goods.findOne({ goodsId }).exec();
    if (!updateGoods) {
      throw new Error('Goods not found');
    }
    if (!goodsPw) {
      throw new Error('"goodsPw" is required');
    }
    if (goodsPw !== updateGoods.goodsPw) {
      throw new Error('"goodsPw" is not same');
    }
    if (goods) {
      updateGoods.goods = goods;
    }
    if (manual) {
      updateGoods.manual = manual;
    }
    if (person) {
      updateGoods.person = person;
    }
    if (condition) {
      updateGoods.condition = condition;
    }
    updateGoods.updateAt = new Date().toLocaleString('en-US', {
      timeZone: 'Asia/Seoul',
    });
    //new Date().toLocaleString("en-US", {timeZone: "Asia/Seoul"}) 이렇게 작성하면 한국시간을 가져올수 있음.
    await updateGoods.save();

    return res.status(200).json({ updateGoods });
  } catch (error) {
    next(error);
  }
});

// 상품 삭제 API
router.delete('/goods/:goodsId', async (req, res, next) => {
  try {
    const { goodsId } = req.params;
    const validation = await updateGoodsSchema.validateAsync(req.body);
    const { goodsPw } = validation;

    const goods = await Goods.findOne({ goodsId }).exec();
    if (!goods) {
      throw new Error('Goods not found');
    }
    if (!goodsPw) {
      throw new Error('"goodsPw" is required');
    }
    if (goodsPw !== goods.goodsPw) {
      throw new Error('"goodsPw" is not same');
    }

    await Goods.deleteOne({ goodsId });

    return res.status(200).json({});
  } catch (error) {
    next(error);
  }
});

export default router;

여기서 모든 error처리를 미들웨어로 넘기기 위해 thorw new Error 를사용하여 강제로 에러를 처리 하였으며

joi 같은 경우도 알아서 error로 처리해주기 때문에 유효성검사를 설정해 주었다.

이렇게 CRUD 기능이 모두 구현된 RESTful 한 설계를 완료하였다. 


< middlewarmies > < error.handle.middleware.js > < 회고 >

export default (err, req, res, next) => {
  console.log('에러 처리 미들웨어가 실행되었습니다.');
  console.error(err);
  if (err.message === 'Goods not found') {
    return res.status(404).json({ errorMessage: '상품이 존재하지 않습니다.' });
  }
  if (err.isJoi) {
    return res.status(400).json({ err: err.message });
  }
  if (err.message === '"goodsPw" is not same') {
    return res
      .status(400)
      .json({ errorMessage: '(goodsPw) 비밀번호가 일치하지 않습니다.' });
  }
  if (err.name === 'ValidationError') {
    if (err.message === '"goods" is required') {
      return res
        .status(400)
        .json({ errorMessage: '(goods) 상품명을 입력해 주세요.' });
    }
    if (err.message === '"manual" is required') {
      return res
        .status(400)
        .json({ errorMessage: '(manual) 상품 설명을 입력해 주세요.' });
    }
    if (err.message === '"person" is required') {
      return res
        .status(400)
        .json({ errorMessage: '(person) 담당자를 입력해 주세요.' });
    }
    if (err.message === '"goodsPw" is required') {
      return res
        .status(400)
        .json({ errorMessage: '(goodsPw) 상품 비밀번호를 입력해 주세요.' });
    }
  }
  if (err.message === '"goodsPw" is required') {
    return res
      .status(400)
      .json({ errorMessage: '(goodsPw) 해당 ID의 비밀번호를 입력해 주세요.' });
  }

  return res.status(500).json({
    errorMessage: '예상치 못한 에러가 발생했습니다. 관리자에게 문의해 주세요.',
  });
};

미들웨어를 위와같이 작성하였는데 사실 좀 더 간략하게 할 수 있는 법이 있지는 않을까 생각이 든다만

아직나의 학습으로 가능한 수준은 여기까지 이다. 좀 아쉽긴 하지만 나중에 회고해 보도록 하겠다.

 

이렇게 해서 개인과제를 모두 완성 하였다. 처음에는 할 수 있을까? 라는 생각이 앞섰지만 배운 강의 내용을 확인하며

작성하니 생각보다 수월하게 작성을 하였다.

그러나 온전히 내것으로 순수히 아무것도 없는 상태에서 이정도를 작성하기에는 아직 무리가 있다.

그러므로 그 숙련은 반복 학습을 통해 단련할 것이다. 

추가적으로 회고해야 할 부분을 넣고 마치도록 하겠다.