업데이트일자: 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;
}
최종소스
참고
하나의 포트로 동시에 서비스하는 방식 (코드베이스)
[Reverse Proxy를 사용하는 다른 방법] |
reverse proxy를 사용하는 것입니다. 예를 들어, nginx를 설치하고 설정하여 next.js와 nest.js 서버 모두에 대한 엔드포인트를 리버스 프록시 서버로 지정할 수 있습니다. 그런 다음 nginx가 모든 요청을 받아들이고 해당 요청을 각각의 서버로 전달하게 됩니다. 이렇게하면 사용자는 하나의 포트 (보통 80 또는 443)에서 next.js와 nest.js 애플리케이션을 모두 실행할 수 있습니다.
프론트와 백엔드를 각각 서버로 동작하는 방식
Nest.js에서 Swagger 사용하기
'개발 > Node.js' 카테고리의 다른 글
try~catch문에서 error타입에 따라 처리하는 함수 (0) | 2024.04.23 |
---|---|
Typescript 현재 접속한 사용자의 아이피 가져오기 (0) | 2023.08.19 |
Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #3 (0) | 2023.04.05 |
Nest.js+Next.js를 이용한 기반 프로젝트 만들기 #2 (0) | 2023.04.05 |
[2021.03] Winston패키지를 이용한 Logger 클래스 (0) | 2021.03.26 |