소스 코드

[리포지 링크]

들어가기 전에

이번 장에서는 앞에서 실습했던 NesJS API 만들기를 실전에 사용해보기 위해, 예전에 실무에서 작성했던 Backedn API 요청항목 중 일부를 참고하여 직접 만들어보려고 한다.

1. Backend API 명세서 확인

Primary Key(회원 고유번호, 클래스 고유번호)를 포함한 필요한 정보는 아래와 같다.

1. 회원 고유번호*
: int
: 조회하는 용도(1).

2. 클래스 고유번호*
: string
: 조회하는 용도(2).

3. 조 챗봇 랜딩 페이지에 적용할 한 개의 학습데이터*
: object
: 학생이 서버에 저장한 학습 데이터들 중 하나를 선택, 저장

4. 조 챗봇 랜딩 페이지의 디자인에 적용될 설정값들*
: object
: 저장하기 버튼 클릭 시, 조회 후, DB의 pageSettings에 설정창의 pageSettings로 덮어쓰기

4-1. 조 랜딩 페이지 대표 칼라 테마값*
: string, 1이상 20 이하의 길이
: 예) "blackWhite"

4-2. 조 랜딩 페이지 타이틀*
: string, 1이상 30 이하의 길이

4-3. 조 랜딩 페이지 설명*
: string, 1 이상 100 이하의 길이

4-4. 조 메인 배너 이미지*
: object, 이미지 파일의 이름, 스토리지 주소
: 구성 값
  : 4-4-1. name (strin)
  : 4-4-2. path (url)

4-5. 조 챗봇 이름*
: string, 1 이상 12 이하의 길이

4-6. 조 챗봇 한줄소개*
: string, 1 이상 30 이하의 길이

4-7. 조 챗봇 목적*
: string, 1 이상 100 이하의 길이

4-8. 조 챗봇 사용설명*
: string, 1 이상 100 이하의 길이

4-9. 조 챗봇 기대효과*
: string, 1 이상 30 이하의 길이

4-10. 조 환영 메시지*
: string, 1 이상 60 이하의 길이

4-11. 조 챗봇 아이콘*
: emoji string(utf8mb4)

2. 스키마 구성을 위한 변수명 정의

NestJS 프로젝트 생성 전, json 형태로 타입을 짜보고, 테스트 데이터도 넣었다. 페이지를 꾸미는 용도의 셋팅값과 챗봇을 구성하는 값을 각각 구분하여 작성했다.

{
    userNum: 1234,
    classNum: 5678,
    chatbotSetting: {
	   learningData: {
            topK: 0,
            topP: 0.7,
            content: "이것은 챗봇 학습 데이터"
    	},
        chatbotName: "챗봇",
        chatbotDescription: "챗봇 한줄소개",
        chatbotPurpose: "챗봇 목적",
        chatbotGuide: "챗봇 사용설명",
        chatbotExpectedEffect: "챗봇 기대효과",
        chatbotWelcomeMessage: "챗봇 환영 메시지",
        chatbotIcon: "😻"
    },
    pageSetting: {
        theme: "blackWhite",
        title: "랜딩페이지 타이틀입니다.",
        description: "랜딩페이지 설명입니다.",
        mainBanner: {
            name: "cat.png",
            path: "https://forumadmin.blob.core.windows.net/images/62f37380-f9cc-11ed-bb64-372aa008c65d.png",
        }
    }
}

3. 프로젝트 생성 및 디렉터리 초기화

순서 1. 원하는 경로에 프로젝트를 생성한다

npm install -g @nestjs/cli

npx @nestjs/cli new nestjs-api-practice-1

순서 2. 아래와 같이 디렉터리를 초기화 한다.

image-20230824141820579


순서 3. 컨트롤러, 서비스를 모듈에 등록하고, main.ts 코드도 아래와 같은지 확인한다. main.ts는 앱이 실행되었을 때 처음으로 실행되는 계층이다.

// app.module.ts

import { Module } from "@nestjs/common";
import { LandingPageController } from "./landingPage.controller";
import { LandingPageService } from './landingPage.service';

@Module({
  imports: [],
  controllers: [LandingPageController],
  providers: [LandingPageService]
})
export class AppModule { }
// landingPage.controller.ts

export class LandingPageController { }
// landingPage.service.ts

export class LandingPageService { }
// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

4. 스키마(landingPage.schema.ts) 작성하기

순서 1. mongoose 의존성 패키지 설치하기

npm install @nestjs/mongoose mongoose

순서 2. 도큐먼트 타입과 스키마를 작성한다

@Prop({ type: Object })를 사용하여 Object 타입을 지정하는 방식은 좋지 않다. 가능한 한 해당 객체의 구조를 명확하게 정의하는 것이 좋다. 아래 작성한 코드를 참고하도록 하자

// landingPage.schema.ts

import { Schema, Prop, SchemaFactory } from "@nestjs/mongoose";
import { Document } from "mongoose";

// 도큐먼트 정의
export type LandingPageDocument = LandingPage & Document;

@Schema()
export class LearningData {
    @Prop()
    topK: number;

    @Prop()
    topP: number;

    @Prop()
    content: string;
}

@Schema()
export class ChatbotSetting {
    @Prop({ type: LearningData })
    learningData: LearningData

    @Prop()
    chatbotName: string;

    @Prop()
    chatbotDescription: string;

    @Prop()
    chatbotPurpose: string;

    @Prop()
    chatbotGuide: string;

    @Prop()
    chatbotExpectedEffect: string;

    @Prop()
    chatbotWelcomeMessage: string;

    @Prop()
    chatbotIcon: string;
}

@Schema()
export class MainBanner {
    @Prop()
    name: string;

    @Prop()
    path: string;
}


@Schema()
export class PageSetting {
    @Prop()
    theme: string;

    @Prop()
    title: string;

    @Prop()
    description: string;

    @Prop({ type: MainBanner })
    mainBanner: MainBanner;
}


// 메인 스키마
@Schema()
export class LandingPage {
    @Prop()
    userNum: number;

    @Prop()
    classNum: number;

    @Prop({ type: ChatbotSetting })
    chatbotSetting: ChatbotSetting; // chatbotSetting 객체의 구조를 명확하게 지정

    @Prop({ type: PageSetting })
    pageSetting: PageSetting; // pageSetting 객체의 구조를 명확하게 지정
}

// 스키마 생성
export const LandingPageSchema = SchemaFactory.createForClass(LandingPage);

5. 몽고DB(landingPage.repository.js)를 사용하는 리포지토리 추가하기

순서 1. Backend API 명세서를 보고 아래와 같이 스펙을 정리한다

URL HTTP 메서드 설명
/landingPage GET 모든 유저의 랜딩페이지 구성값을 가져온다
/landingPage/클래스 고유번호/유저 고유번호 GET params로 넘어온 유저 고유번호와 클래스 고유번호로 해당 유저의 랜딩페이지 구성값을 가져온다
/landingPage POST 랜딩페이지의 구성값을 작성한다
/landingPage PUT 랜딩페이지의 구성값을 수정한다

순서 2. 위 스펙을 인터페이스(LandingPageRepository)를 녹여내어 작성한다. 인터페이스는 클라이언트로 HTTP params와 body등을 전달받아 통신하는 데이터를 의미한다. 그리고 인터페이스에서 Object Object도 위 스키마 정의 때와 비슷하게 필요한 필드를 추가하여 정의할 수 있다.

// landingPage.model.ts

// 각 interface를 필드라고 부른다
export interface LearningData {
    topK: number;
    topP: number;
    content: string;
}

export interface ChatbotSetting {
    learningData: LearningData;
    chatbotName: string;
    chatbotDescription: string;
    chatbotPurpose: string;
    chatbotGuide: string;
    chatbotExpectedEffect: string;
    chatbotWelcomeMessage: string;
    chatbotIcon: string;
}

export interface MainBanner {
    name: string;
    path: string;
}

export interface PageSetting {
    theme: string;
    title: string;
    description: string;
    mainBanner: MainBanner;
}

export interface PostDto {
    userNum: number;
    classNum: number;
    chatbotSetting: ChatbotSetting // chatbotSetting 객체의 구조를 명확하게 지정
    pageSetting: PageSetting; // pageSetting 객체의 구조를 명확하게 지정
}

순서 3. 리포지토리(landingPage.repository.ts)에 인터페이스를 정의하고, 몽고DB를 다룰 함수를 정의한다.

import { InjectModel } from "@nestjs/mongoose";
import { Model } from "mongoose";
import { LandingPage, LandingPageDocument } from "./landingPage.schema";
import { PostDto } from "./landingPage.model";
import { Injectable } from "@nestjs/common";

// 리포지토리 인터페이스 정의
export interface LandingPageRepository {
    getAllPost(): Promise<PostDto[]>;
    getPost(userNum: number, classNum: number): Promise<PostDto>;
    createPost(postDto: PostDto);
    updatePost(userNum: number, classNum: number, postDto: PostDto);
}

@Injectable()
export class LandingMongoRepository implements LandingPageRepository {
    // landingPageModel 주입
    constructor(@InjectModel(LandingPage.name) private landingPageModel: Model<LandingPageDocument>) { }

    // 모든 유저의 랜딩페이지 구성값을 가져옴
    async getAllPost(): Promise<PostDto[]> {
        return await this.landingPageModel.find().exec();
    }

    // 랜딩페이지의 구성값을 작성(생성)
    async createPost(postDto: PostDto) {
        const createPost = {
            ...postDto,
            createdDt: new Date(),
            updatedDt: new Date(),
        };
        this.landingPageModel.create(createPost);
    }

    // 특정 유저의 랜딩페이지의 구성값을 읽기
    // .findOne 메서드로 회원 고유번호와 클래스 고유번호를 키값으로 찾음
    async getPost(userNum: number, classNum: number): Promise<PostDto> {
        return await this.landingPageModel.findOne({ userNum, classNum });
    }

    // 랜딩페이지의 구성값을 수정
    async updatePost(userNum: number, classNum: number, postDto: PostDto) {
        const updatePost = {
            userNum,
            classNum,
            ...postDto,
            updatedPost: new Date()
        };
        await this.landingPageModel.findOneAndUpdate({ userNum, classNum }, updatePost)
    }
}

6. 서비스(LandingPageService) 작성하기

리포지토리 의존성을 주입하고, 리포지토리를 사용하는 단순한 로직으로 변경한다.

// landingPage.service.ts

import { PostDto } from "./landingPage.model";
import { Injectable } from "@nestjs/common";
import { LandingMongoRepository } from "./landingPage.repository";

@Injectable()
export class LandingPageService {
    // 리포지토리 의존성 주입
    constructor(private landingRepository: LandingMongoRepository) { }

    async getAllPosts() {
        return await this.landingRepository.getAllPost();
    }

    createPost(postDto: PostDto) {
        this.landingRepository.createPost(postDto);
    }

    // 1. 예외처리 하지 않으면 서버 종료 됨
    async getPost(userNum: number, classNum: number) {
        try {
            return await this.landingRepository.getPost(userNum, classNum);
        } catch (error) {
            console.log(error)
        }
    }

    // 수정된 결과를 보여주기 위해 return
    updatePost(userNum: number, classNum: number, postDto: PostDto) {
        return this.landingRepository.updatePost(userNum, classNum, postDto)
    }
}

1번. getPost()를 호출하는데도, 리턴값이 없길래 원인을 찾아봤더니, return await을 해주지 않아 결과를 받아볼 수가 없었다. 응답은 되는데 결과가 반환되지 않는다면, 이쪽을 살펴보자


7. 컨트롤러(landingPage.controller) 작성하기

서비스 의존성을 주입하여 아래와 같이 작성한다.

// landingPage.controller.ts

import { LandingPageService } from "./landingPage.service";
import { Controller, Get, Post, Body, Param, Put } from "@nestjs/common";

// API 호출시, <베이스URL>/landingPage 설정
@Controller('landingPage')
export class LandingPageController {

  // 서비스 의존성 주입
  constructor(private landingPageService: LandingPageService) { }

  @Get()
  getAllPosts() {
    console.log('모든 랜딩페이지 정보 가져오기');
    return this.landingPageService.getAllPosts();
  }

  @Post()
  createPost(@Body() postDto) {
    console.log('랜딩페이지 작성');

    this.landingPageService.createPost(postDto);
    return 'success';
  }

  @Get('/:classNum/:userNum')
  getPost(@Param('userNum') userNum: number, @Param('classNum') classNum: number) {
    console.log('랜딩페이지 하나 가져오기');
    return this.landingPageService.getPost(userNum, classNum);
  }

  @Put('/:classNum/:userNum')
  updatePost(@Param('userNum') userNum: number, @Param('classNum') classNum: number, @Body() postDto) {
    console.log('랜딩페이지 업데이트');
    return this.landingPageService.updatePost(userNum, classNum, postDto);
  }
}

8. 모듈(app.module.ts)로 패킹

app.module.ts에 imports와 provider를 설정한다.

// app.module.ts

import { Module } from "@nestjs/common";
import { LandingPageController } from "./landingPage.controller";
import { LandingPageService } from './landingPage.service';
import { MongooseModule } from "@nestjs/mongoose";
import { LandingPage, LandingPageSchema } from "./landingPage.schema";
import { LandingMongoRepository } from "./landingPage.repository";

@Module({
  imports: [
    // 1. 몽고DB 연결 설정, 끝에 데이터베이스(landingPage)의 이름으로 변경
    MongooseModule.forRoot(
      'mongodb+srv://<유저명>:<비번>@cluster0.vbpuhau.mongodb.net/landingPage'
    ),
    // 2. 몽고DB 스키마 설정
    MongooseModule.forFeature([{ name: LandingPage.name, schema: LandingPageSchema }])
  ],
  controllers: [LandingPageController],
  // 3. 프로바이더 설정
  providers: [LandingPageService, LandingMongoRepository]
})
export class AppModule { }

9. Postman 테스트하기

순서 1. Postman에 아래와 같이 request를 생성했다

image-20230825102002112


순서 2. POST 랜딩페이지 작성 테스트

image-20230825102110464


순서 3. GET 랜딩페이지 설정값 하나 가져오기

image-20230825102224729


순서 4. PUT 랜딩페이지 설정값 업데이트

image-20230825102411530


순서 5. GET 모든 랜딩페이지 설정값 가져오기

(생략)


10. 시행착오들

  1. 서버를 실행했을 때, 터미널에서 로그가 보이지 않았는데, 이러한 경우는 main.ts에 코드가 없어서 발생한 문제였다.
// main.ts

import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  await app.listen(3000);
}
bootstrap();

  1. 테스트는 Postman으로 하자.

  2. API 호출 시, 서브 도메인의 설정은 컨트롤러에서 @Controller의 매개변수로 설정할 수 있고, 저기에 있는 값을 메인 도메인뒤에 붙여야 한다.

// landingPage.controller.ts

... 생략 ...
@Controller('landingPage')
export class LandingPageController {

image-20230825103026017

참고

다음에 다루게 될 것

  • NestJS로 직접 기획한 API 만들기(2) - Delete와 예외처리

댓글남기기