반응형

아티클의 제목과 같은 에러가 발생했는데, 전문은 아래와 같다.

 

 

Logger로 기록한 error

[Nest] 64896  - 2022. 10. 13. 오후 8:00:56   ERROR [ExceptionHandler] Nest can't resolve dependencies of the ScriptService (FirstRepository, SecondRepository, ThirdRepository, ?). Please make sure that the argument SentenceRepository at index [3] is available in the ScriptModule context.

Potential solutions:
- If SentenceRepository is a provider, is it part of the current ScriptModule?
- If SentenceRepository is exported from a separate @Module, is that module imported within ScriptModule?
  @Module({
    imports: [ /* the Module containing SentenceRepository */ ]
  })

 

 

에러 내용 해석

Nest가 ScriptService에 ?에 해당하는 것의 의존성 주입을 할 수 없다는 뜻이다.

나는 Potential solutions 중 첫번째에 해당하는 간단한 오류였다.

ScriptModule에 SentenceRepository를 import하지 않고, ScriptService의 생성자에 추가했기 때문에 발생한 오류다.

 

 

 

해결 방법

// script.module.ts

@Module({
  imports: [
    TypeOrmModule.forFeature([
      ScriptRepository, ScriptExampleRepository, UserRepository, NewsRepository
    ]),
  ],
  controllers: [ScriptController],
  providers: [ScriptService, JwtModule]
})
export class ScriptModule {}

위와 같이 module에, service에서 사용한 repository를 import함으로써

의존성 주입에서 발생한 error를 수정할 수 있었다.

 

 

 

 

반응형
반응형

User와 News 사이에 즐겨찾기(Favorite)기능을 구현하고자 한다.

ManyToMany 관계를 설정하고

특정 API에 user와 news 정보를 보내면 관계가 추가되는 기능을 구현해야 한다.

 

1. 관계를 설정하고 (기본, lazy, eager 설명)

2. 기능을 구현하기까지 과정을 정리해보겠다. (lazy 관계 사용)

 

 

 

ManyToMany 관계 설정

관계는 아래와 같이 설정했다.

// user.entity.ts
  @ManyToMany(() => News, (news) => news.favorites)
  @JoinTable()
  favorites: Promise<News[]>;
// news.entity.ts
  @ManyToMany(() => User, (user) => user.favorites)
  favorites: User[]

여기서 드는 의문은,

favorites의 타입이 왜 Promise 형태인지이다. (이는 lazy 설정을 사용하기 위함이다!)

 

TypeORM에서 ManyToMany를 구현하는 형식은 세 가지가 있는데,

1. 기본 설정

2. lazy 관계

3. eager 관계 이다.

 

아래에서 각각의 설정에 대해 설명해보고, 필자가 왜 lazy 관계를 사용했는지도 설명할 것이다.

 

 

 

1. 기본 설정

기본 설정은 아래와 같이 관계를 설정한다.

// user.entity.ts
  @ManyToMany(() => News, (news) => news.favorites)
  @JoinTable()
  favorites: News[];
// news.entity.ts
  @ManyToMany(() => User, (user) => user.favorites)
  favorites: User[]

위와 같은 관계에서,

User를 초기 생성할 때 favorites을 선언하는 케이스는 예시가 많이 나온다.

 

const category1 = new Category()
category1.name = "animals"
await dataSource.manager.save(category1)

const category2 = new Category()
category2.name = "zoo"
await dataSource.manager.save(category2)

const question = new Question()
question.title = "dogs"
question.text = "who let the dogs out?"
question.categories = [category1, category2]
await dataSource.manager.save(question)

위의 예시는 TypeORM docs에서 발췌한 코드인데,

역시 선언하고 저장하는 형식이다.

 

그런데.. 이미 저장된 favorites을 불러와 정보를 추가하는 로직은

도저히 예시를 찾을 수가 없는 것이다.

그러다 발견한 것이 아래 방식이다!

 

 

 

 

2. eager 관계

// user.entity.ts
  @ManyToMany(() => News, (news) => news.favorites, {
    eager: true,
  })
  @JoinTable()
  favorites: News[];
// news.entity.ts
  @ManyToMany(() => User, (user) => user.favorites)
  favorites: User[]

위와 같이 관계를 설정하면,

어느 한 쪽 정보를 조회 시, favorites에 리스트로 저장된 반대편 정보도 함께 조회된다.

 

하지만, 필요하지 않은 경우에도 (많을지도 모르는 양의) 반대편 정보를 조회해야 한다는 단점이 있다.

 

 

 

 

 

3. lazy 관계

// user.entity.ts
  @ManyToMany(() => News, (news) => news.favorites)
  @JoinTable()
  favorites: Promise<News[]>;
// news.entity.ts
  @ManyToMany(() => User, (user) => user.favorites)
  favorites: User[]

 

lazy 관계를 맺으면, 평소에는 서로의 정보를 저장하지 않고

참조 시에만 불러오는 형식을 취한다.

 

즉, User의 입장에서

favorites을 조회하지 않는(News를 참조하지 않는) 경우에는 favorites에 정보가 저장되지 않고,

favorites을 조회하는(News를 참조하는) 경우에는 favorites에 정보가 저장된다.

 

이 정보는 user_favorite_news라고 하는 새로 생성된 Join 테이블에서 가져 온다.

 

lazy 관계를 사용하는 방식은, 위에서처럼 관계를 Promise로 정의하면 된다.

꺼내서 사용하는 경우에도 Promise로 다뤄줘야 하는데,

이는 아래에서 기능을 구현할 때 다시 언급하겠다.

 

 

 

 

기능 구현!

위에서 언급한 대로,

User가 가지고 있는 favorites(News의 리스트)에 원하는 News를 넣는 기능을 구현해야 한다.

그래서 아래와 같이 구현했다!

 

// user.repository.ts
    async addFavoriteNews(user: User, news: News): Promise<User> {
      const favorites = await user.favorites;
      favorites.push(news);
      user.save();
      return user;
    }
    
    async deleteFavoriteNews(user: User, news: News): Promise<User> {
      let favorites = await user.favorites;
      const newsId: number = news.id;

      user.favorites = Promise.resolve(
        favorites.filter((news) => {
          return news.id !== newsId;
        }),
      );

      user.save();
      return user;

    }

user.favorites은 Promise 객체이므로 await으로 불러와야 함에 주의해야 한다.

또한, 위와 같이 수정 시에도 Promise.resolve 형태로 사용해야 한다!

 

그 점만 주의한다면,

- 참조 시에만 데이터를 불러와 경제적이고,

- API 호출 시마다 관계를 추가할 수 있는

좋은 기능을 구현할 수 있었다!

 

위에서 return한 user를 response하면 아래와 같다!

{
    "status": 200,
    "success": true,
    "message": "좋아하는 뉴스 토글 성공",
    "data": {
        "socialId": "123",
        "nickname": "test",
        "__favorites__": [
            {
                "title": "Test title3",
                "category": "사회",
                "id": 3
            },
            {
                "title": "Test title4",
                "category": "연예",
                "id": 4
            }
        ],
        "__has_favorites__": true
    }
}

(status, success, message는 임의로 정한 규격이니 무시해도 좋다.)

__favorites__, __has_favorites__라는 속성으로 표현된다!

 

뭐니뭐니해도 공식문서가 짱이다.

 

 

 

 

참고 자료 :

https://orkhan.gitbook.io/typeorm/docs/eager-and-lazy-relations

 

Eager and Lazy Relations - typeorm

Note: if you came from other languages (Java, PHP, etc.) and are used to use lazy relations everywhere - be careful. Those languages aren't asynchronous and lazy loading is achieved different way, that's why you don't work with promises there. In JavaScrip

orkhan.gitbook.io

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형
반응형
 

[무료] 따라하며 배우는 NestJS - 인프런 | 강의

이 강의를 통해 NestJS에 대해서 배울 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

 

 

 

개요

인프런에서 제공하는 Nest.js 무료 강의.
다른 인프런 무료강의들처럼 유튜브로 배포된 영상을 강의형태로 모았다.

쉽고 재밌습니다.

 

 

 

 

 

 

강의 후기

 

 

🔎 학습 내용

- Nest.js 기본 요소 설명(MVC 패턴에 대한 간략한 설명도)
- 간단한 CRUD 구현 (DB 연결 X)
- Pipe 이용(유효성 체크 등을 위한 미들웨어)
- DB 연동 후 CRUD 수정(Postgresql & TypeORM)
- JWT를 활용한 인증 로직 구현
- 게시물 접근 권한 로직 구현
- 그 외 (Log, Configuration)

 

 

 

 

 

 

🔎 학습 시간

- 강의시간 : 약 10시간
- 공부시간 : 약 20시간

 

 

🔎 수강 조건

- 웹 프레임워크(Express면 더 좋고)에 대한 기본적인 이해

이해가 없어도 들을 수 있는 난이도이기는 하지만, 기초적인 것을 다 설명해주시지는 않기 때문에 기본적인 이해는 필요할 것 같다.

 

 

 

🔎 수강 대상

- Nest.js 기초를 빠르게 잡고 싶은 사람
- Express 개발자 (Nest 함 써보세요 편합니다 ^^)

 

 

 

 


주관적 평가

 

 

👍 좋았던 점

1) 적당한 난이도
어렵지는 않지만 직접 프로젝트를 만들 수 있을 정도의 예제로 구성되어 있어 좋았다! 강의 수강 후 바로 개발 시작하면 될듯.

 

2) 깔끔한 구성

유튜브에 배포된 강의가 맞나 싶을 정도로 강의 구성이 깔끔했다.

 

3) 왜케 재밌지?

너무 재밌어가지고 밥 먹을 때도 자꾸 다음 강의 생각나고 그래서 하루 만에 다 들었다.

 

 

 

 

 

👎 나빴던 점

버전 문제가 좀 있는데요, 강사님 깃에서 package.json, package.lock.json 그대로 복사해오면 해결되더라구요..

 

 

 


프로젝트 기술 스택을 Express에서 Nest.js로 옮기기 위해 급하게 들었던 강의.
아티클이랑 깃허브 리파지토리 뒤져서 그냥 시작할까하다가, 강의 한 번 들어보고 싶어서 들었는데
너무 잘한 선택이었다!

 

후기 끝.

 

 

 

 

 

 

 

반응형
반응형

 mongoose 내장 함수인 findByID, findByIdAndUpdate, findByIdAndDelete에 대해 알아 보자.

 

(Collection은 가칭)

Collection.findByID(id) : Collection에서 _id가 id인 document를 찾는다.

Collection.findByIdAndUpdate(id, data) : Collection에서 _id가 id인 document를 찾아서 data 내용에 따라 수정한다.

Collection.findByIdAndDelete(id) : Collection에서 _id가 id인 document를 찾아서 삭제한다.

 

코드를 통해 살펴보자.

const findUserById = async (userId: string): Promise<UserResponseDto | null> => {
    try {
        const user = await User.findById(userId);
        if (!user) {
            return null;
        }

        return user;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

위 코드는 어떤 프로젝트의 Service 코드이다.

findById(나머지 둘도 마찬가지)는 document를 불러와 저장할 수 있다.

 

이때 유의할 점은, user가 존재하지 않을 수 있기 때문에(존재하지 않는 userId)

null을 반환할 수 있도록 반환 타입 명시와 반환값 설정을 위와 같이 해줘야 한다.

 

const updateMovie = async (movieId: string, movieUpdateDto: MovieUpdateDto): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndUpdate(movieId, movieUpdateDto);
        if (!movie) {
            return null;
        }

        return movie;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

이번에는 findByIdAndUpdate를 사용했다.

유의할 점은, find And Update 순서에서 눈치챌 수 있듯이,

find가 먼저 적용되기 때문에,

movie에는 update 전 document 정보가 저장된다.

 

따라서, update 후 document 정보를 저장하고 싶다면 아래와 같이 코드를 수정해야 한다.

const updateMovie = async (movieId: string, movieUpdateDto: MovieUpdateDto): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndUpdate(movieId, movieUpdateDto);
        if (!movie) {
            return null;
        }
        const data = await Movie.findById(movieId);
        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

(더 좋은 방법이 있을텐데.. 아시는 분 있으시다면 댓글로 조언 부탁드립니다. 감사합니다.)

 

 

마지막으로 findByIdAndDelete를 사용해보자.

const deleteMovie = async (movieId: string): Promise<MovieResponseDto | null> => {
    try {
        const movie = await Movie.findByIdAndDelete(movieId);
        if (!movie) {
            return null;
        }

        return movie;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

이때도 movie에는 삭제 전 정보가 저장된다.

내가 삭제한 정보에 대해서 확인하고 document를 보내주기 위해(?) 위와 같은 로직을 작성하였다.

 

 

 

이렇게 mongoose 내장 함수인 findById, findByIdAndUpdate, findByIdAndDelete를 통해

update, delete, get 메서드로 정보를 수정, 삭제, 조회하는 코드를 구현하는 방법을 공부해보았다.

 

 

 

반응형
반응형

NoSQL인 MongoDB는 Join의 개념이 없다!

하지만 분명 값을 참조해올 필요는 존재할 것이다.

 

본 포스팅에서는,

'User', 'Movie' 컬렉션과 'Review' 컬렉션이 존재할 때,

 

'Review' 컬렉션에 'movie'라는 field를 만들어 'Movie'의 값들을 전부 참조해오는 것과

'Review' 컬렉션에 'writer'라는 field를 만들어 'User'의 필드 중 'name'만 참조해오는 것을 구현해볼 것이다.

 


Collection 만들기

// Movie.ts
import mongoose from "mongoose";
import { MovieInfo } from "../interfaces/movie/MovieInfo";

const MovieSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true,
    },
    director: {
        type: String,
        required: true,
    },
    startDate: {
        type: Date,
    },
    thumbnail: {
        type: String,
    },
    story: {
        type: String,
    },
});

export default mongoose.model<MovieInfo & mongoose.Document>("Movie", MovieSchema);

Movie 컬렉션은 별다른 참조 필드 없이 작성했다.

 

import mongoose from "mongoose";
import { ReviewInfo } from "../interfaces/review/ReviewInfo";

const ReviewSchema = new mongoose.Schema({
    writer: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "User",
    },
    movie: {
        type: mongoose.Types.ObjectId,
        required: true,
        ref: "Movie",
    },
    title: {
        type: String,
        required: true,
    },
    content: {
        type: String,
        required: true,
    }
});

export default mongoose.model<ReviewInfo & mongoose.Document>("Review", ReviewSchema);

Review 컬렉션은

writer 필드에서 User의 id를,

movie 필드에서 Movie의 id를 참조했다.

 

User는 따로 적진 않겠지만, 'name'이라는 필드를 가지고 있다.

 

컬렉션 생성에 필요한 interface 들은 아래와 같다.

// ReviewInfo.ts
import mongoose from "mongoose";

export interface ReviewInfo {
    writer: mongoose.Types.ObjectId;
    movie: mongoose.Types.ObjectId;
    title: string;
    content: string;
}

// MovieInfo.ts
export interface MovieInfo {
    title: string;
    director: string;
    startDate: Date;
    thumbnail: string;
    story: string;
}

 

 

Service에서 참조 로직 작성하기

Controller 등 다른 것들은 참조 여부와 큰 관련이 없다.

Service 로직에서 참조를 어떻게 구현하는지 살펴보자.

 

첫번째로 'updateReview'를 만들 것이다.

도입부에서 언급한대로, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const updateReview = async (reviewId: string, reviewUpdateDto: ReviewUpdateDto): Promise<ReviewResponseDto | null> => {
    
    try {
        const review = await Review.findByIdAndUpdate(reviewId, reviewUpdateDto)
        .populate('writer', 'name').populate('movie');
        if (!review) {
            return null;
        }
        const data: any = {
            writer: review.writer,
            movie: review.movie,
            title: review.title,
            content: review.content
        }
        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

.populate() 메서드를 통해 참조를 구현할 수 있다.

위에서 볼 수 있듯이,

특정 필드(name)만 참조해오려면 -> .populate('필드명', '참조해올 필드명')

전체를 참조해오려면 -> .populate('필드명')

과 같이 작성한다.

 

또한 받아온 정보를 사용하기 전에 가공 과정이 필요하다.

이때, TypeScript를 사용하고 있다면,

data에 any 타입을 지정해주는 것에 유의해야 한다.

 

참고로 반환 타입인 ReviewResponseDto는 아래 코드와 같다.

import { MovieInfo } from "../movie/MovieInfo";

export interface ReviewResponseDto {
    writer: string;
    title: string;
    content: string;
    movie: MovieInfo;
}

movie에 Moive 전체 필드를 가져올 것이므로, 타입을 MovieInfo로 지정해주었다.

 

 

두번째로는 'getReviews'를 만들어 보자.

'movie'가 특정 id값과 일치하는 모든 Review documents를 불러오는 로직이다.

 

이 또한, writer(User의 id 참조)를 통해 User의 name을 참조해오고,

movie(Movie의 id 참조)를 통해 Movie의 전체 필드를 가져올 것이다.

const getReviews = async (movieId: string): Promise<ReviewResponseDto[] | null> => {
    try {
         const reviews = await Review.find({
             movie: movieId
         }).populate('writer', 'name').populate('movie');
         if (reviews.length === 0) {
            return null;
        }

         const data = await Promise.all(reviews.map((review: any) => {           
             const result = {
                writer: review.writer.name,
                movie: review.movie,
                title: review.title,
                content: review.content
             };

             return result;
         }));

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

참조해온 방식은 위의 ' updateReview'와 동일하다.

 

여기서는 mongoose 내장 함수인 find를 활용해

Movie 컬렉션에서 movie가 받아온 movieId와 같은 모든 document의 정보를 가져오고

각각을 populate로 참조해서 적용한다.

 

한 가지 유의할 다른 점은, 참조해온 데이터 가공 방식이다.

Promise.all()과 map을 활용하는데,

map이 여러개의 Promise를 반환하기 때문에 await Promise.all()을 활용해

await 병렬 구조로 실행한다.

 

Promise.all()을 쓰지 않아도 동일한 결과를 나타내지만(적어도 저 코드에서는 그렇더라는..),

성능을 위해 Promise.all()을 사용하는 것 같다.

 

 

아래는 위에서 작성한 로직(getReviews)의 Response 예시이다.

{
    "status": 200,
    "success": true,
    "message": "리뷰 조회 성공",
    "data": [
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "첫번째 리뷰",
            "content": "재밌는 영화였습니다."
        },
        {
            "writer": "test1",
            "movie": {
                "_id": "627b4f23f22c32b2fcddc951",
                "title": "닥터 스트레인지",
                "director": "이우진",
                "startDate": "2022-05-01T15:00:00.000Z",
                "thumbnail": "https://example-sopt.s3.ap-northeast-2.amazonaws.com/%E1%84%80%E1%85%A7%E1%86%BC%E1%84%87%E1%85%A9%E1%86%A8%E1%84%80%E1%85%AE%E1%86%BC.jpeg",
                "story": "재밌는 영화입니다."
            },
            "title": "두번째 리뷰",
            "content": "재미 하나도 없는 영화였습니다."
        }
    ]
}

'movie'가 '627b4f23f22c32b2fcddc951' 인 모든 Review documents를 불러오고,

'movie'는 Movie document의 모든 필드를,

'writer'는 User document의 'name' 필드를 가져온다.

 

실제 Review에는 아래와 같이 저장되어 있다.

(tmi : __v 는 버전을 나타내는 versionKey 필드인데, mongoose를 통해 데이터를 집어넣으면 생긴다고 한다. 궁금해서 방금 쳐봤다.)

 

 

이렇게 MongoDB에서 데이터를 참조하는 것을 구현해보았다.

 

 

 

 

 

 

 

반응형
반응형

express-validator는 유효성 검사를 도와주는 npm 라이브러리이다.

 

본 포스팅을 통해 아래와 같은 유효성 검사 로직을 적용해보자.

비어있지 않음 - notEmpty()

email 형식 - isEmail()

date 형식 - isDate()

error message 변경 - withMessage()

 

또한 본 포스팅에서는

router에서 validation 로직을,

controller에서 error handling 로직을 작성할 것이다.

 

 

 


 

express-validator 설치

yarn add express-validator

// 또는
npm install express-validator

 

 

router 수정 (validation 부분)

// 적용 전
import { Router } from "express";
import { UserController } from "../controllers";

const router: Router = Router();
router.post('/', UserController.createUser);

// 적용 후
import { Router } from "express";
import { body } from "express-validator/check";
import { UserController } from "../controllers";

const router: Router = Router();
router.post('/', [
    body("name")
        .notEmpty().withMessage("name이 비어있습니다."),
    body("phone")
        .notEmpty().withMessage("phone이 비어있습니다."),
    body("email")
        .notEmpty().withMessage("email이 비어있습니다.")
        .isEmail().withMessage("email 형식이 올바르지 않습니다."),
], UserController.createUser);

notEmpty() : 비어있으면 error 발생

 

메서드명이 대부분 직관적으로 이해된다.

withMessage는 바로 앞 메서드에 대해 error message를 변경해준다.

(default value는 'Invalid value'로 심플해서 웬만하면 수정하는 것이 좋다.)

 

 

// 적용 전
import express, { Router } from "express";
import { body } from "express-validator/check";
import MovieController from "../controllers/MovieController";

const router: express.Router = Router();
router.post('/', MovieController.createMovie);

export default router;



// 적용 후
import express, { Router } from "express";
import { body } from "express-validator/check";
import MovieController from "../controllers/MovieController";

const router: express.Router = Router();
router.post('/', [
    body("title")
        .notEmpty().withMessage('title이 비어있습니다.'),
    body("director")
        .notEmpty().withMessage('director가 비어있습니다.'),
    body("startDate")
        .isDate().withMessage('startDate의 형식이 올바르지 않습니다.'),
] ,MovieController.createMovie);

export default router;

isDate() : date 형식이 아니면 error 발생

 

 

이번에는 controller에서 error를 handling 해보자.

 

 

controller 수정 (error handling 로직)

// 적용 전
const createMovie = async (req: Request, res: Response): Promise<void | Response> => {
    const movieCreateDto: MovieCreateDto = req.body;
    try {
        const data = await MovieService.createMovie(movieCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_MOVIE_SUCCESS, data));
    } catch (error) {
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

// 적용 후
// ...
import { validationResult } from "express-validator";
// ...

const createMovie = async (req: Request, res: Response): Promise<void | Response> => {
    const error = validationResult(req);	// validation 결과 받아오기
    if (!error.isEmpty()) {
        console.log(error);
        return res.status(statusCode.BAD_REQUEST).send(util.fail(statusCode.BAD_REQUEST, error.array()[0].msg));
    }

    const movieCreateDto: MovieCreateDto = req.body;
    try {
        const data = await MovieService.createMovie(movieCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_MOVIE_SUCCESS, data));
    } catch (error) {
        console.log(error);
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

위와 같은 로직으로 error를 받아와 그 여부를 확인(!error.isEmpty())하고  (-> error가 있으면 isEmpty() 결과가 false가 나온다.)

'error.array()[0].msg' 로 그 msg를 받아온다.

 

참고로 error에 대한 여러 정보가 error.array()에 담겨 있는데, 아래와 같다.

 

// console.log(error) 결과
Result {
  formatter: [Function: formatter],
  errors: [
    {
      value: undefined,
      msg: 'director가 비어있습니다.',
      param: 'director',
      location: 'body'
    }
  ]
}

// console.log(error.array()) 결과
[
  {
    value: undefined,
    msg: 'director가 비어있습니다.',
    param: 'director',
    location: 'body'
  }
]

저 배열 값을 활용하려고 이것 저것 써봤는데..

답은 error.array() 였다!

 

 

그 외에도 많은 기능과 용법을 공식 문서에서 확인할 수 있다!

https://express-validator.github.io/docs/

 

Getting Started · express-validator

express-validator is a set of [express.js](http://expressjs.com/) middlewares that wraps

express-validator.github.io

 

 

읽어주셔서 감사합니다.

 

 

 

반응형
반응형

node와 express를 활용한 프로젝트를 시작하고

간단한 CRUD api를 작성해보는 포스팅을 적어보려 한다.

 

프로젝트를 시작할 때 참고하면 좋을 것 같다.

 

DB 생성과 초기 세팅은 이전 포스팅 참고.

 

MongoDB Cluster 생성하기 (링크🔗)

Express 초기 세팅, yarn 사용법 (링크🔗)

 

 

 


 

.env 만들기

루트 디렉토리에 '.env' 파일을 만든다 (* 반드시 .gitignore에 추가한다.)

 

 

// .env
PORT=8080
MONGODB_URI=mongodb+srv://id:password@clusterName.dgufc.mongodb.net/어쩌구저쩌구?retryWrites=true&w=majority

위와 같이 입력하는데, 'MONGOB_URI'는 MongoDB 홈페이지에서 가져온다

 

홈페이지에서 로그인하면 아래 화면이 바로 나온다.

 

 

 

 

기타 파일 생성 및 수정

1. 'config/index.ts' 생성

import dotenv from "dotenv";

// Set the NODE_ENV to 'development' by default
process.env.NODE_ENV = process.env.NODE_ENV || "development";

const envFound = dotenv.config();
if (envFound.error) {
  // This error should crash whole process

  throw new Error("⚠️  Couldn't find .env file  ⚠️");
}

export default {
  /**
   * Your favorite port
   */
  port: parseInt(process.env.PORT as string, 10) as number,

  /**
   * MongoDB URI
   */
  mongoURI: process.env.MONGODB_URI as string,
};

 

2. 'loaders/db.ts' 생성

import mongoose from "mongoose";
import config from "../config";

const connectDB = async () => {
    try {
        await mongoose.connect(config.mongoURI);

        mongoose.set('autoCreate', true);

        console.log("Mongoose Connected ...");
    } catch (err: any) {
        console.error(err.message);
        process.exit(1);
    }
};

export default connectDB;

 

3. 'src/index.ts' 수정

import express, { Request, Response, NextFunction } from "express";
import config from "./config";
const app = express();
import connectDB from "./loaders/db";
import routes from './routes';
require('dotenv').config();

connectDB(); // DB 연결하기

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

app.use(routes);   //라우터 분리
// error handler

interface ErrorType {
  message: string;
  status: number;
}

// 모든 에러에 대한 핸들링
app.use(function (err: ErrorType, req: Request, res: Response, next: NextFunction) {

  res.locals.message = err.message;
  res.locals.error = req.app.get("env") === "production" ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render("error");
});

app
  .listen(config.port, () => {
    console.log(`
    ################################################
          🛡️  Server listening on port 🛡️
    ################################################
  `);
  })
  .on("error", (err) => {
    console.error(err);
    process.exit(1);
  });

 

 

4. modules 폴더 만들기

// modules/util.ts -> success, fail 메세지 가공
const util = {
    success: (status: number, message: string, data?: any) => {
        return {
            status,
            success: true,
            message,
            data,
        };
    },
    fail: (status: number, message: string, data?: any) => {
        return {
            status,
            success: false,
            message,
        };
    },
};

export default util;

// modules/responseMessage.ts -> response message 가공
const message = {
    NULL_VALUE: '필요한 값이 없습니다.',
    NOT_FOUND: '존재하지 않는 자원',
    BAD_REQUEST: '잘못된 요청',
    INTERNAL_SERVER_ERROR: '서버 내부 오류',

    // 포스팅 조회
    READ_POST_SUCCESS: '포스팅 조회 성공',
    CREATE_POST_SUCCESS: '포스팅 생성 성공',
    DELETE_POST_SUCCESS: '포스팅 삭제 성공',
    UPDATE_POST_SUCCESS: '포스팅 수정 성공',
}

export default message;

// modules/statusCode.ts
const statusCode = {
    OK: 200,
    CREATED: 201,
    NO_CONTENT: 204,
    BAD_REQUEST: 400,
    UNAUTHORIZED: 401,
    FORBIDDEN: 403,
    NOT_FOUND: 404,
    CONFLICT: 409,
    INTERNAL_SERVER_ERROR: 500,
    SERVICE_UNAVAILABLE: 503,
    DB_ERROR: 600,
};

export default statusCode;

 

 

라우팅 설정하기

routes/PostRouter.ts

import { Router } from "express";
import { PostController } from "../controllers";

const router: Router = Router();

router.post('/', PostController.createPost);
router.put('/:postId', PostController.updatePost);
router.get('/:postId', PostController.findPostById);
router.delete('/:postId', PostController.deletePost);

export default router;
위에서 작성한대로 나머지를 작성하려고 한다.
PostController.ts를 만들어 createPost, updatePost, findPostById, deletePost 4개의 함수를 만든다.
put, get, delete 메서드에 대해서는 postId를 파라미터로 받는다.

 

routes/index.ts

//router index file
import { Router } from 'express';
import PostRouter from "./PostRouter";

const router: Router = Router();

router.use('/post', PostRouter);

export default router;

 

 

DTO 작성

DTO는 Data Type Object의 약자로,

데이터를 받아오거나 반환하는 규격으로 interface로 작성한다.

(장고의 serializer 생각하면 편하더라는..)

 

// interfaces/post/PostCreateDto.ts -> create 용
export interface PostCreateDto {
    title: string;
    content: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostInfo.ts -> model 정의 용도
export interface PostInfo {
    title: string;
    content: string;
    additional: {
        category: string;
        season: string;
    };
}

// interfaces/post/PostResponseDto.ts -> 정보 조회 용
import mongoose from "mongoose";
import { PostCreateDto } from "./PostCreateDto";

export interface PostResponseDto extends PostCreateDto { // PostCreateDto 를 상속받아 확장시킨다.
    _id: mongoose.Schema.Types.ObjectId;
}


// interfaces/post/PostUpdateDto.ts -> update 용
import mongoose from "mongoose";

export interface PostUpdateDto {
    _id: mongoose.Schema.Types.ObjectId;
    title?: string;
    content?: string;
    additional?: {
        category: string;
        season: string;
    };
}

// interfaces/common/PostBaseResponseDto.ts -> id만 노출할 때
import mongoose from "mongoose";

export interface PostBaseResponseDto {
    _id: mongoose.Schema.Types.ObjectId;
}

다른 로직 작성하다가 필요에 맞게 DTO를 작성해주는 것이 좋다.

 

 

model 작성

models/Post.ts 를 만든다.

import mongoose from "mongoose";
import { PostInfo } from "../interfaces/post/PostInfo";

const PostSchema = new mongoose.Schema({
    title: {
        type: String,
        required: true
    },
    content: {
        type: String,
        required: true,
    },
    dateTimeOfPosting: {
        type: Date,
        required: true,
        default: Date.now,
    },
    additional: {
        category: { type: String },
        season: { type: String },
    },
});

export default mongoose.model<PostInfo & mongoose.Document>("Post", PostSchema);

dateTimeOfPosting은 생성 당시 일시를 저장한다.

additional은 중복 document(MongoDB에서 row에 해당하는 개념)



 

Controller 작성

import express, { Request, Response } from "express";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import statusCode from "../modules/statusCode";
import message from "../modules/responseMessage";
import util from "../modules/util";
import { PostService } from "../services";


const createPost = async (req: Request, res: Response): Promise<void> => {
    const postCreateDto: PostCreateDto = req.body;
    
    try {
        const data = await PostService.createPost(postCreateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.CREATED, message.CREATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const updatePost = async (req: Request, res: Response): Promise<void> => {
    const postUpdateDto: PostUpdateDto = req.body;
    const { postId } = req.params;

    try {
        const data = await PostService.updatePost(postId, postUpdateDto);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.UPDATE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const findPostById = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.findPostById(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.READ_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

const deletePost = async (req: Request, res: Response): Promise<void> => {
    const { postId } = req.params;

    try {
        const data = await PostService.deletePost(postId);
        res.status(statusCode.CREATED).send(util.success(statusCode.OK, message.DELETE_POST_SUCCESS, data));
    } catch (error) {
        res.status(statusCode.INTERNAL_SERVER_ERROR).send(util.fail(statusCode.INTERNAL_SERVER_ERROR, message.INTERNAL_SERVER_ERROR));
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

Request body는 req.body에, Request parameters는 req.params에 저장되어 있어서

필요한 경우에 맞게 할당시켜 사용한다.

 

controllers/index.ts도 수정한다.

import PostController from "./PostController"

// controller index file
export {
   PostController,
}

 

 

Service 작성

import { PostBaseResponseDto } from "../interfaces/common/PostBaseResponseDto";
import { PostCreateDto } from "../interfaces/post/PostCreateDto";
import { PostResponseDto } from "../interfaces/post/PostResponseDtd";
import { PostUpdateDto } from "../interfaces/post/PostUpdateDto";
import Post from "../models/Post";



const createPost = async (postCreateDto: PostCreateDto): Promise<PostBaseResponseDto> => {
    try {
    	// create를 위해 각 filed명에 값들을 할당시켜준다.
        const post = new Post({
            title: postCreateDto.title,
            content: postCreateDto.content,
            additional: {
                category: postCreateDto.additional?.category,
                season: postCreateDto.additional?.season,
            }
        });
        await post.save();

        const data = {
            _id: post.id
        };

        return data;
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const updatePost = async (postId: string, postUpdateDto: PostUpdateDto): Promise<PostUpdateDto | null> => {
    try {
        await Post.findByIdAndUpdate(postId, postUpdateDto); // update 로직
        const post = await findPostById(postId); // update 된 정보를 불러오는 로직
        // null이 될 경우를 처리해줘야 한다.
		if (!post) {
            return null;
        }
        return post;

    } catch (error) {
        console.log(error);
        throw error;
    }
}

const findPostById = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findById(postId);
        if (!post) {
            return null;
        }
        return post;
        
    } catch (error) {
        console.log(error);
        throw error;
    }
}

const deletePost = async (postId: string): Promise<PostResponseDto | null> => {
    try {
        const post = await Post.findByIdAndDelete(postId);
        if (!post) {
            return null;
        }
        return post;
    } catch (error) {
        console.log(error)
        throw error;
    }
}

export default {
    createPost,
    updatePost,
    findPostById,
    deletePost,
}

findById와 같은 메서드들은 mongoose 내장 함수이다.

용법이 그렇게 어렵지 않다.

 

 services/index.ts 도 수정해준다.

import PostService from "./PostService"

//service index file
export {
    PostService,
}

 

이렇게 작성이 끝이 났다.

 

 

요청 보내기

yarn run dev 실행

 

POST

PUT

userId는 무시해도 좋다(포스팅 내용과 다른 코드 때문)

 

GET

 

DELETE

 

 


 

프로젝트 구조에 대해 간단히 언급하고 포스팅을 마치려 한다.

 

Request <--> Controller <--> Service <--> Response(DB)

 

Controller는 Request 데이터를 적절히 가공해서 Service에 전달하는 역할만 한다.

Service는 가공된 데이터를 가지고 원하는 로직을 적용한 뒤 DB에 요청을 전달해 결과값을 받아 온다.

그리고 그 과정에서 DTO라는 틀을 통해 데이터를 주고 받는다.

 

Service 단에서 사용하는 로직을 '비즈니스 로직'이라고 한다.

이 '비즈니스 로직'을 Route, Controller에서 분리시켜 재사용, 유지보수가 용이하게 한다.

 

Request 객체를 받아오고 Response 객체를 만들어 전달하는 것을 Controller에서,

그것에 원하는 로직을 적용하는 것은 Service에서 실행하는 것이다.

 


 

이렇게 express 프로젝트를 생성하고 간단하게 모델과 CRUD api를 작성해서 테스트해보았다.

앞으로 node, express, MongoDB를 사용하는 프로젝트가 있다면,

본 포스팅 코드를 기반으로 서버 구현을 시작해도 좋을 것 같다.

 

 

 

반응형
반응형

이전 포스팅에서는

아주 기본적인 형태의 라우팅을 적용해보았다.

(+ 라우팅이 무엇인지)

🔗 이전 포스팅 링크 : https://woojin.tistory.com/25

 

[Express] 간단한 라우팅 적용하기 (Typescript 사용) #Node.js #Express

이전 포스팅에 이어서, 간단한 라우팅을 적용해보자. (이전 포스팅) Express 간단한 서버 세팅 👇 https://woojin.tistory.com/24 라우팅(Routing)이란? 애플리케이션 엔드포인트(URI)의 정의, 그리고 URI가 클

woojin.tistory.com

 

이번 포스팅에서는

유지보수가 용이하도록

라우터 관련 코드를 적절히 분리시켜보자.

 


 

디렉토리 구조 만들기

위와 같이 디렉토리 구조를 설계한다.

빈껍데기인 파일만 만들어 놓아도 된다.

 

src/api : 블로그 관련 api를 묶어놓은 폴더

src/router : api 폴더에 있는 api들의 라우팅을 담당하는 폴더

 

blog.ts, signup.ts, user.ts는 간단한 api를 구현한 것으로, 아래에서 상세 내용을 다루겠다.

 

 

 

디렉토리 구조 설명

대략적인 디렉토리 구조를 설명하고 넘어가보려 한다.

 

localhost:8080/api/user/1 에 GET 메서드를 요청한다면,,

 

1. index.ts로 이동한다.

2 .api를 보고 router/index.ts로 이동한다.

3. user를 보고 router/userRouter.ts로 이동한다.

4. get 메서드를 보고 파라미터(userId : 1)를 가지고 api/user.ts 에서 정의한 selectUser로 이동한다.

5. api/user.ts에서 정의한 selectUser를 실행한다.

 

쉽게 말하면,

api에서는 api 기능에 대한 내용을 다루고,

router에서는 메서드 종류와 요청에 대한 분기를 다룬다.

 

내용에 따라 폴더가 분리되어 있어, 쉽게 위치를 파악하고 유지보수할 수 있다.

 

 

api 폴더 작성

// blog.ts

import { Request, Response } from "express";

const selectPost = async (req: Request, res: Response) => {
    const param = req.params;

    return res.status(200).json({
        status: 200,
        message: "포스팅 조회 성공",
        posting_number: param.blogId
    });
};

const likePost = async (req: Request, res: Response) => {
    const param = req.params;

    return res.status(200).json({
        status:200,
        message: "좋아요 성공",
        posting_number: param.blogId
    });
};

export { selectPost, likePost };

blog.ts에서는 selectPost와 likePost 두 개의 함수를 정의한다.

req.params는 뒤에서 정의할 request url 상 파라미터를 가져오는 것이다.

 

// signup.ts

import { Request, Response } from 'express';

const signup = async (req: Request, res: Response) => {
    return res.status(201).json({
        status: 201,
        message: "회원가입 성공",
    });
}

export default signup;
// user.ts
import { Request, Response } from "express";

const selectUser = async (req: Request, res: Response) => {
    const param = req.params

    return res.status(200).json({
        status: 200,
        message: "유저 조회 성공",
        data: param
    });
};

export default selectUser;

json 형식의 메세지와 상태코드를 응답(response)한다.

 

 

router 폴더 작성

// blogRouter.ts

import express, { Router } from "express";
import { likePost, selectPost } from "../api/blog";

const router: Router = express.Router();

router.get('/:blogId', selectPost);
router.get('/like/:blogId', likePost);

export default router;

Router를 따로 import해놓고, express.Router()를 사용하는 것은

에어비엔비 표준 규격을 맞추기 위함이라고 한다.

 

코드에서 보이다시피, 특정 엔드포인트에 특정 메서드가 요청되면, api 폴더에 작성한 파일의 함수가 불러와진다.

 

엔드포인트 뒤에 ':blogId'와 같은 것은 url로 전달받은 파라미터를 저장할 변수명이다.

예를 들어 블로그 id가 3인 포스팅을 'likePost'하려고 한다면,

localhost:8080/api/blog/like/3 과 같이 요청을 받아

3을 'blogId'에 저장해서 likePost 함수가 있는 api/blog.ts로 전달하는 것이다.

 

// signupRouter.ts

import express, { Router } from 'express';
import signup from '../api/signup';

const router: Router = express.Router();

router.post('/', signup);

export default router;
// userRouter.ts

import express, { Router } from "express";
import selectUser from "../api/user";

const router: Router = express.Router();

router.get('/:userId', selectUser);

export default router;

 

마지막으로 index.ts를 만들어준다 (router 폴더 내부에 있는 index.ts)

// router/index.ts

import express, { Router } from "express";

import userRouter from "./userRouter";
import blogRouter from "./blogRouter";
import signupRouter from "./signupRouter";

const router: Router = express.Router();

router.use('/user', userRouter);
router.use('/blog', blogRouter);
router.use('/auth', signupRouter);

export default router;

위에서 작성한 파라미터 앞단에 붙는 url을 정의한다.

/user는 userRouter로 이동하고 (..)

 

이렇게 정의한 router는,

아래에서 루트 디렉토리에 있는 index.ts로 전달한다.

 

 

index.ts(루트 디렉토리) 작성

// index.ts

import express, { Request, Response, NextFunction } from 'express';
import apiRoute from './router/index'; // export한 이름과 다르게 import할 수 있다!

const app = express();

app.use(express.json());

app.use('/api', apiRoute);

app.get('/', (req: Request, res: Response, next: NextFunction) => {
    res.send('Hi! This is server tutorial.');
});

app.listen('8080', () => {
    console.log(`
    #############################################
        🛡️ Server listening on port: 8000 🛡️
    #############################################
    `);
});

 

이제 라우팅 구조가 명확하게 보일 것이다.

아래 예시들에 설명을 달아보자.

 

1. localhost:8080/ (get)

index.ts의 app.get에 따라 'Hi! This is server tutorial.'이 response로 보내진다.

실행 결과는 위와 같다.

 

2. localhost:8080/api/user/1 (get)

index.ts에서,

app.use('/api', apiRoute) 를 보고 apiRoute를 import한 ./router/index.ts로 이동한다.

 

./router/index.ts에서,

router.use('/user', userRouter)에 의해 userRouter를 import한 ./router/userRouter.ts로 이동한다.

 

./router/userRouter.ts에서,

router.get('/:userId', selectUser)로 이동해 userId:1 을 selectUser로 전달한다.

 

./api/user.ts에서,

selectUser가 실행되고, userId:1 은 req.params를 통해 전달된다.

실행 결과는 위와 같다.

 

 

 

간단한 라우팅 구조에서

유지보수가 용이한 라우터 분리 구조로 수정해보았다.

 

 

 

 

 

 

 

 

 

 

 

 

 

반응형

+ Recent posts