즐겁게 개발을...

개발보다 게임이 더 많이 올라오는 것 같은...

개발/Node.js

Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #1

다물칸 2023. 3. 30. 13:34
728x90

업데이트일자: 2023.04.05

참고했던 글들이 100% 완벽하게 되지 않아서 3~4일 헤메다 얼추 기반 프로젝트에 준하는 결과물을 만들 수 있을 것 같아 작성해봅니다. 아마 몇 주, 몇 달 후에 이 글을 따라 하시는 분들도 저와 같이 100% 성공하지는 못할 수도 있습니다. 

자바스크립트 진영은 매일/매시간 패키지들이 업데이트 되는 터라 서로 버전이 맞지 않아 발생될 수도 있는 일이니 그 부분 염두에 두고 이 글을 봐주셨으면 감사하겠습니다. 

개발 스택

Language Typescript
Backend Framework Nest.js
Frontend Framework Next.js (for React.js)
Database Sqlite
Database ORM Prisma
UI Framework Chakra-ui
Tools ESLint

글에 앞서 이 글을 쓴 기준을 종속된 패키지 버전을 참고해서 글을 이해해주시기 바랍니다. 

종속버전

"dependencies": {
    "@chakra-ui/next-js": "^2.1.1",
    "@chakra-ui/react": "^2.5.4",
    "@emotion/react": "^11.10.6",
    "@emotion/styled": "^11.10.6",
    "@nestjs/common": "^9.3.12",
    "@nestjs/core": "^9.3.12",
    "@nestjs/platform-express": "^9.3.12",
    "@nestjs/swagger": "^6.2.1",
    "@prisma/client": "^4.12.0",
    "@types/node": "18.15.11",
    "@types/react": "18.0.31",
    "@types/react-dom": "18.0.11",
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.0",
    "eslint": "8.37.0",
    "eslint-config-next": "13.2.4",
    "framer-motion": "^10.9.2",
    "next": "13.2.4",
    "next-auth": "^4.20.1",
    "react": "18.2.0",
    "react-dom": "18.2.0",
    "swagger-ui-express": "^4.6.2",
    "typescript": "5.0.2"
  },
  "devDependencies": {
    "@nestjs/cli": "^9.3.0",
    "dotenv-cli": "^7.1.0",
    "prisma": "^4.12.0"
  }

Node.js 버전: v18.15.0

 

개발환경구축

Next.js 프로젝트를 먼저 생성한 후, 생성된 폴더로 진입합니다.

# Next.js 프로젝트 생성 (기본 typescript, eslint를 사용)
# 프로젝트 이름을 "baseprj"로 명명
# 선택옵션은 모두 기본값으로 설정합니다. 
$ npx create-next-app --ts --eslint nextjs_nestjs_base
✔ Would you like to use `src/` directory with this project? … No* / Yes
✔ Would you like to use experimental `app/` directory with this project? … No* / Yes
✔ What import alias would you like configured? … @/*
# 생성된 폴더로 진입
$ cd nextjs_nestjs_base

tsconfig.json 수정

TypeScript오류를 방지하기 위해 옵션을 추가합니다.

{
  "compilerOptions": {
    ...,
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "strictPropertyInitialization": false,
  }
}

 

Nest.js 패키지를 설치하고 사용할 폴더와 기본 app서비스를 구성 합니다.

※ 소스를 왜 이렇게 해야 하는지에 대한 설명은 참고 URL 중 "Next.js와 Nest.js를 같이 써보자"를 참고해주세요.

# nest.js 패키지 설치
$ npm install @nestjs/common @nestjs/core @nestjs/platform-express
$ mkdir server
$ cd server
$ nest g service app
$ nest g controller app
$ nest g module app
$ rm app/*.spec.ts

app/app.service.ts 수정

import { Injectable } from "@nestjs/common";

@Injectable()
export class AppService {
  getHello(): string {
    return "Hello World!";
  }
}

app/app.controller.ts 수정

import { Controller, Get } from '@nestjs/common';
import { AppService } from './app.service';

@Controller()
export class AppController {
  constructor(private readonly appService: AppService) {}

  @Get()
  getHello(): string {
    return this.appService.getHello();
  }
}

app/app.module.ts 수정

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";

@Module({
  imports: [],
  controllers: [AppController],
  providers: [AppService],
})
export class AppModule {}

Next.js와 Nest.js를 연결

server/main.ts 파일생성

import { NestFactory } from "@nestjs/core";
import { AppModule } from "@/server/app/app.module";
import * as http from "http";
import { NextApiHandler } from "next";
import { INestApplication } from "@nestjs/common";

export module Main {
  let app: INestApplication;

  export async function getApp() {
    if (!app) {
      app = await NestFactory.create(AppModule, { bodyParser: false });
      app.setGlobalPrefix("api");
      await app.init();
    }

    return app;
  }

  export async function getListener() {
    const app = await getApp();
    const server: http.Server = app.getHttpServer();
    const [listener] = server.listeners("request") as NextApiHandler[];

    return listener;
  }
}

pages/api/[...catchAll].ts 파일생성

import { Main } from "@/server/main";
import { NextApiRequest, NextApiResponse } from "next";

const catchAll = (req: NextApiRequest, res: NextApiResponse) =>
  new Promise(async (resolve) => {
    const listener = await Main.getListener();
    listener(req, res);
    res.on("finish", resolve);
  });

export default catchAll;

pages/api/index.ts 파일생성

export { default } from "./[...catchAll]";

Prisma 설치

# server 폴더에서 수행합니다. 
# Prisma 설치
npm install prisma @prisma/client dotenv-cli @nestjs/cli --save-dev
npx prisma init

Prisma 스키마 설정

위의 과정을 수행하면 server폴더에 prisma폴더가 생성되고 그 안에 schema.prisma파일이 생성됩니다. 그 파일을 다음과 같이 수정합니다. 

데이터베이스는 Sqlite로 하고, 간단히 Prisma 인증 및 관리 서비스를 위한 사용자(user) 테이블 하나를 생성했습니다.

generator client {
  provider = "prisma-client-js"
}

datasource db {
  provider = "sqlite"
  url      = "file:./dev.db"
}

model user {
  id        Int       @id @default(autoincrement())
  email     String    @unique
  password  String
  name      String?
}

스키마를 실제 데이터베이스와 동기화 및 클래스 생성

$ npx prisma migrate dev

# 아래 패키지는 Prisma의 타입오류를 방지하기 위함입니다.
$ npm install class-transformer class-validator

# 세팅된 스키마를 기반으로 클래스를 생성한다.
$ npx prisma generate resource

Prisma 서비스 생성 및 소스수정

$ nest g service prisma
$ rm prisma/*spec.ts

생성된 prisma.service.ts 수정

import { Injectable, OnModuleDestroy, OnModuleInit } from "@nestjs/common";
import { PrismaClient } from "@prisma/client";

@Injectable()
export class PrismaService extends PrismaClient implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await this.$connect();
  }
  async onModuleDestroy() {
    await this.$disconnect();
  }
}

(옵션) user Restful API 생성

$ nest g service user
$ nest g controller user
$ nest g module user
$ rm user/*spec.ts

(옵션) user.entity.ts 파일추가 및 수정

export class CreateUserDto {
    email: string;
    name: string;
}

export class UpdateUserDto {
    email?: string;
    name?: string;
}

(옵션) user.service.ts 수정

import { Injectable } from "@nestjs/common";
import { PrismaService } from "@/server/prisma/prisma.service";
import { CreateUserDto, UpdateUserDto } from "@/server/user/user.entity";

@Injectable()
export class UserService {
  constructor(private readonly db: PrismaService) {}

  async findUserById(id: number) {
    return await this.db.user.findUnique({
      where: {
        id,
      },
    });
  }

  async findUserByEmail(email: string) {
    return await this.db.user.findUnique({
      where: {
        email,
      },
    });
  }

  async getList() {
    return await this.db.user.findMany();
  }

  async create(data: CreateUserDto) {
    return await this.db.user.create({
      data,
    });
  }

  async update(id: number, data: UpdateUserDto) {
    return await this.db.user.update({
      where: {
        id,
      },
      data,
    });
  }
}

(옵션) user.controller.ts 수정

import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { user } from "@prisma/client";
import { CreateUserDto } from "./user.entity";
import { UserService } from "./user.service";

@Controller("user")
export class UserController {
  constructor(private readonly userService: UserService) { }

  @Get("/:id")
  async findUserById(@Param("id") id: number): Promise<user | null> {
    return await this.userService.findUserById(id);
  }

  @Get("/email/:email")
  async findUserByEmail(@Param("email") email: string): Promise<user | null> {
    return await this.userService.findUserByEmail(email);
  }

  @Get("/")
  async getList(): Promise<user[]> {
    return await this.userService.getList();
  }

  @Post("/create")
  async create(@Body() createUserDto: CreateUserDto): Promise<user> {
    return await this.userService.create({
      email: createUserDto.email,
      name: createUserDto.name});
  }

  @Post("/update/:id")
  async update(@Param("id") id: number, @Body() updateUserDto: CreateUserDto): Promise<user> {
    return await this.userService.update(id, {
      email: updateUserDto.email,
      name: updateUserDto.name});
  }
}

(옵션) user.module.ts 수정

import { Module } from "@nestjs/common";
import { PrismaService } from "@/server/prisma/prisma.service";
import { UserController } from "./user.controller";
import { UserService } from "./user.service";

@Module({
    controllers: [UserController],
    providers: [UserService, PrismaService],
  })
  export class UserModule {}

(옵션) app.module.ts 수정

이 파일에 새로 추가된 user모듈을 넣어야 연결된다.

import { Module } from "@nestjs/common";
import { AppController } from "./app.controller";
import { AppService } from "./app.service";
import { PrismaService } from "@/server/prisma/prisma.service";

import { UserModule } from "@/server/user/user.module";

@Module({
  imports: [UserModule],
  controllers: [AppController],
  providers: [AppService, PrismaService],
})
export class AppModule {}

Swagger UI 설치 및 적용

위에서 생성한 API를 테스트 하기 위한 Swagger-UI를 설치하자. Swagger는 협업 또는 검수 시 API를 문서화 할 때도 활용된다. 

# server폴더에서 실행
$ npm install --save @nestjs/swagger swagger-ui-express

main.ts 파일 수정

swagger 문서를 생성하기 위한 코드를 삽입한다. 문서의 경로와 기존 api경로가 충돌이 나서 기존 전역 prefix옵션을 주석처리했음을 확인해보자.

import { NestFactory } from "@nestjs/core";
import { AppModule } from "@/server/app/app.module";
import * as http from "http";
import { NextApiHandler } from "next";
import { INestApplication } from "@nestjs/common";
import { DocumentBuilder, SwaggerModule } from '@nestjs/swagger';

export module Main {
  let app: INestApplication;

  export async function getApp() {
    if (!app) {
      app = await NestFactory.create(AppModule, { bodyParser: false });

      const config = new DocumentBuilder()
        .setTitle('Swagger Example')
        .setDescription('Swagger study API description')
        .setVersion('1.0.0')
        .addTag('swagger')
        .build();

      // config를 바탕으로 swagger document 생성
      const document = SwaggerModule.createDocument(app, config);
      // Swagger UI에 대한 path를 연결함
      // .setup('swagger ui endpoint', app, swagger_document)
      SwaggerModule.setup('api', app, document);

      //app.setGlobalPrefix("api");
      await app.init();
    }

    return app;
  }

  export async function getListener() {
    const app = await getApp();
    const server: http.Server = app.getHttpServer();
    const [listener] = server.listeners("request") as NextApiHandler[];

    return listener;
  }
}

Swagger 데코레이션을 각 controller을 수정

import { Body, Controller, Get, Param, Post } from "@nestjs/common";
import { user } from "@prisma/client";
import { CreateUserDto } from "./user.entity";
import { UserService } from "./user.service";

import { ApiOperation, ApiTags } from '@nestjs/swagger';

@ApiTags('user')  // Swagger에 Tag를 생성
@Controller("api/user")  // main.ts에서 api를 전역으로 한 걸 주석처리해서 여기서 처리
export class UserController {
  constructor(private readonly userService: UserService) { }

  @ApiOperation({ summary: '사용자 조회' })  // Swagger에 API를 생성
  @Get("/:id")
  async findUserById(@Param("id") id: number): Promise<user | null> {
    return await this.userService.findUserById(id);
  }

  @ApiOperation({ summary: '사용자 이메일로 조회' })  // Swagger에 API를 생성
  @Get("/email/:email")
  async findUserByEmail(@Param("email") email: string): Promise<user | null> {
    return await this.userService.findUserByEmail(email);
  }

  @ApiOperation({ summary: '사용자 목록 조회' })  // Swagger에 API를 생성
  @Get("/")
  async getList(): Promise<user[]> {
    return await this.userService.getList();
  }

  @ApiOperation({ summary: '사용자 생성' })  // Swagger에 API를 생성
  @Post("/create")
  async create(@Body() createUserDto: CreateUserDto): Promise<user> {
    return await this.userService.create({
      email: createUserDto.email,
      name: createUserDto.name,
      password: createUserDto.password
    });
  }

  @ApiOperation({ summary: '사용자 수정' })  // Swagger에 API를 생성
  @Post("/update/:id")
  async update(@Param("id") id: number, @Body() updateUserDto: CreateUserDto): Promise<user> {
    return await this.userService.update(id, {
      email: updateUserDto.email,
      name: updateUserDto.name});
  }
}

user.entity도 수정한다.

import { ApiProperty } from '@nestjs/swagger';

export class CreateUserDto {
    @ApiProperty({description: '이메일', required: true })
    email: string;
    @ApiProperty({description: '이름', required: true })
    name: string;
    @ApiProperty({description: '암호', required: true })
    password: string;
}

export class UpdateUserDto {
    @ApiProperty({description: '이메일', required: false })
    email?: string;
    @ApiProperty({description: '이름', required: false })
    name?: string;
}

 

최종소스

Github바로가기

참고

하나의 포트로 동시에 서비스하는 방식 (코드베이스)

[Reverse Proxy를 사용하는 다른 방법]
더보기

reverse proxy를 사용하는 것입니다. 예를 들어, nginx를 설치하고 설정하여 next.js와 nest.js 서버 모두에 대한 엔드포인트를 리버스 프록시 서버로 지정할 수 있습니다. 그런 다음 nginx가 모든 요청을 받아들이고 해당 요청을 각각의 서버로 전달하게 됩니다. 이렇게하면 사용자는 하나의 포트 (보통 80 또는 443)에서 next.js와 nest.js 애플리케이션을 모두 실행할 수 있습니다.

 

NextJS 와 NestJS를 같이 써보자 (1)

포스트하기 앞서 짧게 두 프레임워크를 연동하려 하는 이유를 말하자면, 우선 개인 포트폴리오 웹 페이지를 만들고 싶었다. 그래서 무료 클라우드 서버를 찾는 와중 국내 서비스인 클라우드타

velog.io

프론트와 백엔드를 각각 서버로 동작하는 방식

 

[nest.js + next.js] 가계부 만들기 1 - nest.js + next.js설치

RELATED POSTS nest.js + next.js 가계부 만들기 1. nest.js + next.js installation 2. antd 설치 및 레이아웃 커스터마이징 3. typeorm 설치와 mariaDB(mysql) 연동 4. swagger 설치와 간단한 crud api 개발 5. nest-next 패키지로 서

iamhmin.github.io

Nest.js에서 Swagger 사용하기

 

[Nest.js] swagger 적용하기

Nest.js에서 swagger document 작성해보기

velog.io

반응형