김데이의 개발공부

[ TIL ] Day 25 ~ 26 - 실습 : 관계 모델 CRUD 작성 + Route 분리 / 유효성 검사 적용 본문

코드잇 Node.js(BE) 부트 캠프/TIL (Today I Learn) 📑

[ TIL ] Day 25 ~ 26 - 실습 : 관계 모델 CRUD 작성 + Route 분리 / 유효성 검사 적용

theday365 2025. 10. 31. 19:32
반응형

🗓️ 수업 일자 : 2025.10.30~31

✨ 오늘의 수업 평가 :  [ PROJECT ]  드디어 형태가 보인다 👀🏗️

 

Comment 모델은 "목록 전용 페이지"와 각 "Product / Article에 모아 보기(및 부가 기능)"가 있다고 가정하고 작업했다.

 

"Comment 목록 전용 페이지"에서 사용할 RUD요청은 금방 작성 했는데, 

C = Create 는 로직 상 Product / Article에 각각 생기는 게 맞을 거 같아서

각각의 모델 페이지에서 작업했는데 조금 헤멧다 😅

(각 모델의 입장에선 comments가 수정되는거니까.. PATCH로 작업 한건데 맞는지 모르겠다 ㅎ) 

 

제일 어려웠던건 Product 또는 Article 상세 목록에서 특정 comment를 지정하여 "수정, 삭제" 하는 부분이었는데 

이렇게 되면 route가 "/products/:productid/comments/:commentId" (혹은 더 짧게)되면서 ID도 두 번 사용하고, 

실제 필요한 데이터의 깊이도 깊어지면서 뭔가 하면 할 수록 꼬여서

하루 넘게 작업하다 포기하고 "Comment 목록 전용 페이지" 에서 썻던 Patch/Delete 요청을 그대로 썼다 

 

그러면서 전체 일정이 많이 늦어져서 마음이 급해졌.. 

 

 

👩‍💻 [프로젝트] 오늘 작업 내용 💻

- 각 모델 별 CRUD 작업

   1. 관계형 모델 CRUD 작업 : Comment + Product / Comment + Article 
   2. 개별 모델 GET 요청 추가 작업 : Product / Article GET

- 모델 Route 분리 작업

   1. 관계형 모델 Route 분리 

   2. 개별 모델 Route 재정리

- 유효성 검사 적용 

 


0. 전체 작업 순서 

  1. 작업 내용 분석 : 데이터 모델 정리, 작업 파일 구조화 등 (완벽하지 않아도 됨, 전체 틀을 잡는 다는 생각으로)
  2. 작업 공간 셋팅 : 필요한 npm 다운로드 및 설치 확인(기본 구문으로 테스트), 필요 시 Git 연결 (with new branch)
  3. 데이터 모델링 및 시딩 작업 
    • 데이터 모델링 : schema.prisma 파일에 작업 후 마이그레이션(초기 init) 작업
    • 시딩 작업 : 비어있는 DB에 테스트용 초기 데이터를 넣는 작업
  4. 각 모델 별 CRUD 작업 : (아직 익숙하지 않으므로) app.js 혹은 main.js에 한번에 작업
    이후 모델 별 route 분리 작업 : route 폴더에 모델별로 1차 분기
                                                  → xx_route .js에는 서버 요청만 남기고 xx_controller.js 에 실행 함수 분리.
  5. 유효성 검사 적용 : 공용으로 제작 후, 각 모델별로 분리하여 xx_controller.js에 적용
  6. 비동기 에러 래퍼 함수 적용 : 서버 다운을 막기 위하여 각 xx_route.js에 적용
  7. 미들웨어 적용 작업 : 공용 / 각 모델별 미들웨어를 각각 분리해서 app.js / xx_route.js 에 각각 적용

- 작업을 진행하며 xx.http를 이용해 지속적인 검토 진행

- 데이터 모델링 수정이 필요한 경우, 작업 후 꼭 마이그레이션 진행 

 

 

1-1. 관계형 모델 CRUD 작업

1. 관계형 모델에서 사용 할 요청작업(with URL) 정리 

   1) Comment 모델에서 작업

요청 URL 기능 부가설명
GET /comments 전체 댓글 조회 where + category 쿼리 스트링 조합으로 
product / article 모아보기 제공
/comments/:id 세부 댓글 조회 where + id 조합으로 세부 댓글 보기
PATCH /comments/:id 댓글 조회 페이지에서 수정 comment의 content만 수정 가능
DELETE /comments/:id 댓글 조회 페이지에서 삭제 연결 된 product / article 이 아닌 현재 댓글만 삭제

 

   2) Product 모델에서 작업
       - Product 상세페이지에서 요청 작업 진행

요청 URL prisma method 기능
GET /products/:id/comments .findUnique() product 상세페이지의 댓글 확인
POST comment.create()
product.update()
product 상세페이지의 댓글 신규 작성
(이미 있는 상품에 댓글을 넣는 작업이므로
생성 후 업데이트 작업)
PATCH /products/:id/comments/:id comment.update() product 상세페이지의 댓글 수정
DELETE comment.delete() product 상세페이지의 댓글 삭제

 

   3) Article 모델에서 작업
       - Article 상세페이지에서 요청 작업 진행

요청 URL prisma method 기능
GET /articles/:id/comments .findUnique() article 상세페이지의 댓글 확인
POST comment.create()
article.update()
article 상세페이지의 댓글 신규 작성
(이미 있는 글에 댓글을 넣는 작업이므로 
생성 후 업데이트 작업)
PATCH /articles/:id/comments/:id comment.update() article 상세페이지의 댓글 수정
DELETE comment.delete() article 상세페이지의 댓글 삭제

 

 

2. 모델 별 CRUD 작업 - 작업 별 간략 특징

  • Comment 모델 : "/comments" - GET , "/comments/:id" - GET, PATCH, DELETE 요청 작성
    • GET : cursor 방식의 페이지 네이션 기능
                 → 끊김없이 데이터가 나열되는 페이징 기능으로, 최신 글이 맨 위로 오도록 셋팅
  • Product & Article 모델 : 기존 라우터에서 새로운 "/comments", "/comments/:id" 라우터를 추가 후 각각 요청 내용 작성
    • POST : Comment DB 에 데이터를 생성 한 뒤, 각각의 모델에 연결(update & connect)하는 방식 사용

 

1-2. 개별 모델 GET 요청 추가 작업 

추가 할 내용 

  1. 특정 필드의 검색 기능 추가  
    • Product Route : name / description 필드에서 검색 
    • Article Route : title / content 필드에서 검색
  2. 조회 시 보여 줄 항목 정리 
    • Product Response : id, name, price, createdAt 표기
    • Article Response : id, title, content, createdAt 표기
    • Comment Response : id, content, createdAt 표기

 

[기존 GET 요청 작업] - 대표 product model

productRoute
  .route('/')
  .get(async (req, res) => {
    const { offset = 0, limit = 10, order = 'newest' } = req.query;

    let orderBy;
    switch (order) {
      case 'oldest':
        orderBy = { createdAt: 'asc' };
        break;
      case 'newest':
        orderBy = { createdAt: 'desc' };
        break;
      default:
        orderBy = { createdAt: 'asc' };
    }

    const productList = await prisma.product.findMany({
      skip: parseInt(offset),
      take: parseInt(limit),
      orderBy,
    });
    res.status(200).send(productList);
  })

 

[ 수정 한 GET 요청 작업 ] - 대표 product model

productRoute
  .route('/')
  .get(async (req, res) => {
  
    // 1. 받아오는 데이터에 name / description 키워드를 추가 
    const {offset = 0, limit = 10, order = 'newest', name = '', description = ''} = req.query;
    
    let orderBy;
    switch (order) {
      case 'oldest':
        orderBy = { createdAt: 'asc' };
        break;
      case 'newest':
        orderBy = { createdAt: 'desc' };
        break;
      default:
        orderBy = { createdAt: 'asc' };
    }

    const productList = await prisma.product.findMany({
      
      // 2. 해당 데이터를 찾기 위하여 where 프로퍼티를 추가하고 각각 키워드 전달
      where: {
        name: { contains: name },
        description: { contains: description },
      },
      
      skip: parseInt(offset),
      take: parseInt(limit),
      orderBy,
      
      // 3. 최종으로 response 할 정보만 설정
      select: {
        id: true,
        name: true,
        price: true,
        createdAt: true,
      },
    });

    res.status(200).send(productList);
  })

 

 

 

+ 추가 관계형 모델 스키마 작성시 함께 사용하는 옵션 onDelete

model Comment {
  ...
  product   Product? @relation(fields: [productId], references: [id], onDelete: Cascade)
  ...
}

 

- 기본 내용 : 관계형 모델 속성인 @relation 안에 사용하는 옵션으로,
                    연결된 값이 삭제되는 경우 현재 모델의 데이터를 어떻게 할지 정의

1) Cascade :  연결 된 값이 삭제되면, 현재 모델의 데이터를 같이 삭제

                       예시 - 블로거 계정이 삭제되면 작성한 포스팅도 모두 사라짐

2 ) Restrict : 연결 된 값을 삭제하고 싶으면, 현재 모델의 데이터 먼저 삭제하고 연결 된 값을 삭제 가능 
                     예시 - 게시글에 댓글이 달린 상태에서는 게시글을 삭제할 수 없음 (댓글을 먼저 삭제해야 함)

3) SetNull : 연결 된 값이 삭제되면, 해당 값을 가리키던 필드가 Null로 변경, @relation 필드와 FK 필드 모두 옵셔널(?) 설정 필수
                    예시 - 쇼핑몰의 유저가 탈퇴해도, 주문 목록은 유지(주문자 명은 없음) 

4) SetDefault : 연결 된 값이 삭제되면, 해당 값을 가리키던 필드에 디폴트 값 넣어줌, FK에 @default("...") 셋팅 필요
                         예시 - 댓글을 남긴 작성자의 계정이 삭제되도 댓글은 남고 이름에 (탈퇴 한 유저) 라고 나오는 경우 

 

2. 유효성 검사 적용

1. 유효성 데이터 검사를 진행 할 필드 구분 

전체 모델에서 유효성 검사가 필요 한 값 정리
전체 모델에서 유효성 검사가 필요 한 값 정리

 

2. 유효성 검사용 파일에 필요한 셋팅 진행하기 ( 샘플 : Product Model )

[ productStruct.js ]

import * as structs from 'superstruct';

 

 

3. 파일 안에 유효성 검사를 진행하는 내용 작성하기

    Patch용 유효성 검사의 경우 Creat검사용에서 옵셔널하게 검사 가능하므로 partial() 메소드를 사용해 작성

[ productStruct.js ]

import * as structs from 'superstruct';

// POST용 유효성 검사
export const CreateProduct = s.object({
  name: s.size(s.string(), 1, 30),
  description: s.size(s.string(), 1, 500),
  price: s.min(s.number(), 0),
  tags: s.array(s.string()),
});

// PATCH용 유효성 검사
export const PatchProduct = structs.partial(createProduct);

  

 

4. 유효성 검사를 진행 할 파일 안에 Superstruct의 assert함수유효성 검사 이름을 import로 불러오고, 

     assert 함수를 사용하여 필요한 부분에서 검사 진행 

[ productRoute.js] 

...
import { assert } from 'superstruct';
import { CreateProduct, PatchProduct } from '../structs/productStructs';
...

// POST요청에 적용 한 내용
app.post('/products', async (req, res) => {
  assert(req.body, CreateProduct);
  ...

});

// PATCH요청에 적용 한 내용
app.patch('/products/:id', async (req, res) => {
  assert(req.body, PatchProduct);
  ...

});

 

 

5. 나머지 다른 모델에도 모두 적용

 

+ 추가  작업 시 참고했던 SuperStructJS 홈페이지 : https://docs.superstructjs.org/

 

Introduction | Superstruct

CopyIntroduction Superstruct makes it easy to define interfaces and then validate JavaScript data against them. Its type annotation API was inspired by TypeScript, Flow, Go, and GraphQL, giving it a familiar and easy to understand API. But Superstruct is d

docs.superstructjs.org

 


 

📃 내일은 뭘 배울까 🤔

- route 파일 2차 분기 ⇒ controll 파일로 작업 영역 이동

- 비동기 에러 래퍼 함수 적용

- 미들웨어 적용 작업

+ 가능하다면 "이미지(multer) 작업" 진행

반응형