[황영일] 스프린트 미션 6#80
Conversation
devbini
left a comment
There was a problem hiding this comment.
🎉 고생 많으셨습니다!
이번 PR은 기존 프론트엔드에 Express 서버와 Prisma, PostgreSQL 기반의 데이터 모델을 붙여보는 작업인데.,.. 서비스 안의 데이터가 어떻게 연결되는지 고민한 흔적이 보인 것 같아요.
특히 댓글이 게시글과 상품 양쪽에 연결될 수 있다는 점을 스키마에 반영해보려고 한 부분이나, 사용자 삭제 시 SetNull이나 Cascade처럼 관계별 삭제 전략도 잡아간 부분도 좋았습니다.
requests.http로 API 호출 예시를 남겨둔 것도 좋았습니다. 이런 파일은 본인 테스트에도 좋고, 다음 사람이 서버를 확인할 때도 꽤 도움이 되거든요.
다만 아직 프론트와 백엔드가 아직 하나의 흐름으로 딱 맞지 않는다는 느낌이 조금 있어요.
전체적으로 로컬에서 앱을 실행했을 때 한 번에 검증하기 어려워 보였습니다. 이 부분은 기능을 더 추가하기 전에 먼저 맞춰두면 이후 디버깅이 훨씬 쉬워질 것 같습니다.
또... 공통 에러 핸들러처럼 새로 배운 도구를 적극적으로 써보려는 시도도 좋았지만 다만 이런 도구들은 입력값이 조금만 어긋나도 바로 500으로 이어질 수 있어서, userId나 페이지네이션 값처럼 외부에서 들어오는 값은 한 번 더 검증하는 습관을 들이면 훨씬 안정적인 코드가 될 것 같아요.
앞으로는 프론트 API 주소처럼 실행 흐름에 직접 영향을 주는 부분부터 정리해보면 완성도가 많이 올라갈 것 같습니다.
너무나 고생 많으셨고, 파이팅입니다! 👍
| // // PRODUCTS // | ||
| // //GET list | ||
| // app.get("/products", async (req, res) => { | ||
| // try { | ||
| // const sort = req.query.sort; | ||
| // const page = Number(req.query.page) || 1; | ||
| // const limit = Number(req.query.count) || 10; | ||
|
|
||
| // const skipIndex = (page - 1) * limit; | ||
|
|
||
| // const sortOption = { _id: sort === "recent" ? "desc" : "asc" }; | ||
|
|
||
| // const totalProducts = await Product.countDocuments(); | ||
| // const totalPages = Math.ceil(totalProducts / limit); | ||
|
|
||
| // const products = await Product.find() | ||
| // .sort(sortOption) | ||
| // .skip(skipIndex) | ||
| // .limit(limit); | ||
|
|
||
| // res.send({ | ||
| // products: products, | ||
| // currentPage: page, | ||
| // totalPages: totalPages, | ||
| // }); | ||
| // } catch (e) { | ||
| // res.status(500).send({ message: "서버 오류가 발생했습니다" }); | ||
| // } | ||
| // }); | ||
|
|
||
| // //GET id | ||
| // app.get("/products/:id", async (req, res) => { | ||
| // const product = await Product.findById(req.params.id); | ||
| // if (product) { | ||
| // res.send(product); | ||
| // } else { | ||
| // res.status(404).send({ message: "Cannot find given id." }); | ||
| // } | ||
| // }); | ||
|
|
||
| // //POST | ||
| // app.post("/products", async (req, res) => { | ||
| // const newProduct = await Product.create(req.body); | ||
| // res.status(201).send(newProduct); | ||
| // }); | ||
|
|
||
| // //PATCH | ||
| // app.patch("/products/:id", async (req, res) => { | ||
| // const id = req.params.id; | ||
| // const product = await Product.findById(id); | ||
|
|
||
| // if (product) { | ||
| // Object.keys(req.body).forEach((key) => { | ||
| // product[key] = req.body[key]; | ||
| // }); | ||
|
|
||
| // // 이것 작성해야 mongoDB에 저장됨. 필수! | ||
| // await product.save(); | ||
|
|
||
| // res.send(product); | ||
| // } else { | ||
| // res.status(404).send({ message: "Cannot find given id." }); | ||
| // } | ||
| // }); | ||
|
|
||
| // //DELETE | ||
| // app.delete("/products/:id", async (req, res) => { | ||
| // const id = req.params.id; | ||
| // const product = await Product.findByIdAndDelete(id); | ||
| // if (product) { | ||
| // res.sendStatus(204); | ||
| // } else { | ||
| // res.status(404).send({ message: "Cannot find given id. " }); | ||
| // } | ||
| // }); |
There was a problem hiding this comment.
🚩 Check Point
혹시 어떤 이유가 있는 걸까요?
현재 상태만으로는 상품 목록 조회부터 등록 흐름까지 확인하는 데 어려움이 있는 것 같아요.
다른 곳에서 제대로 구현을 이미 했다면, 사용하지 않는 코드는 바로바로 제거해서 실제로 동작하는 API들만 남겨주시는 게 좋습니다 🤗
| <div className='pagination'> | ||
| <button | ||
| onClick={() => setPage(page - 1)}> | ||
| {'<'} | ||
| </button> | ||
|
|
||
| {pageNumbers.map((pageNumber) => ( | ||
| <button key={pageNumber} | ||
| onClick={() => setPage(pageNumber)}>{pageNumber} | ||
| </button> | ||
| ))} | ||
|
|
||
| <button | ||
| onClick={() => setPage(page + 1)}> | ||
| {'>'} | ||
| </button> |
There was a problem hiding this comment.
🚩 Check Point
1페이지에서 이전 버튼을 누르면 page가 0이 되고, 마지막 페이지에서 다음 버튼을 누르면 전체 페이지보다 큰 값이 요청되는 문제가 발생할 것 같아요.
이 경우 API가 예상하지 못한 페이지 값을 받거나, 빈 데이터가 계속 렌더링될 수 있어요.
이전 버튼은 page === 1일 때 비활성화하고, 다음 버튼은 page === totalPages일 때 막아두는 예외처리에 대한 장애대응을 습관화 해 두시면 좋습니다.
| app.delete("/articles/:id", async (req, res) => { | ||
| const { id } = req.params; | ||
| await prisma.article.delete({ where: { id } }); | ||
| res.send(204); |
There was a problem hiding this comment.
🚩 Check Point
보통 상태 코드를 보낸다 라는 건 res.sendStatus 또는 res.status(204).send(...) 형태로 써 주셔야 해요.
이 경우에는 그저 "204" 라는 "string 값"을 반환하는 것으로 보여서, 원하는 대로 동작하지 않을 위험이 보이네요!
| name: name, | ||
| description: description, | ||
| price: Number(price), | ||
| tags: tags, |
There was a problem hiding this comment.
☕ Thinking
상품 등록 요청에서 tags가 문자열 하나로 전송되고 있네요.
프론트 상태도 useState('')라서 여러 태그를 배열로 관리하기 어렵고, 서버나 API가 tags: string[] 형태를 기대한다면 문제가 생길 수 있어요.
음.. 가능하면 이렇게 "여러개를 관리해야 하는 요소"라면, 배열로 관리하는 게 더 유지보수적으로도, 코드를 개발할 때도 좋은 방법이 되기도 한답니다.
거기에 추가로 Enter로 추가하는 흐름을 만들면 UX적으로도 향상이 될 것 같아요ㅎㅎ 🤗
| // ARTICLES | ||
| // | ||
| app.get("/articles", async (req, res) => { | ||
| const { offset = 0, limit = 10, order = "newest" } = req.query; |
There was a problem hiding this comment.
☕ Thinking
이것도 문제는 아니지만,
숫자를 바로 DB로 던지고 있는 것 같아요. 페이지네이션 관련 데이터를..
정상적인 상황에서는 문제가 없지만, 실제 환경에서는 어떤 문제가 나올지 몰라요.
예를 들어- limit 데이터가 음수로 들어간다거나, 비정상적으로 엄청 큰 데이터가 들어간다던가...
Math 라이브러리를 활용하면 아마 상/하 제한을 걸 수 있을텐데, 서버 리소스 보호를 생각하는 방법이 하나 더 있지 않을까 싶어 남겨둡니다ㅎㅎ
| article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade) | ||
| articleId String? | ||
| product Product? @relation(fields: [productId], references: [id], onDelete: Cascade) | ||
| productId String? |
There was a problem hiding this comment.
🚩 Check Point
현재 스키마 구조를 보면,
productId와 articleId가 모두 없는 댓글도 만들 수 있고, 반대로 둘 다 있는 댓글을 만들 수 있네요.
XOR 제약을 직접 표현하기 어렵다면, 댓글 생성 라우터에서 productId와 articleId 중 하나만 들어가도록 검증을 추가하거나, ProductComment, ArticleComment와 같이 모델을 분리하는 방법이 필요 해 보여요.
| const comment = await prisma.comment.create({ | ||
| data: { | ||
| content: req.body.content, | ||
| user: { connect: { id: req.body.userId } }, |
There was a problem hiding this comment.
☕ Thinking
댓글 생성 시 userId를 body에서 받아 바로 connect하는데
바로 위 코멘트처럼 스키마상 Comment.userId는 optional이라, 실제 API 관점에서는 userId가 필수인지 선택인지 애매한 것 같아요.
지금 코드에서는 userId가 없으면 Prisma 에러로 이어질 수 있으니, 필수라면 400으로 먼저 막고, 선택이라면 userId가 있을 때만 connect하는 구조를 추가하는 건 어떨까요?
| return res.status(404).json({ error: `Couldn't find requested data` }); | ||
| } | ||
|
|
||
| res.status(500).json({ message: err.message }); |
There was a problem hiding this comment.
🚩 Check Point
음.. 이건 조금 위험해요.
요구사항에 500에 대한 응답 에러처리가 있어서 이렇게 해도 되는건지는 잘 모르겠지만..
500의 경우에는 DB 연결 내용이나 구현에 있어 치명적일 수 있는 서버 오류메시지가 사용자에게 그대로 전달되게 됩니다.
가능하면 message를 하드하게 별도로 만들거나, 에러핸들러를 따로 만들어서 관리하는 게 좋을 것 같아요.
블라인드 SQL 인젝션 등, 다양한 보안적 위험요소가 존재한답니다.
자세한 오류 로그는 서버 로그에만 남기는 게 좋아요! 🥹
요구사항
기본 요구사항
중고마켓
공통
자유게시판
댓글