상세 컨텐츠

본문 제목

[Node.js] Spart Store 1

projects/node.js

by 서울의볼 2024. 1. 23. 15:32

본문

ejs로 구현한 상품 목록 조회 페이지

Tech stack:

  • Node.js
  • MongoDB
  • Express.js
  • ejs

Other tools used:

  • Insomnia (API client)
  • Studio 3T (MongoDB client)
  • Mongoose (ODM)
  • yarn (package manager)
  • joi 라이브러리 (유효성 검사)

과제 주제로 Node.js와 express를 활용하여 쇼핑몰의 백엔드 서버를 만들게 되었음. 아래의 필수요구사항을 모두 구현하였음:

  1. 상품 작성 API
    • 상품명, 작성 내용, 작성자명, 비밀번호를 request에서 전달 받기
    • 상품은 두 가지 상태, 판매 중(FOR_SALE)및 판매 완료(SOLD_OUT) 를 가질 수 있습니다.
    • 상품 등록 시 기본 상태는 판매 중(FOR_SALE) 입니다.
  2. 상품 목록 조회 API
    • 상품명, 작성자명, 상품 상태, 작성 날짜 조회하기
    • 상품 목록은 작성 날짜를 기준으로 내림차순(최신순) 정렬하기
  3. 상품 상세 조회 API
    • 상품명, 작성 내용, 작성자명, 상품 상태, 작성 날짜 조회하기
  4. 상품 정보 수정 API
    • 상품명, 작성 내용, 상품 상태, 비밀번호를 request에서 전달받기
    • 수정할 상품과 비밀번호 일치 여부를 확인한 후, 동일할 때만 글이 수정되게 하기
    • 선택한 상품이 존재하지 않을 경우, “상품 조회에 실패하였습니다." 메시지 반환하기
  5. 상품 삭제 API
    • 비밀번호를 request에서 전달받기
    • 수정할 상품과 비밀번호 일치 여부를 확인한 후, 동일할 때만 글이 삭제되게 하기
    • 선택한 상품이 존재하지 않을 경우, “상품 조회에 실패하였습니다." 메시지 반환하기

대부분은 강의를 응용하면 API를 구현하는 방법 자체는 이해하기 어렵지 않았음. 다만 언제나 그렇듯 개발 과정에서 크고 작은 문제는 상당히 많았음 (순서 뒤죽박죽 주의)

  • MongoDB URL을 감추기 위해 dotenv를 활용하여 환경변수를 설정하였는데, 이를 EC2 인스턴스 서버에서 repository로부터 git clone하면 dotenv파일이 clone이 안됨. Classic하게는, 우분투 서버에서 직접 터미널을 통해 .env파일을 생성하여 MongoDB 연결이 가능하게 만들 수 있음(아래 참고). 근데, 향후 배운다 하지만 보통은 애초에 인스턴스 생성시 환경변수를 함께 설정하도록 한다 함 --- 이래야 유지 보수에 더 용이하다고 들음

vi .env -> 로컬의 .env파일내 내용 복붙 -> esc -> : -> wq -> enter 후 실행하면 잘 됨

  • 상품의 상태를 FOR SALE과 SOLD OUT으로 표현하기로 하였는데, router로는 잘 구현하였으나 insomnia로 데이터를 호출했을 때 나오지 않았음. 문제는 몽고DB 스키마에 해당 데이터가 저장되도록 정의가 되어있지 않았음 (내가 간과했음ㅠ). 둘 중 하나의 값을 보여주는 거라 코린이의 킹리적 갓심으론 Boolean 타입으로 해도 될 것 같았지만 우선은 만만한 String타입으로 하여 구현하였음
status: {
    type: String, // status 필드 추가
    default: "FOR_SALE", // 기본 값은 "FOR_SALE"
  },
  • 에러 핸들러 미들웨어를 별도로 만들어서 400번 에러코드(joi의 유효성검사를 통과하지 못했을 때)를 반환하도록 설정하였음. 상품등록 시 필드값들에 대해 try catch 문으로 코드를 쭉 읽고 해결이 안되면 next(error)를 통해 해당 미들웨어로 가게끔 하였으나, 상품수정 API가 해당 기능이 적용이 안되고 있는 걸 확인함
    이유는 변수로 선언해두었던 각 필드값이 상품 등록 API의 지역객체로 들어있었는데 나는 const로 선언이 된 걸 보고 그 밑에 작성한 API인 상품수정에는 별도로 선언이 필요 없을 거라 생각했음. 그래서 해당 변수들을 전역객체로 빼서 두(2) API에 적용할 수도 있었으나 알고보니 검증하는 게 좀 다른 부분이 있어 상품수정 API에도 별도로 변수 할당을 해주었음
const createdSpartSchema = joi.object({
  name: joi.string().min(3).max(50).required(),
  thumbnailUrl: joi.string(),
  price: joi.number().min(100).max(10000000).required(),
  description: joi.string().min(10).required(),
  seller: joi.string().min(3).max(30).required(),
  password: joi.string().min(4).required(),
});

const updatedSpartSchema = joi.object({
  name: joi.string().min(3).max(50),
  description: joi.string().min(10),
  status: joi.string(),
  price: joi.number().min(100).max(10000000),
});

// 1. 상품 작성 API
router.post("/goods", async (req, res, next) => {
  try {
    const validation = await createdSpartSchema.validateAsync(req.body);

    const { name, thumbnailUrl, price, description, seller, password } =
      validation; // 전역변수로 빼보자

    // 기본 상태는 판매 중
    const status = "FOR_SALE";

    const product = new Spart({
      name,
      thumbnailUrl,
      price,
      description,
      seller,
      password,
      status,
    });

    const savedProduct = await product.save();

    return res
      .status(201)
      .json({ message: "상품 등록됐음~~", product: savedProduct });
  } catch (error) {
    next(error);
  }
});

// 4. 상품 정보 수정 API
router.put("/goods/:productId", async (req, res, next) => {
  try {
    const { productId } = req.params;
    const { password, ...updateFields } = req.body;

    const validation = await updatedSpartSchema.validateAsync(updateFields);

    const product = await Spart.findById(productId).exec();

    if (!product) {
      return res.status(404).json({ message: "상품 없음...!!!" });
    }

    if (password !== product.password) {
      return res.status(401).json({ message: "비번 그게 아님" });
    }

    // 업데이트 필드 적용
    Object.assign(product, validation);

    const updatedProduct = await product.save();

    return res
      .status(200)
      .json({ message: "수정됐음~~", product: updatedProduct });
  } catch (error) {
    next(error);
  }
});
  • 마지막으로 기억에 남는 것은, 기존 강의자료에선 frontend 파일을 임의로 제공해주었음. 그러나 이번 프로젝트를 하며 해당 파일을 재사용하기엔 무리가 있어 내가 직접 만들고자 하였음 (사실 이거까지 과제인줄 알았으나 아니었음). 근데 conceptual하게 내가 구현한 API들을 렌더링 하는데 어떻게 파일을 구성하고 어떻게 연결해야 하는지 헷갈리고 잘 모르겠었음. 예를 들면 상품목록 조회 페이지가 최초에 나오면 특정 상품을 클릭 했을 때 상품상세조회 API로부터 데이터를 받아오고 또다른 html 파일이 열리며 렌더링이 되는 것인지 등등 온갖 개념들이 혼재되어 대단히 혼란스러웠음
    튜터님이 추천해준 건 ejs를 활용해서 서버사이드렌더링을 해보라 했으나, 필수기능을 모두 구현 후에 한 번 도전해보라 하셨음. 당시 모두 구현은 완료하였으나, 이게 그렇게 어려운 건가 싶었고 당연히 과제를 제출했을 때 화면이 보이는게 중요한 거 아닌가 라는 생각으로 객기를 부리며 도전(그 당시엔 도전이라 생각도 안함)했음.
    ejs 모듈을 yarn add ejs를 통해 다운받고 index.ejs 파일을 만들어 간단한 그리드 형태의 html 태그를 구현함. 몽고DB로부터 해당 ejs파일을 연결하는 것은 아래 ejs endeavor라 주석처리 해둔 부분에서 확인할 수 있음. 지금보면 당연해보이고 간단해 보이지만 이 과정에서 엄청난 시간을 낭비함. 자꾸 에러가 뜨며 뭐가 연결이 안됐고 몽고DB의 어떤 데이터가 ejs태그에 렌더링이 되지 않고 이런 저런 이슈들이 있었음.
    어찌저찌 ejs에 데이터를 호출하고 표현하는 것까진 완료하였으나 또 문제가 발생한 지점은 css였음. css파일이 적용이 안되는 것. 이건 여기저기 구글링해보니 같은 현상을 겪는 사람들이 꽤 있어 그나마 금방 해결했음. Express작동 방식에 의해 상위 폴더 경로는 별도 지정이 필요가 없다는 것을 뒤늦게 알게됨. 아래의 app.js와 ejs 코드들을 참고하면 좋을 듯 함
import express from "express";
import connect from "./schemas/index.js";
import spartRouter from "./routes/spart.router.js";
import errorHandler from "./middlewares/error-handler.js";
import Spart from "./schemas/spart.schemas.js";

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

connect();

// ejs endeavor
app.set("view engine", "ejs");
app.set("views", "./views");

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

// ejs endeavor
app.use(express.static("./style"));

app.use((req, res, next) => {
  console.log("Request URL:", req.originalUrl, "-", new Date());
  next();
});

const router = express.Router();

// ejs endeavor
app.get("/", async (req, res) => {
  const products = await Spart.find()
    .select("name thumbnailUrl price seller status createdAt")
    .sort("-createdAt")
    .exec();

  res.render("index", { post: products });
});

app.use("/api", [router, spartRouter]);

app.use(errorHandler);

app.listen(PORT, () => {
  console.log(PORT, "포트로 서버가 열렸어요!");
});
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Spart Store</title>
    <link rel="stylesheet" href="styles.css" />
  </head>
  <body>
    <h1>Spart Store</h1>
  
    <div class="product-grid">
      <% post.forEach(product => { %>
        <div class="product">
          <img src="<%= product.thumbnailUrl %>" alt="<%= product.name %>">
          <h3><%= product.name %></h3>
          <p><%= product.price %> 원</p>
          <p><%= product.seller %></p>
          <p><%= product.status %></p>
        </div>
      <% }); %>
    </div>
  </body>
</html>

 

느낀점/배운점은 과제 제출하며 작성한 답변으로 퉁치겠음:

Q2. 대표적인 HTTP Method의 4가지 (GET, POST, PUT, DELETE)는 각각 어떤 상황에서 사용하였나요?
- 상품등록: POST / 상품목록조회 및 상세조회: GET / 상품정보수정: PUT / 상품삭제: DELETE

Q3. API 설계 시 RESTful한 원칙을 따랐나요? 어떤 부분이 RESTful한 설계를 반영하였고, 어떤 부분이 그렇지 않았나요?
- REST의미에 따라 직관적으로 리소스를 정의하여 이해하기 쉽도록 RESTful한 설계를 반영하였습니다. URL 경로별 앞서 언급한 HTTP method에 따라 해당 자원에 대한 기능/행위를 표현할 수 있도록 구현했습니다.

Q4. 폴더 구조(Directory Structure)를 역할 별로 분리하였다면, 어떤 이점을 가져다 주었을까요?
- 우선 기능과 역할별로 폴더 구조를 분리하며 느낀점은 가독성이 높아 코드를 쉽게 찾고 이해하는데 용이하다는 것입니다. 이는 향후 유지보수에 있어서도 도움이 될 것이라 생각되며, 특정 기능이나 모듈을 담당하는 파일들이 한 폴더에 모여있으면 해당 기능에 대한 테스트나 디버깅에도 수월할 것 같습니다.

Q5.  mongoose에서 상품 상태는 어떠한 방식으로 관리하였나요? 이 외에도 어떤 방법들이 있었을까요?  
- 상품 상태의 경우 저는 String타입으로 관리하였으나 옵션이 두 가지(FOR SALE / SOLD OUT) 뿐이라 boolean타입으로도 관리할 수 있을 것 같다 생각했습니다. 제 todo 리스트에 있었으나 시간관계상 시도해보지 못했습니다.

'projects > node.js' 카테고리의 다른 글

[Node.js] sparamin 1 - 공사중  (2) 2024.01.31
[Node.js] Spart Store 2  (4) 2024.01.23
[Node.js] NBC Movie Review 2  (3) 2024.01.16
[Node.js] - NBC Movie Review 1  (1) 2024.01.11
2024.01.08 - 개인프로젝트 1차 제출完  (2) 2024.01.08

관련글 더보기