안녕하세요

MongoDB 타입 설정 + DB에서 게시물 요청 (필드 제한하기) 본문

Next13 블로그 프로젝트

MongoDB 타입 설정 + DB에서 게시물 요청 (필드 제한하기)

sakuraop 2023. 9. 12. 18:17

목차

1. Next.js에서 MongoDB 연결하기

  • 설치
  • 연결
  • Type 세팅

2. DB collection에 데이터 삽입 및 출력

 

3. DB에서 요청한 데이터로 게시물 목록, 게시물 상세 구현

  • 요청할 데이터의 필드 제한하기
  • collection Type 에러 해결
  • Type extends
  • findOne Type 에러 해결

1. MongoDB와 타입스크립트를 쓰기 위해 필요한 설정을 해봅시다.

MongoDB를 설치합니다.

npm install mongodb

타입스크립트로 진행하기 위해서는 mongodb에도 타입을 지정해주어야 합니다.

 

MongoDB 에 연결하기 위해서는 다음과 같이 작성하면 됩니다.

// src/utils/db/db.ts

import { MongoClient } from "mongodb";

const url: string | undefined = process.env.DB_CONNECT;

// 환경변수 설정하지 않으면 에러 발생
if (!url) {
  throw new Error("The MONGODB_URL environment variable is not defined");
}

let connectDB: Promise<MongoClient>;

// 개발 단계에서는 global 변수에 저장하여 connect를 저장할 때마다 반복하지 않도록 함
if (process.env.NODE_ENV === "development") {
  if (!global._mongo) {
    global._mongo = new MongoClient(url).connect();
  }
  connectDB = global._mongo;
} else {
  connectDB = new MongoClient(url).connect();
}

export { connectDB };

여기서 필요한 타입은 MongoClientglobal._mongo입니다.

 

/src/types 디렉토리에 global 타입을 모아둘 파일을 생성합니다.

// src/types/global.d.ts

export {};

declare global {
  var _mongo: Promise<MongoClient> | undefined;
}

global.d.ts
* 파일명에서 .d.ts는 타입 선언 파일로 취급한다는 의미입니다.

  해당 파일에는 타입 정의만이 들어갈 수 있게 됩니다.

 

export {};

* 이 부분은 파일을 모듈 처리하도록 하여 격리된 스코프를 가지도록 합니다.

 

 

declare global {
  var _mongo: Promise | undefined;
}

* 전역 속성인 global._mongo 의 타입을 글로벌 스코프에서 지정해줍니다. 

루트 디렉토리의 tsconfig.json 파일에 다음 속성을 추가해줍니다.

{
  ...
  // TypeScript가 타입 정의 파일을 검색할 위치
  "typeRoots": ["./src/types", "./node_modules/@types"],
  
  //  TypeScript 컴파일 대상 설정
  "include": [..., ".next/types/**/*.ts", "src/**/*", "global.d.ts"],
}

typeRoots

타입을 검색할 경로를 지정합니다.

 

include

TypeScript 컴파일러가 컴파일할 JavaScript 파일 범위를 경로로 지정합니다.

 

똑같이 설정을 했다면 타입 경고가 해결되어야 합니다.


2. connectDB를 import하여 collection의 도큐먼트를 출력해봅시다.

import { connectDB } from "@/utils/db/db";

const Home = async () => {
  const db = (await connectDB).db("blog");
  let result = await db.collection("post").find().toArray();
  console.log(result);
  return <div>메인</div>;
};

export default Home;

db.ts에서 export한 connectDB에는 DB와 연결된 MongoClient 객체가 할당되어 있습니다.

 

blog DB의 post collection에 있는 documents를 출력할 것입니다.

 

const Home = async () => {

db에 연결하는 작업은 비동기로 실행되어야 합니다.

 

  const db = (await connectDB).db("blog");

비동기로 connectDB를 호출하고, blog DB에  연결시켜 줍니다.

 

  let result = await db.collection("post").find().toArray();

post collection의 도큐먼트들을 불러옵니다.

 

다음과 같이 임시 데이터를 post collection에 삽입하고 출력해보겠습니다.

 

🔻삽입할 데이터, insert 방법 펼쳐보려면🔻

더보기
  const cardData = [
    {
      id: 1,
      title: "제목제목제목제목제목제목제목제목제목제목제목제목",
      subtitles: ["#페이지네이션", "#레이지로딩", "#예약", "#페이지네이션", "#레이지로딩", "#예약"],
      languages: ["#JavaScript", "#Rust", "#Go", "#JavaScript", "#Rust", "#Go"],
      content: "ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ",
      commentCount: 10,
      author: "young",
      date: new Date(),
      likes: 7,
    },
    {
      id: 2,
      title: "제목",
      subtitles: ["#페이지네이션", "#예약"],
      languages: ["#JavaScript", "#Rust", "#Go"],
      content: "ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ",
      commentCount: 10,
      author: "young",
      date: new Date(),
      likes: 7,
    },
    {
      id: 3,
      title: "제목",
      subtitles: ["#레이지로딩", "#예약"],
      languages: ["#JavaScript", "#Rust", "#Go"],
      content: "ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ",
      commentCount: 10,
      author: "young",
      date: new Date(),
      likes: 7,
    },
    {
      id: 4,
      title: "제목",
      subtitles: ["#예약"],
      languages: ["#JavaScript", "#Rust", "#Go"],
      content: "ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ",
      commentCount: 10,
      author: "young",
      date: new Date(),
      likes: 7,
    },
    {
      id: 5,
      title: "제목",
      subtitles: ["#드래그앤드롭"],
      languages: ["#JavaScript", "#Rust", "#Go"],
      content: "ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ",
      commentCount: 10,
      author: "young",
      date: new Date(),
      likes: 7,
    },
  ];

 

    const result = await postCollection.insertMany(cardData);

 

올바르게 삽입이 되었다면 출력을 해봅시다.

  const result = await postCollection.find().toArray();

  console.log(result);

 

🔻출력 결과 펼쳐보려면🔻

더보기

[
  {
    _id: new ObjectId("65002c07a382eb0e8de06ae1"),
    id: 1,
    src: 'https://cdn.pixabay.com/photo/2023/08/29/19/09/starling-8221990_640.jpg',
    title: '제목제목제목제목제목제목제목제목제목제목제목제목',
    subtitles: [ '#페이지네이션', '#레이지로딩', '#예약', '#페이지네이션', '#레이지로딩', '#예약' ],
    languages: [ '#JavaScript', '#Rust', '#Go', '#JavaScript', '#Rust', '#Go' ],
    content: 'ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ',
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  },
  {
    _id: new ObjectId("65002c07a382eb0e8de06ae2"),
    id: 2,
    src: 'https://cdn.pixabay.com/photo/2023/08/26/18/01/planet-8215532_640.png',
    title: '제목',
    subtitles: [ '#페이지네이션', '#예약' ],
    languages: [ '#JavaScript', '#Rust', '#Go' ],
    content: 'ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ',
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  },
  {
    _id: new ObjectId("65002c07a382eb0e8de06ae3"),
    id: 3,
    src: 'https://cdn.pixabay.com/photo/2023/09/04/13/17/mushrooms-8232731_1280.jpg',
    title: '제목',
    subtitles: [ '#레이지로딩', '#예약' ],
    languages: [ '#JavaScript', '#Rust', '#Go' ],
    content: 'ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ',
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  },
  {
    _id: new ObjectId("65002c07a382eb0e8de06ae4"),
    id: 4,
    src: 'https://cdn.pixabay.com/photo/2023/05/14/17/46/ducklings-7993465_1280.jpg',
    title: '제목',
    subtitles: [ '#예약' ],
    languages: [ '#JavaScript', '#Rust', '#Go' ],
    content: 'ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ',
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  },
  {
    _id: new ObjectId("65002c07a382eb0e8de06ae5"),
    id: 5,
    src: 'https://cdn.pixabay.com/photo/2023/08/28/23/17/superb-fairywren-8220199_640.jpg',
    title: '제목',
    subtitles: [ '#드래그앤드롭' ],
    languages: [ '#JavaScript', '#Rust', '#Go' ],
    content: 'ㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ\nㅇㅇㅇㅇㅇㅇㅇ',
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  }
]


요청한 데이터로 post cards를 구현해봅시다.

 

이제 서버에서 불러온 데이터를 통해서 게시물을 출력할 수 있게 되었습니다.

 

* type에러가 발생할 것입니다. 

collection을 지정할 때 type을 collection에 함께 지정하면 schema처럼 특정 타입만을 포함한 게시물인지 확인을 할 수 있게 됩니다.

import { Post } from "@/types/post";

const Category = async () => {
  const db = (await connectDB).db("blog");
  let postCollection = db.collection<Post>("post");
  const postData: Post[] = await postCollection.find().toArray();

  return (
  ...

  let postCollection = db.collection<Post>("post");
post 콜렉션의 파일은 Post 타입이어야만 합니다.

 

  const postData: Post[] = await postCollection.find().toArray();
postData에는 find().toArray() 메서드를 통해 불러온 데이터는 배열이 될 것이기 때문에 Post[] 타입이 되어야 합니다.


필드 제한하기

card에는 content가 포함되지 않습니다. 하지만 content에는 많은 데이터가 포함됩니다.

따라서 card에서는 content를 제외하고 데이터를 요청하도록 하여 DB와 클라이언트의 메모리를 절약할 수 있도록 합시다.

  // _id 필드는 노출되지 않는 것이 좋습니다. content 필드는 제외하고 도큐먼트를 불러옴으로써 card에 불필요한 데이터는 제외합니다.
  const postData: Post[] = await postCollection.find({}, { projection: { content: 0, _id: 0 } }).toArray();

  find({}, { projection: { content: 0, _id: 0 } })

find({}) 메서드는 모든 데이터를 가져올 때 이용되는데,

두번째 속성에 projection 을 이용하여 제외할 필드를 선택할 수 있습니다. content: 0 과 같이 작성합니다.

(_id 는 MongoDB에서 도큐먼트 고유의 id를 자동으로 할당한 것이며, 이는 클라이언트에게 불필요한 정보입니다.

민감한 정보를 포함한 ObjectID의 경우에는 특히나 노출되지 않도록 주의하여야 합니다.)

 

이렇게 필드를 제한하고 출력을 해보면 _id와 content 필드가 제외된 데이터를 가져온 것을 볼 수 있습니다.

  {
    id: 5,
    src: 'https://cdn.pixabay.com/photo/2023/08/28/23/17/superb-fairywren-8220199_640.jpg',
    title: '제목',
    subtitles: [ '#드래그앤드롭' ],
    languages: [ '#JavaScript', '#Rust', '#Go' ],
    commentCount: 10,
    author: 'young',
    date: 2023-09-12T09:14:47.969Z,
    likes: 7
  }

 

Card 타입과 Post 타입을 구분하면 좋겠죠

export interface Card {
  id: number;
  src: string;
  title: string;
  subtitles: string[];
  languages: string[];
  author: string;
  date: Date;
  commentCount: number;
  likes: number;
}

export interface Post extends Card {
  content: string;
}

Card 타입에는 content가 포함되어 있지 않고,

Post 타입은 Card 타입을 extends하여 content를 갖도록 합니다.


요청한 데이터로 post detail 페이지를 구현해봅시다.

const Post = async ({ postId }: any) => {
  const db = (await connectDB).db("blog");
  const postCollection = db.collection<Post>("post");
  const postData: Post | null = await postCollection.findOne(
    { id: Number(postId) },
    { projection: { _id: 0 } }
  );
  
    return (
    ...

* findOne 메서드를 이용하면 "withid..." 어쩌고 하는 type에러가 발생할 것입니다. 

  const postData: Post | null = await postCollection.findOne(
위와 같이 타입에 null 타입을 함께 지정해주어야 합니다.

존재하지 않는 id일 수 있기 때문입니다.