Skip to content

[이상경] 스프린트 미션 5, 6#72

Open
chomchomii wants to merge 17 commits into
codeit-sprint-fullstack:react-이상경from
chomchomii:react-이상경

Hidden character warning

The head ref may contain hidden characters: "react-\uc774\uc0c1\uacbd"
Open

[이상경] 스프린트 미션 5, 6#72
chomchomii wants to merge 17 commits into
codeit-sprint-fullstack:react-이상경from
chomchomii:react-이상경

Conversation

@chomchomii

@chomchomii chomchomii commented Jun 30, 2026

Copy link
Copy Markdown
Collaborator

스프린트 미션 5

기본 요구사항

공통

  • Github에 스프린트 미션 PR을 만들어 주세요.
  • React, Express를 사용해 진행합니다.

프론트엔드 구현 요구사항

랜딩 페이지

  • HTML과 CSS로 구현한 랜딩페이지를 React로 마이그레이션하세요.
  • 랜딩 페이지 url path는 "/"로 설정하세요.

중고마켓 페이지

라우팅

  • 중고마켓 페이지 url path를 "/items"으로 설정하세요.
  • 페이지 주소가 "/items" 일 때 상단내비게이션바의 "중고마켓" 버튼의 색상은 "3692FF"입니다.

상품 목록

  • 중고마켓 페이지 판매 중인 상품은 본인이 만든 GET 메서드를 사용해 주세요.
    • 다만 좋아요 순 정렬 기능은 제외해 주세요.
    • 사진은 디폴트 이미지로 프론트엔드에서 처리해주세요.
    • 베스트 상품 목록 조회는 구현하지 않습니다.

상품 등록 이동

  • '상품 등록하기' 버튼을 누르면 "/registration" 로 이동합니다. ( 빈 페이지 )

상품 등록 페이지

페이지 구성

  • PC, Tablet, Mobile 디자인에 해당하는 상품 등록 페이지를 만들어 주세요.
  • 상품 등록 url path는 "/registration"입니다.

상품 등록

  • 상품 등록은 본인이 만든 POST 메서드를 사용해 주세요.
  • 등록 성공 시, 해당 상품 상세 페이지로 이동합니다. (빈페이지)

백엔드 구현 요구사항

중고마켓

Product 스키마

  • Product 스키마를 작성해 주세요.
    • id, name, description, price, tags, createdAt, updatedAt필드를 가집니다.
    • 필요한 필드가 있다면 자유롭게 추가해 주세요.

API (등록, 조회, 수정, 삭제)

  • 상품 등록 API를 만들어 주세요.
    • name, description, price, tags를 입력하여 상품을 등록합니다.
  • 상품 상세 조회 API를 만들어 주세요.
    • id, name, description, price, tags, createdAt를 조회합니다.
  • 상품 수정 API를 만들어 주세요.
    • PATCH 메서드를 사용해 주세요.
  • 상품 삭제 API를 만들어 주세요.
  • 상품 목록 조회 API를 만들어 주세요.
    • id, name, price, createdAt를 조회합니다.
    • offset 방식의 페이지네이션 기능을 포함해 주세요.
    • 최신순(recent)으로 정렬할 수 있습니다.
    • name, description에 포함된 단어로 검색할 수 있습니다.

API 에러 처리

  • 각 API에 적절한 에러 처리를 해 주세요.
  • 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요.

서버 환경

  • . env 파일에 환경 변수를 설정해 주세요.
  • CORS를 설정해 주세요.
  • render.com로 배포해 주세요.
  • MongoDB를 활용해 주세요.

프론트엔드 심화 요구사항

상품 등록 페이지

버튼 활성화

  • 모든 입력 input box에 빈 값이 있을 경우, 등록 버튼이 비활성화됩니다.

태그 기능

  • 태그를 입력한 후 엔터키를 누르면, 그 태그가 칩 형태로 쌓입니다.

Validation

  • 상품명, 상품 소개, 판매 가격, 태그에 대한 유효성 검사 Custom Hook을 만들어주세요. 유효성 검사를 통과하지 않을 경우, 각 input에 빨간색 테두리와, 각각의 Input 아래에 빨간색 에러 메시지를 보여주세요.
    • 유효한 조건
      • 상품명: 1자 이상, 10자 이내
      • 상품 소개: 10자 이상, 100자 이내
      • 판매 가격: 1자 이상, 숫자
      • 태그: 5글자 이내


스프린트 미션 6

기본 요구사항

중고마켓

MongoDB → PostgreSQL 마이그레이션

  • mongoDB에서 PostgreSQL을 사용하도록 코드를 마이그레이션 해주세요.

공통

데이터베이스

  • PostgreSQL를 이용해 주세요.
  • 데이터 모델 간의 관계를 고려하여 onDelete를 설정해 주세요.
  • 데이터베이스 시딩 코드를 작성해 주세요.

API 에러 처리

  • 각 API에 적절한 에러 처리를 해 주세요.
  • 각 API 응답에 적절한 상태 코드를 리턴하도록 해 주세요.

자유게시판

Article 스키마

  • Article 스키마를 작성해 주세요.
    • id, title, content, createdAt, updatedAt 필드를 가집니다.

API (등록, 조회, 수정, 삭제)

  • 게시글 등록 API를 만들어 주세요.

    • title, content를 입력해 게시글을 등록합니다.
  • 게시글 조회 API를 만들어 주세요.

    • id, title, content, createdAt를 조회합니다.
  • 게시글 수정 API를 만들어 주세요.

  • 게시글 삭제 API를 만들어 주세요.

  • 게시글 목록 조회 API를 만들어 주세요.

    • id, title, content, createdAt를 조회합니다.
    • offset 방식의 페이지네이션 기능을 포함해 주세요.
    • 최신순(recent)으로 정렬할 수 있습니다.
    • title, content에 포함된 단어로 검색할 수 있습니다.

댓글

API (등록, 조회, 수정, 삭제)

  • 댓글 등록 API를 만들어 주세요.

    • content를 입력하여 댓글을 등록합니다.
  • 댓글 수정 API를 만들어 주세요.

    • PATCH 메서드를 사용해 주세요.
  • 댓글 삭제 API를 만들어 주세요.

  • 댓글 목록 조회 API를 만들어 주세요.

    • id, content, createdAt 를 조회합니다.
    • cursor 방식의 페이지네이션 기능을 포함해 주세요.

중고마켓 / 자유게시판 API 분리

  • 중고마켓, 자유게시판 댓글 등록 API를 따로 만들어 주세요.
  • 중고마켓, 자유게시판 댓글 목록 조회 API를 따로 만들어 주세요.

구현 중 고민한 점 (미션 6)

1. Comment 모델 설계

댓글은 상품 댓글과 게시글 댓글을 모두 지원해야 하므로 Comment 모델에서 productId, articleId를 선택적으로 가질 수 있도록 설계했습니다. 상품 댓글인 경우에는 productId를, 게시글 댓글인 경우에는 articleId를 연결하도록 했고, Product 또는 Article이 삭제될 때 연결된 댓글도 함께 삭제되도록 onDelete: Cascade를 적용했습니다.

2. 댓글 API 공통 로직 분리

상품 댓글과 게시글 댓글은 라우트는 분리되어 있지만, 내부 생성/조회 흐름은 거의 동일했기에 중복을 줄이기 위해 댓글 관련 공통 로직을 commentService.js로 분리해봤습니다. 댓글 생성 시 [parentKey]: parentId 형태를 사용해 productId 또는 articleId를 동적으로 연결하도록 구현해서 상품 댓글과 게시글 댓글 등록 로직을 하나의 함수로 공통화했습니다.

3. Cursor Pagination 검증

댓글 목록 조회에는 cursor 방식 페이지네이션을 적용했습니다. 단순히 댓글이 조회되는지만 확인하지 않고, seed 데이터에서 특정 상품과 게시글에 댓글을 여러 개 연결해 실제 페이지 이동이 정상적으로 동작하는지 확인했습니다. 현재는 createdAt desc 정렬과 id cursor를 사용하고 있습니다. 동작은 확인했지만, 같은 생성일자를 가진 데이터가 여러 개 있을 경우 더 안정적인 페이지네이션을 위해 추후 createdAt + id를 함께 사용하는 방식으로 리팩토링 예정입니다.

4. 응답 형식 정리

성공/실패 응답을 확인하기 쉽도록 messagecode를 함께 반환하도록 했습니다. 삭제 API의 경우 처음에는 204 No Content도 고려했지만, 성공 메시지와 code를 응답으로 내려주기 위해 200 OK와 JSON 응답을 사용하는 방식으로 변경했습니다.

5. 테스트 데이터 설계

API 테스트를 위해 작성한 seed 데이터는 Product, Article은 검색/정렬/페이지네이션을 확인할 수 있도록 여러 개의 데이터를 넣었고, Comment는 cursor pagination을 검증하기 위해 특정 상품과 게시글에 여러 개의 댓글이 연결되도록 구성했습니다. 이 과정을 통해 CRUD뿐만 아니라 실제 관계 데이터와 페이지네이션 흐름까지 확인할 수 있었습니다.

테스트 완료

  • Product 등록, 목록 조회, 상세 조회, 수정, 삭제 테스트
  • Article 등록, 목록 조회, 상세 조회, 수정, 삭제 테스트
  • Comment 등록, 목록 조회, 상세 조회, 수정, 삭제 테스트
  • Product / Article 검색, 정렬, 페이지네이션 테스트
  • 상품 댓글 / 게시글 댓글 cursor pagination 테스트
  • 상품 댓글 6개를 limit=2로 조회하여 1페이지, 2페이지, 3페이지 모두 중복/누락 없이 조회되는지 확인
  • 게시글 댓글 4개를 limit=2로 조회하여 1페이지, 2페이지 모두 중복/누락 없이 조회되는지 확인
  • 마지막 페이지에서 nextCursor: null이 반환되는지 확인
  • 존재하지 않는 상품, 게시글, 댓글에 대한 조회, 수정, 삭제 에러 처리 테스트

@devbini
devbini self-requested a review June 30, 2026 11:58
@chomchomii chomchomii changed the title [이상경] 스프린트 미션 5 [이상경] 스프린트 미션 5, 6 Jul 6, 2026

@devbini devbini left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 고생 많으셨습니다!

단순히 API만 추가한 게 아니라 routes와 services/validators, 그리고 미들웨어 등 서버 코드의 역할을 나누려고 한 점이 특히 좋았습니다. (역할 분할)
이러한 역할 분할은 현업에서도 중요하게 생각하면서 많이 간과하는데, 좋은 습관이 자리잡고 있는 것 같아요.

특히 댓글 구현에 있어서 라우트는 분리하면서도 내부 생성/조회 로직은 공통화하려고 한 점이 좋았어요. 연결 필드 관련 개발 방식도 처음 관계형 데이터 모델을 다루는 단계에서 꽤 좋은 시도이자 경험치였다 생각합니다.

superstruct와 errorHandler를 따로 나눠서 검증과 에러처리를 나눠서 관리하고, seed 데이터도 검색/정렬/페이지네이션 등을 세밀하게 확인할 수 있도록 꽤 많이 준비해둔 점이 좋았습니다.

PR 본문에 적어주신 것처럼 cursor pagination을 직접 여러 페이지로 나눠 테스트해본 것도 인상적이었어요. 단순 CRUD만 확인하고 끝낸 게 아니라 실제 데이터 흐름을 보려고 한 점이 보였습니다. 이런 실험 결과와 과정은 나중에 큰 도움이 될 거라 생각해요.

다만, 그래도 실제 코드 흐름 기준으로는 프론트와 백엔드 응답 형태가 아직 하나로 이어지진 않는 것 같긴 해요. 일부 라우트나 CORS 설정처럼 화면에서 바로 티가 날 수 있는 부분부터 먼저 맞추면 전체 완성도가 훨씬 올라갈 것 같습니다! (실제로 내가 사용자라면 뭘 해볼까? 를 생각해보는 사고를 기르는거죠!!)

전반적으로... 구조화에 대한 감각이 있는 것 같아요.
이제는 조금 더 단단하게 다듬으면 다음 작업에서 훨씬 안정적으로 확장할 수 있을 것 같아요.
요구사항에 따라 세밀하게 관리하고 조절하는 능력만 천천히 기르는거죠!
이미 엔지니어링적 사고는 거의 잡혀가고 계신 것 같으니까요..!

너무나 고생 많으셨고, 파이팅입니다! 👍


추가로.. PR 본문에 적어주신 “구현 중 고민한 점”에 답을 남기면...

Comment 모델을 하나로 통합한 선택은 현재 단계에서는 합리적이라 생각은 하지만. 다만 productId, articleId가 모두 optional이 되면서 잘못된 상태가 들어올 여지가 생긴 것 같아요. 라우터나 서비스 레이어에서 “둘 중 하나만 연결” 규칙을 잡아주면 좋겠습니다.
지금은 안전 장치가 없어서- 공격이 가능한 경로가 생겨버렸다.. 라고도 생각이 되는 것 같아요.

댓글 공통 로직을 별도 파일로 뺀 것도 좋아요. 특히 parentKey는 중복을 줄이는 데 의미가 있지요.... 이제는 유효하지 않은 값 등 비정상 데이터를 정리하기만 해도 충분 해 보입니다.

cursor pagination은 위에서 이미 언급했지만.. 직접 테스트해본 점이 너무 좋았어요.
createdAt + id를 함께 쓰는 방식으로 정렬 안정성까지 챙기면 훨씬 프로덕션에 가까운 구현이 될 것 같습니다.

전반적으로 구현과 테스트, 공부 방향이 잘 잡혀있는 것 같아요.
어느 회사로 가게 될지, 벌써 그 회사가 부럽다는 생각이 드네요ㅎㅎ

})

// 백엔드 응답이 _id 또는 id인 경우 모두 대응
navigate(`/items/${product._id || product.id}`)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Check Point

상품 등록 후 이동하는 페이지의 매개변수를 product._id로 쓰고 있네요?
제가 알기론 Prisma 데이터 응답 식별자가 _id가 아니라 id를 쓸 텐데
등록이 성공해도 이동하는 nav 주소가 undefined가 되는 불상사가 있을 수도 있을 것 같아요.

실제로 구동 시 문제가 없다면 괜찮겠지만
혹시 모르니 리뷰 남겨둘게요.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Check Point

추가로 상품 등록 성공 후 /items/:id로 이동하려는 코드인데,
App.jsx에 저장 된 현재 라우터에는 /items/:id 경로가 없는 것 같아요-!

Comment thread server/index.js
Comment on lines +7 to +16
const app = express()
const PORT = process.env.PORT || 3000

app.use(express.json())

app.use(productRoutes)
app.use(articleRoutes)
app.use(commentRoutes)

app.use(errorHandler)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☕ Thinking

음.. 문제는 없지만, 프론트와 백엔드가 각각 Vite dev server와 Express server로 다른 포트에서 뜨면 브라우저 요청이 CORS 정책에 막힐 수 있어요.

cors 패키지는 의존성에 들어가 있으니 import cors from 'cors'app.use(cors())를 추가해두면 차후에 나올 문제를 미리 방지할 수 있지 않을까요?🤗

<p>검색 결과가 없습니다.</p>
) : (
products.map((product) => (
<ProductCard key={product._id} product={product} />

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☕ Thinking

상품 카드 렌더링에서 key={product._id}를 쓰고 있네요!
아까... 위쪽 다른 Check Point에서도 있던 문제랑 동일한데

현재 Prisma 기반 백엔드는 id를 내려주고 있어서 _id는 없어 보여요.
화면이 당장 깨지지는 않을 수 있지만... React 리스트 생성 키가 undefined가 되면 정렬이나 페이지 이동이 발생할 때 렌더링이 깨져버릴 수 있어 보여요!

Comment on lines +11 to +12
const skip = Number(offset) || 0
const take = Number(pageSize) || 10

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☕ Thinking

이것도 문제는 아니지만,
숫자를 바로 DB로 던지고 있는 것 같아요. 페이지네이션 관련 데이터를..

정상적인 상황에서는 문제가 없지만, 실제 환경에서는 어떤 문제가 나올지 몰라요.
예를 들어- limit 데이터가 음수로 들어간다거나, 비정상적으로 엄청 큰 데이터가 들어간다던가.... 그러면 Prisma 에러가 나거나 서버가 너무 큰 조회를 하게 될 수 있어요.

숫자 변환 후 하한/상한을 검증하는 공통 함수를 하나 만들어두면 상품/게시글 목록 조회에서 같이 쓸 수 있을 것 같습니다.

또는 Math 라이브러리를 활용하면 아마 상/하 제한을 걸 수 있을텐데, 서버 리소스 보호를 생각하는 방법이 하나 더 있지 않을까 싶어 남겨둡니다ㅎㅎ

content: true,
createdAt: true,
},
orderBy: { createdAt: 'desc' },

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☕ Thinking

PR 본문에도 적어주신 부분인데, 현재 댓글 페이지네이션은 createdAt으로 정렬하면서 cursor는 id를 사용하고 있네요.

이건... 데이터가 적을 때는 잘 동작해 보일 수 있지만, 같은 createdAt을 가진 댓글이 여러 개 생기면 페이지 사이에서 순서가 꺠지거나 레이스컨디션이 생길 수도 있어요.
위에 주석으로 리팩토링 예정이라고 적어주신 것처럼, createdAt desc, id desc를 함께 쓰는 방식으로 정리하면 더 안정적일 것 같습니다 🙇🤗

Comment on lines +32 to +35
productId String?
product Product? @relation(fields: [productId], references: [id], onDelete: Cascade)
articleId String?
article Article? @relation(fields: [articleId], references: [id], onDelete: Cascade)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Check Point

현재 스키마 구조를 보면,
productId와 articleId가 모두 없는 댓글도 만들 수 있고, 반대로 둘 다 있는 댓글을 만들 수 있네요.

XOR 제약을 직접 표현하기 어렵다면, 댓글 생성 라우터에서 productId와 articleId 중 하나만 들어가도록 검증을 추가하거나, ProductComment, ArticleComment와 같이 모델을 분리하는 방법이 필요 해 보여요.

})

commentRoutes.post('/articles/:articleId/comments', async (req, res) => {
assert(req.body, CreateComment)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

☕ Thinking

assert(req.body, CreateComment)가 try 밖에 있네요.
의도 된 건지 까먹은건지는 모르겠지만, 아래쪽에 다른 코드에도 동일한 내용이 보입니다.

async handler에서 던진 에러가 미들웨어로 넘어갈 수 있긴 하지만..
같은 파일 안에서 어떤 에러는 route catch에서 처리되고 어떤 에러는 전역 errorHandler로 넘어가는 구조가 된 것 같아요.

쉽게 말하면, 통일성이 떨어져 보이기도 하는 것 같아요.
검증하는 로직도 try 안으로 넣거나.. 별도의 검증 미들웨어를 따로 두면 에러 응답 형식이 더 일관되게 되어 유지보수적으로 이점이 생기지 않을까 싶습니다.

res.sendStatus(404)
} else {
console.log(err)
res.status(500).send({ message: err.message })

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Check Point

음.. 이건 조금 위험해요.
요구사항에 500에 대한 응답 에러처리가 있어서 이렇게 해도 되는건지는 잘 모르겠지만..

500의 경우에는 DB 연결 내용이나 구현에 있어 치명적일 수 있는 서버 오류메시지가 사용자에게 그대로 전달되게 됩니다.

가능하면 message를 하드하게 별도로 만들거나, 에러핸들러를 따로 만들어서 관리하는 게 좋을 것 같아요.

블라인드 SQL 인젝션 등, 다양한 보안적 위험요소가 존재한답니다.

자세한 오류 로그는 서버 로그에만 남기는 게 좋아요! 🥹

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants