[개인 프로젝트 Lottodolist] C,R을 위한 prisma, nestJS 세팅하기(백엔드)

규갓 God Gyu·2025년 4월 14일
0

프로젝트

목록 보기
81/81

첫 풀스택이다보니 가장 기초중의 기초인 C와 R먼저 잘 작동하는지 해보려 한다.

백엔드(API)

데이터 모델과 API 엔드포인트가 확립되어 프론트 개발을 이어서 진행해볼 예정

  • 데이터 모델
// apps/api/src/posts/post.entity.ts
export class Post {
id: number;
title: string;
createdAt: Date;
}

일단은 todolist를 성공해야하기에 부수적인거 빼고 제목만 넣어보기로!

여기서 entity란

데이터베이스의 테이블을 표현하는 클래스

객체 관계형 매핑(ORM)시스템에서 중요한 개념,

데이터베이스 테이블의 구조를 코드로 정의함

  • id:게시글 고유 식별자
  • title:게시글 제목
  • createdAt:게시글 생성 일시

그래서 결론으로 엔티티 클래스는 보통

  1. 데이터베이스 테이블의 스키마 정의
  2. 객체-관계 매핑(ORM)을 통해 객체 지향 코드와 관계형 데이터베이스 간의 다리 역할
  3. 데이터 유효성 검사 및 비즈니스 로직 캡슐화

근데 나는 TypeORM이 아닌 prisma를 사용할 예정이므로 코드내용이 달라짐

  • packages/database/prisma/schema.prisma 파일 내용
generator client {
provider = "prisma-client-js"
}

datasource db {
provider = "postgresql"
url      = env("DATABASE_URL")
}

model Post {
id        Int      @id @default(autoincrement())
title     String
createdAt DateTime @default(now())
}

이후 package.json쪽 수정이 필요한데

터보레포, 즉 monorepo구조로 프로젝트를 설정하고 있는데,

database쪽 package.json의 수정이 필요하다

이전 코드

{
"name": "@lottodolist/database",
"version": "1.0.0",
"description": "",
"private": true,
"main": "./src/index.ts",
"types": "./src/index.ts",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"db:generate": "prisma generate",
"db:push": "prisma db push",
"build": "tsup index.ts --format esm,cjs --dts"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@prisma/client": "^6.5.0",
"prisma": "^6.5.0"
},
"devDependencies": {
"typescript": "^5.8.2"
}
}

수정 코드

{
"name": "@lottodolist/database",
"version": "0.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"db:generate": "prisma generate",
"db:push": "prisma db push"
},
"dependencies": {
"@prisma/client": "^5.0.0"
},
"devDependencies": {
"prisma": "^5.0.0",
"typescript": "^5"
}
}

차이점을 비교하면

  1. 빌드 출력 경로

이전 : main, types가 소스 파일(./src/index.ts)를 직접 가리킴

권장: 컴파일된 결과물(dist/index.js, dist/index.d.ts)를 가리

  1. 빌드 도구

이전: tsup(더 현대적 도구, ESM/CJS양식 모두 지원)

권장:tsc사용(표준 ts 컴파일러)

tsup
TypeScript 프로젝트를 위한 빠르고 간단한 번들링 도구
esbuild를 기반으로 만들어졌고 TS코드를 다양한 형식(ESM,CJS 등)으로 빠르게 컴파일하고 번들링함. 복잡한 설정 없이 사용하기 쉬우며, 특히 라이브러리 만들 때 유용

  • 매우 빠른 빌드 속도(esbuild)
  • TS, JSX 지원
  • ESM/CJS 동시 출력 지원
  • Code splitting 지원
  • 최소한의 설정으로 사용 가능

tsc
TS Compiler의 약자, TS코드를 JS로 변환하는 공식 TS 컴파일러
tsconfig.json 파일 통해 다양한 컴파일 옵션 설정할 수 있으며, 타입 체크, 코드 변환 수행함

  • TS 코드의 타입 검사
  • ES5, ES6 등 다양한 JS 버전으로 컴파일 가능
  • 선언 파일(.d.ts)생성 가능
  • 프로젝트 구조와 의존성 분석

ESM
ECMAScript Modules는 JS의 공식 모듈 시스템
ES6에서 도입됨
import, export 키워드 사용하여 모듈 가져오고 내보내는 방식

  • 정적 분석이 가능한 구조(static imports)
  • 트리 쉐이킹에 최적화(사용하지 않는 코드 제거)
  • 비동기적 모듈 로딩 지원(dynamic imports)
  • 브라우저에서 네이티브 지원(<script type='module'>

CJS
CommonJS는 노드에서 사용되는 모듈 시스템, 서버사이드 JS 개발 위해 만들어짐
require()과 module.exports를 사용하여 모듈 가져오고 내보냄

  • 동기적 모듈 로딩(서버환경에 적합)
  • Node.js에서 기본으로 사용
  • 런타임에 모듈 평가(동적 require 가능)
  • 순환 의존성 처리 메커니즘
  1. 의존성 관리

이전: prisma가 일반 의존성에 있음

권장:prisma가 개발 의존성에 있음(더 적절한 위치)

일반 의존성

개발 의존성

  1. 패키지 메타데이터

이전: private:true (npm에 게시 불가)

권장: private 설정 없음(모노레포 내부에서 다른 패키지에 의존성으로 사용 가능)

dist/index.js가 ts가 아닌 이유

  • 소스 코드 : .ts 파일(TS)
  • 빌드된 코드:.js파일과 .d.ts파일(타입 정의)

ts가 개발 언어이지만, node.js는 js만 실행할 수 있음

그래서 개발시엔 .ts파일 작성하고 빌드과정에서 .js로 컴파일됨 그리고 타입 정보는 .d.ts파일로 추출

그리고 필드에서 main은 다른 패키지가 이 패키지 가져올 때 불러올 js 파일

types는 TS가 타입 체크에 사용할 타입 정의 파일

패키지 설정 중요한 이유

api가 database 패키지를 의존할 경우 중요함

  1. 타입 지원: ts가 올바르게 타입 인식하려면 types 필드가 정확한 타입 정의 파일을 가리켜야함
  2. 모듈가져오기:main필드는 컴파일된 js코드를 가리켜야 다른 패키지에서 올바르게 가져올 수 있음
  3. 빌드 프로세스:의존 패키지 변경될 때 올바르게 다시 빌드되어야 함

최종 package.json

{
"name": "@lottodolist/database",
"version": "0.0.1",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"scripts": {
"build": "tsup src/index.ts --format esm,cjs --dts --outDir dist",
"db:generate": "prisma generate",
"db:push": "prisma db push"
},
"dependencies": {
"@prisma/client": "^6.5.0"
},
"devDependencies": {
"prisma": "^6.5.0",
"typescript": "^5.8.2",
"tsup": "^latest"
}
}

이 설정이 나은 이유는

  1. 다른 패키지가 이 패키지 가져올 때 컴파일된 코드(dist/index.js)를 사용하도록 설정
  2. 현대적 도구:더 빠른 빌드 위해 tsup계속 사용하되, 출력 디렉토리 dist로 명시
  3. 의존성 도구:개발에만 필요한 도구 prisma는 devDependencides에 배치

그 다음에 실제 api의 package.json엔 이 내용 추가해야 데이터베이스쪽 사용 가능

"dependencies": {
"@lottodolist/database": "workspace:*",
// 다른 의존성들...
}

index.ts

import { PrismaClient } from '@prisma/client';

const prismaClientSingleton = () => {
  return new PrismaClient();
};

declare global {
  var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

export const prisma = globalThis.prisma ?? prismaClientSingleton();

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

export * from '@prisma/client';

일단 prisma가 node와 ts를 위한 현대적 데이터베이스 ORM도구

import { PrismaClient } from '@prisma/client';

PrismaClient - Prisma핵심 클래스, DB와의 모든 상호작용 처리

@prisma/client: prisma 스키마 기반 자동 생성된 클라이언트 라이브러리

const prismaClientSingleton = () => {
  return new PrismaClient();
};

PrismaClient의 새 인스턴스 생성

싱글톤 패턴 구현 위한 함수

여기서 싱글톤패턴은 데이터베이스 클라이언트 인스턴스를 애플리케이션 전체에서 하나만 생성해서 공유하는걸 의미

Next.js나 서버리스 환경에서 Prisma인스턴스를 계속 새로 만들면 DB 커넥션이 너무 많아져서 에러가 발생되므로 매우 중요

declare global {
  var prisma: undefined | ReturnType<typeof prismaClientSingleton>;
}

declare global: TS에게 전역 객체를 확장한다고 알려줌

var prisma: Node.js의 전역 변수에 prisma라는 속성 추가

ReturnType: prismaClientSingleton 함수의 반환 타입을 자동으로 추론

export const prisma = globalThis.prisma ?? prismaClientSingleton();

globalThis.prisma:전역 객체에서 prisma 변수를 찾음

??: Nullish병합 연산자, 왼쪽 값이 null이나 undefined일때만 오른쪽 값 사용

prismaClientSingleton():전역에 prisma없을 시 새 인스턴스 생성

if (process.env.NODE_ENV !== 'production') globalThis.prisma = prisma;

개발환경에서만 prisma 인스턴스를 전역 객체에 저장

이러면 Hot Reload나 Fast Refresh 중에도 DB 연결 유지됨

Hot Reload

코드가 변경될 때 애플리케이션의 상태 유지하면서 변경된 모듈만 교체하는 기술

  • 코드 변경 시 전체 페이지를 새로고침 하지 않음
  • 일반적으로 애플리케이션 상태 유지하지만, 항상 완벽하진 않음
  • Webpack의 Hot Module Replacement(HMR)이 대표적인 구현체
  • Vue,React,Angular 등 다양한 프레임워크에서 지원

Fast Refresh(빠른 새로고침)

React팀이 개발한 개선된 형태의 핫리로드 시스템

  • React 컴포넌트 전용으로 최적화
  • 컴포넌트 상태를 더 안정적으로 유지
  • 함수형 컴포넌트와 훅을 더 잘 지원
  • 오류 복구 메커니즘 개선
  • Nest.js, React Native, Create React App 등에서 기본 제공

만약 개발 중 Hot Reload나 Fast Refresh가 발생하면

  • 코드 일부 다시 실행
  • 이때 new PrismaClient()처럼 새 인스턴스를 생성하는 코드도 다시 실행됨
  • 결과적으로 여러 데이터베이스 연결이 생성됨
  • 이것이 연결 제한 도달 문제를 일으킬 수 있음

그치만 싱글톤 패턴은 이미 존재하는 인스턴스를 재사용해 이 문제를 방지함

이전 코드의

if(process.env.NODE_ENV !== 'production'

부분이 개발 환경에서 이 문제를 해결해줌

즉, 코드가 개발 중 여러번 다시 로드되어도 데이터베이스 연결 하나만 유지되도록 보장함

일단 prisma 기본 세팅은 끝났으니 이제

백엔드(NestJS)

NestJS에 Prisma 모듈 추가

apps/api/src/prisma/prisma.service.ts 생성

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { prisma } from '@lottodolist/database';

@Injectable()
export class PrismaService implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {
    await prisma.$connect();
  }

  async onModuleDestroy() {
    await prisma.$disconnect();
  }

  get client() {
    return prisma;
  }
}

이제 기존 Prisma클라이언트를 database쪽에서 만들어놨으니 NestJS애플리케이션에서 그 클라이언트를 관리하는 서비스를 만드는 코드임

import { Injectable, OnModuleInit, OnModuleDestroy } from '@nestjs/common';
import { prisma } from '@lottodolist/database';
  • @nestjs/common : NestJS의 핵심 기능 제공하는 패키지
  • Injectable: 이 클래스가 Nest의 의존성 주입 시스템에 등록될 수 있음 나타냄
  • OnModuleInit, OnModuleDestroy: NestJS의 생명주기 인터페이스, 모듈이 초기화되거나 종료될 때 실행될 메서드를 정의함
  • prisma:앞서 설정한 데이터베이스 패키지에서 가져온 Prisma클라이언트 인스턴스
@Injectable()
export class PrismaService implements OnModuleInit, OnModuleDestroy {
  • @Injectable(): 이 데코레이터는 NestJS에게 이 클래스가 서비스로 주입될 수 있다고 알려줌
  • implements OnModuleInit, OnModuleDestroy: 이 클래스가 NestJS 생명주기 인터페이스를 구현함을 나타냄
async onModuleInit() {
  await prisma.$connect();
}
  • onModuleInit(): NestJS 애플리케이션이 시작될 때 자동으로 호출
  • prisma.$connect(): Prisma클라이언트가 데이터베이스에 연결하도록 함. 자동으로도 이루어진다고 하지만 명시적으로 작성하여 애플리케이션 시작 시 연결문제를 즉시 확인할 수 있음
async onModuleDestroy() {
  await prisma.$disconnect();
}
  • onModuleDestroy(): NestJS 애플리케이션이 정상적으로 종료될 때 호출됨
  • prisma.$disconnect():데이터베이스 연결을 깔끔하게 닫아 리소스 누수를 방지
get client() {
  return prisma;
}
  • get client(): 이것은 게터 메서드(getter), PrismaService인스턴스를 통해 prisma클라이언트에 접근할 수 있게 함
  • 다른 서비스나 컨트롤러에서 이 서비스를 주입받아 prisma클라이언트를 사용할 수 있음

이 서비스의 목적

NestJS에서 Prisma 서비스 만드는 이유

  1. 생명주기 관리: 애플리케이션 시작과 종료 시 데이터베이스 연결을 적절히 관리
  2. 의존성 주입:NestJS의 의존성 주입 시스템 활용해 다른 서비스나 컨트롤러에 Prisma 기능 제공
  3. 테스트 용이성:실제 데이터베이스 대신 모의(mock)객체 주입하기 쉽게 만듬
  4. 중앙 집중화:DB관련 설정이나 로직 한곳에서 관리 가능

apps/api/src/prisma/prisma.module.ts생성

import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';

@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}

이 코드를 이용하면 NestJS의 모듈 시스템을 사용하고 PrismaService를 애플리케이션 전체에서 사용할 수 있게 해줌

import { Module, Global } from '@nestjs/common';
import { PrismaService } from './prisma.service';
  • Module: NestJS에서 애플리케이션의 구조를 구성하는 데코레이터
  • Global: 이 모듈을 전역으로 등록하는 데코레이터
  • PrismaService: 방금 만든 Prisma서비스 가져옴
@Global()
@Module({
  providers: [PrismaService],
  exports: [PrismaService],
})
export class PrismaModule {}
  • @Global(): 모듈을 전역으로 만들어 다른 모듈에서 임포트하지 않아도 사용할 수 있게 함
  • @Module(): NestJS 모듈 정의하는 데코레이터
    • providers:모듈 내에서 생성되고 공유될 서비스들
    • exports:모듈에서 외부로 노출되어 다른 모듈에서 사용할 수 있게 될 서비스들
  • export class PrismaModule: 실제 모듈 클래스 선언

여기서 모듈은 NextJS에선 관련 기능을 모듈로 그룹화 함. 모듈은 서비스,컨트롤러, 미들웨어 등 포함할 수 있음

게시글 모듈 생성

apps/api/src/posts/dto/create-post.dto.ts 생성

import { IsNotEmpty, IsString } from 'class-validator';

export class CreatePostDto {
  @IsNotEmpty()
  @IsString()
  title: string;
}

게시글 생성 요청의 데이터 구조를 정의하고 검증해주는 DTO(Data Transfer Object)클래스임

DTO란?

계층 간 데이터 전송을 위한 객체

  1. 데이터 형식 정의: 클라이언트에서 서버로 전송되는 데이터 구조 명확히 정의
  2. 유효성 검증: 입력된 데이터가 올바른 형식인지 확인
  3. 문서화: 어떤 데이터가 필요한지 명확히 하여 API 사용 더 쉽게함

코드 설명

  1. class-validator 라이브러리에서 검증데코레이터를 가져옴
  2. CreatePostDto 클래스를 정의함
    • title이 비어있지 않은 문자열이여야함

여기서 게시글 모듈이란?

Next.JS에서 모듈은 관련 기능을 그룹화하는 방법.

즉 게시글 관련된 모든 기능을 모아둔다 생각하면 됨

  • Controller: 클라이언트 HTTP 요청 처리(게시글 생성,조회,수정,삭제)
  • Service:비즈니스 로직 처리(데이터베이스에 게시글 저장, 조회 etc)
  • DTO:데이터 검증 및 전송 객체(클라이언트쪽 데이터 검증)
  • Entity:데이터베이스 테이블 구조 정의(게시글 테이블)

| class-validator와 class-transformer 설치해야하는 이유

NestJS애플리케이션에서 데이터 검증과 변환을 위한 라이브러리임

  1. 데이터 검증
  • 클라이언트에서 받는 데이터가 우리가 기대하는 형식과 규칙에 맞는지 확인해줌
  1. 타입 변환
  • HTTP 요청으로 받은 JSON 데이터를 TS객체로 자동변환해야함.
  • JSON으로 받은 데이터는 기본적으로 모두 일반 객체이므로, 우리가 정의한 클래스의 인스턴스로 변환해야함
  1. 실제 작동 방식
  • 클라이언트가 POST 요청으로 게시글 데이터 보냄(JSON)
  • NestJS는 자동으로 이 JSON을 CreatePostDto 클래스의 인스턴스로 변환(class-transformer 사용)
  • ValidationPipe는 이 인스턴스에 대해 class-validator 데코레이터 기반 유효성 검사 실행
  • 검증에 실패하면 자동으로 400 Bad Request 오류 반환
  • 검증에 성공하면 컨트롤러 메서드 실행

여기서 ValidationPipe는 main.ts에 적용하는데

// apps/api/src/main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // 전역 파이프로 ValidationPipe 추가
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // DTO에 정의되지 않은 속성은 자동으로 제거
    transform: true,  // 자동으로 타입 변환 활성화
  }));
  
  await app.listen(3000);
}
bootstrap();

이런식으로 사용한다

apps/api/src/posts/posts.service.ts 생성

import { Injectable } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
import { CreatePostDto } from './dto/create-post.dto';

@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}

  create(createPostDto: CreatePostDto) {
    return this.prisma.client.post.create({
      data: createPostDto,
    });
  }

  findAll() {
    return this.prisma.client.post.findMany({
      orderBy: {
        createdAt: 'desc',
      },
    });
  }

  findOne(id: number) {
    return this.prisma.client.post.findUnique({
      where: { id },
    });
  }
}

NestJS의 PostsService 정의

게시글과 관련된 데이터베이스 작업을 처리하는 서비스 클래스

일단 당장은 생성만 하기로 했으니 나머지는 뺄 예정이고(추가로 공부하면서 그때 다시 추가하면서 리마인드)

  • Injectable 데코레이터 가져옴
  • 이전에 만든 PrismaService 가져와 데이터베이스에 연결
  • 게시글 생성 위한 DTO 클래스 가져옴
@Injectable()
export class PostsService {
  constructor(private prisma: PrismaService) {}
  • @Injectable() 데코레이터는 이 클래스가 NestJS 의존성 주입 시스템에 등록됨을 의미
  • 생성자에서 PrismaService 주입닫음(의존성 주입)
create(createPostDto: CreatePostDto) {
    return this.prisma.client.post.create({
      data: createPostDto,
    });
  }
  • 게시글 생성 메서드
  • Prisma 클라이언트 통해 데이터베이스에 새 게시글 생성
  • createPostDto의 데이터가 그대로 DB에 저장

결국 이 서비스 클래스는 컨트롤러에서 사용되어 HTTP요청을 처리하게 됨

컨트롤러는 클라이언트 요청을 받아 이 서비스의 메서드를 호출하고, 서비스는 실제 데이터베이스 작업을 수행함

apps/api/src/posts/posts.controller.ts

import { Controller, Get, Post, Body, Param, ParseIntPipe } from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';

@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}

  @Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }

  @Get()
  findAll() {
    return this.postsService.findAll();
  }

  @Get(':id')
  findOne(@Param('id', ParseIntPipe) id: number) {
    return this.postsService.findOne(id);
  }
}

이 코드는 NestJS의 컨트롤러.

클라이언트의 HTTP 요청을 받아서 처리하는 역할임

물론 Post 제외하면 아직 안씀

import { Controller, Get, Post, Body, Param, ParseIntPipe } from '@nestjs/common';
import { PostsService } from './posts.service';
import { CreatePostDto } from './dto/create-post.dto';
  • @nestjs/common 에서 필요한 데코레이터와 파이프를 가져옴
  • PostsService를 가져와서 실제 데이터 처리를 위임함
  • 게시글 생성에 사용할 DTo 클래스 가져옴
@Controller('posts')
export class PostsController {
  constructor(private readonly postsService: PostsService) {}
  • Controller(’posts’)데코레이터는 이 컨트롤러가 /posts 경로에 대한 요청을 처리함을 의미
  • 생성자에서 PostsService를 주입받아 데이터 처리를 위임
@Post()
  create(@Body() createPostDto: CreatePostDto) {
    return this.postsService.create(createPostDto);
  }
  • @Post() 데코레이터는 HTTP POST 요청 처리함(ex - /posts로의 POST요청)
  • @Body() 데코레이터는 HTTP 요청 본문을 createPostDto 매개변수로 변환
  • 결국 이 메서드는 새 게시글을 생성하는 API 엔드포인트임

결국 계속 리마인드 하자면

  1. 클라이언트가 HTTP 요청 보냄
  2. NestJS 라우팅 시스템이 요청을 적절한 컨트롤러 메서드로 전달
  3. 컨트롤러는 요청 데이터를 처리하고 서비스 메서드를 호출함
  4. 서비스는 비즈니스 로직을 실행하고 데이터베이스 작업 수행
  5. 결과가 다시 컨트롤러로 반환되고, 컨트롤러는 이를 클라이언트에게 응답으로 보냄
  • 컨트롤러: HTTP 요청/응답 처리 담당
  • 서비스:비즈니스 로직 및 데이터 액세스 담당
  • DTO: 데이터 검증 및 전송 형식 정의 담당

apps/api/src/posts/posts.module.ts 생성

import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';

@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}

post관련 모듈 정의한 것

import { Module } from '@nestjs/common';
import { PostsService } from './posts.service';
import { PostsController } from './posts.controller';
  • @nestjs/common에서 Module 데코레이터 가져옴
  • 앞서 만든 PostsService와 PostsController를 가져옴
@Module({
  controllers: [PostsController],
  providers: [PostsService],
})
export class PostsModule {}
  • @Module() 데코레이터는 NestJS 모듈을 정의함
  • controllers:이 모듈에 포함될 컨트롤러 목록
  • providers:이 모듈에 포함될 서비스나 다른 프로바이더 목록

모듈의 역할과 중요성

  1. 조직화: 관련 기능을 함께 그룹화하여 코드를 더 체계적으로 관리
  2. 캡슐화: 모듈 내부 구현을 숨기고 필요한 것만 내보냄
  3. 의존성 관리: 다른 모듈과의 의존성을 명확하게 정의

즉 이 PostsModule은 게시글 관련 모든 기능(컨트롤러와 서비스)을 하나로 묶어 놓은 것

이 모듈은 루트 모듈(AppModule)에 가져와져서 애플리케이션의 일부로 등록

import { Module } from '@nestjs/common';
import { AppController } from './app.controller';
import { AppService } from './app.service';
import { PostsModule } from './posts/posts.module';

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

이런식으로 imports에 넣어서 사용

백쪽 에러들 잡기

  1. prisma.service.ts

여기서

import { prisma } from '@lottodolist/database';

임포트 에러가 ㅅ발생하고 있는데 이유는 @lottodolist/database 패키지에서 prisma를 제대로 내조내지 않거나, 모노레포 설정때문일 수 있음

database 패키지의 index.ts를 살펴보니

여기서부터 @prisma/client와 process 객체를 찾고 있지 못하고 있었음 그래서 추가로 설치 시도

pnpm add @prisma/client

이걸 추가 설치하니 해결되었는데,

@prisma/client란?

Prisma ORM의 핵심 구성 요소 중 하나

데이터베이스와 상호작용 하는데 필요한 클라이언트 라이브러리

일반적으로 Prisma사용할 때 2개 패키지가 필요

  1. prisma

이것은 개발 도구로, Prisma CLI를 포함함. 데이터베이스 스키마를 정의하고, 마이그레이션을 관리하고, 데이터베이스를 시각화하는 등의 작업에 사용됨

  1. @prisma/client

애플리케이션 코드에서 데이터베이스와 통신하는데 사용하는 실제 클라이언트. Prisma 스키마를 기반으로 자동 생성된 타입-세이프한 쿼리빌더

일반적 워크플로우는

  • prisma를 개발 의존성으로 설치
  • 스키마를 정의(prisma/schema.prisma
  • npx prisma generate 명령을 실행하여 스키마를 기반으로 @prisma/client를 실행
  • 애플리케이션 코드에서 @prisma/client를 사용하여 데이터베이스 쿼리 실행

다음은 process 객체인데

process는 Node.js에서 제공하는 전역 객체

이 객체는 현재 실행 중인 Node.js 프로세스에 대한 정보와 제어 기능을 제공

process.env는 환경 변수에 접근할 수 있게 도와줌

process를 찾지 못하는 원인 일 수 있는 문제들

  1. 브라우저 환경에서 실행중

브라우저엔 process객체가 기본적으로 전재하지 않음. Node.js환경에서만 사용할 수 있음

  1. TS 설정 문제

TypeScript에서 process를 인식하지 못하는 경우

  1. 번들러 설정 문제

Webpack같은 번들러가 process 객체를 자동으로 제공안할 수도있음

일단 패키지가 다양한 디렉토리에 존재하는데

packages/database쪽 루트로 이동해서 @types/node 패키지를 개발 의존성으로 설치 시도

pnpm add -D @types/node

여기서 개발 의존성으로 설치하는 이유는, @types/node는 TS에서 Node.js의 타입 정의를 제공하는 패키지로써 개발 시에만 필요하고 실제 프로덕션 환경에서는 필요하지 않아서 개발 의존성으로 설치하는 것임

이후 packages/database에서 tsconfig.json 파일을 추가하고

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "types": ["node"],
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

해당 내용을 넣으니 process의 빨간줄이 사라졌는데

tsconfig.json파일은 TS 프로젝트의 컴파일 옵션과 설정을 정의하는 파일임

보통 프로젝트 루트 디렉토리나 각 패키지 디렉토리에 위치함

나같은 경우는 모노레포여서 각 패키지에 넣어주는게 좋을 듯 함

@types/node 패키지를 설치했기 때문에, Node.js의 내장 객체와 API에 대한 TS 타입 정의를 제공받음

tsconfig.json파일 내용에

{
  "compilerOptions": {
    "types": ["node"]
  }
}

이 설정을 넣었으니 Node.js의 타입 정의를 사용하라고 명시적으로 알려준것임

이 부분을 해결했어도 api쪽의 prisma.service.ts에서 여전히

@lottodolist/database 패키지에서 prisma 를 제대로 내보내고 있지 않음

api쪽의 package.json에 의존성을 넣어주려고

"dependencies": {
  "@lottodolist/database": "workspace:*",
  // 다른 의존성들...
}

이 내용을 담아줬어도 소용이 없엇다.

이후 해결을 위해

  1. Prisma 클라이언트 생성하기
cd packages/database
pnpm db:generate

database쪽에서 prisma generate실행해서 Prisma 클라이언트를 생성함

  1. database 패키지의 tsconfig.json 수정
{
  "compilerOptions": {
    "target": "es2016",
    "module": "commonjs",
    "moduleResolution": "node",
    "declaration": true,
    "outDir": "./dist",
    "esModuleInterop": true,
    "forceConsistentCasingInFileNames": true,
    "strict": true,
    "skipLibCheck": true
  },
  "include": ["src/**/*"]
}

기존 코드는

{
  "extends": "../../tsconfig.json",
  "compilerOptions": {
    "types": ["node"],
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src/**/*"]
}

이렇게 넣어서 index.ts의 process 에러는 해결은 했지만 왜 이렇게 차이가 나는지 비교해보자

주요 차이점

  1. extends

기존: 루트 디렉토리의 tsconfig.json 파일 상속

이후: 독립적인 설정으로, 상속 없음

  1. 설정의 상세함

기존:루트에서 상속받고, 패키지별 특수 설정만 추가

이후:모든 필요한 설정 직접 명시(타겟,모듈 시스템,엄격모드 등)

기존도 충분히 근거있는 방식이지만, 기존 방식으로 하였을 때 tsconfig.json을 찾지 못한다고 하였기 때문에, 이후 방식을 채택해보자(그리고 심지어 루트에 tsconfig.json은 없엇네 ;;)

이렇게 하니 tsconfig.json파일도 정상적으로 작동!!

이제 prisma 클라이언트 생성을 위해

pnpm db:generate 이후

pnpm build 까지 시도하였으나 여기서 또 에러가 발생했다

Prisma 클라이언트 생성까진 잘 되었으나, tsup 빌드 중에 오류가 발생한 것인데,

src/index.ts(1,10): error TS2305: Module '"@prisma/client"' has no exported member 'PrismaClient'.

@prisma/client 에서 PrismaClient 가져오는 방식이 문제가 있다는 것이다.

그치만 코드 자체가 문제가 없으므로, packages에서 database 폴더 안에서 관리하던 prisma 관련된 코드를 api에서 직접 prisma를 설정하는걸로 바꿔보려 한다.

일단 apps/api쪽으로 넘어와서

pnpm add -D prisma
pnpm add @prisma/client

설치 진행

그리고 src/prisma쪽에서 module, service 생성한 것과 다르게 prisma 디렉토리를 api바로밑에 생성

schema.prisma 파일도 생성(내용 그대로)

이후 Prisma 클라이언트 생성

cd apps/api
pnpm prisma generate

생성은 이전에도 잘되었으니 이번에도 잘 되었고

const prisma = new PrismaClient();

직접 인스턴스 생성해서 해결됨 후..

당장은 기존 database에서 가져다 쓰려고 많은 부분을 코드입력했고, 그거때문에 다시 재수정해야하긴하지만.. 일단은 넘어가자 나중에 중앙화 시킬수도있기에..database쪽에

그리고 prisma generate 명령어는 항상 실행해야 하는 것은 아님

  1. schema.prisma 파일 처음 만들었을 때
  2. 파일 수정했을 때
  3. ㅅㅐ 환경에서 프로젝트 클론했을 때(처음 설정 시)
  4. @prisma/client 패키지 업데이트했을 때

이후 스키마 변경 사항 실제 데이터 베이스에 적용하려면 prisma db push 이용하면 됨

api쪽 Dockerfile 수정

이제 docker-compose up 하면 정상 실행되지만,

설정 수정 중 npm으로 설치하고 prisma관련 내용을 넣어줘야해서 수정작업을 들어갔다

FROM node:18-alpine

# NestJS CLI 전역 설치
RUN npm install -g @nestjs/cli

WORKDIR /app

# 패키지 파일만 복사
COPY package.json .
COPY tsconfig.json .
COPY nest-cli.json .

# 의존성 설치
RUN npm install

# 소스 코드 복사
COPY src/ ./src/

# 개발 서버 실행
CMD ["npm", "run", "dev"]

기존 내용에서,

FROM node:18-alpine

# pnpm 전역 설치
RUN npm install -g pnpm

# NestJS CLI 전역 설치 (필요한 경우)
RUN pnpm add -g @nestjs/cli

WORKDIR /app

# 패키지 파일 복사
COPY package.json .
COPY pnpm-lock.yaml .  # pnpm 락 파일도 복사
COPY tsconfig.json .
COPY nest-cli.json .

# pnpm으로 의존성 설치
RUN pnpm install

# 소스 코드 복사
COPY src/ ./src/
COPY prisma/ ./prisma/  # prisma 폴더도 복사

# prisma 클라이언트 생성
RUN pnpm prisma generate

# 개발 서버 실행
CMD ["pnpm", "run", "dev"]

수정하고 up했을 때 정상실행 완료 후후

아직 클라이언트 쪽에서 데이터를 전송안했을뿐이지。。。 prisma와 nestJS 설정 완료햇다 ㅠㅠㅠㅠㅠ 오늘은 여기까지

이전 캡쳐 화면을 보면 단순히 클라이언트쪽을 만들지 않아서 응답을 반환하고 있지 않다고 생각했으나,

ERR_EMPTY_RESONSE 상태의 원인으로는

  1. 서버가 실행되었지만 요청을 처리 못함
  2. 서버가 응답 보내기 전에 연결이 닫힘
  3. 애플리케이션이 올바르게 구성되지 않음

나는 무조건 1번이라 생각했는데

docker-compose logs api

로 로그를 보더라도 에러가 없었다.

이제 찐 마지막으로 CORS 설정을 위해

apps/api/src/main.ts 코드를 보자면

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  app.enableCors({
    origin: 'http://localhost:3000',
    methods: ['GET', 'POST'],
    allowedHeaders: ['Content-Type'],
  });
  
  app.useGlobalPipes(new ValidationPipe());
  
  await app.listen(3000);
}
bootstrap();

이 코드로 할 시

  • CORS 설정 구체적으로 지정 - 오직 localhost:3000 에서만 API 접근 허용
  • 포트 고정 값 3000 사용
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true,
    transform: true,
  }));
  await app.listen(process.env.PORT ?? 3001);
}
bootstrap();

이 코드는

  • CORS 따로 설정 없음(제한 x)
  • ValidationPipe에 추가 옵션 있음
  • 포트 - 환경 변수 또는 기본 값 3001만 사용

이 두 코드에서 실용적으로 종합해서 사용하면

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

async function bootstrap() {
  const app = await NestFactory.create(AppModule);
  
  // CORS 설정 - Docker 환경에서 중요
  app.enableCors({
    origin: 'http://localhost:3000', // 프론트엔드 주소
    methods: ['GET', 'POST', 'PUT', 'DELETE'],
    allowedHeaders: ['Content-Type', 'Authorization'],
  });
  
  // 유효성 검사 파이프
  app.useGlobalPipes(new ValidationPipe({
    whitelist: true, // DTO에 정의되지 않은 속성 제거
    transform: true,  // 자동 타입 변환
  }));
  
  // 모든 인터페이스에서 리스닝
  await app.listen(3000, '0.0.0.0');
}
bootstrap();

whilelist는 DTO에 정의되지 않은 속성 제거시키고,

transform으로 JSON 데이터 타입 변환 시켜주기

그리고 main.ts는 NestJS애플리케이션의 시작점이라 할 수 있는데

  • 애플리케이션 인스턴스 생성
  • 전역 설정을 구성
  • 서버를 특정 포트에서 실행

즉 애플리케이션을 시작하는 전원 버튼과 같다

import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
  • ValidationPipe - 들어오는 데이터의 유효성 검사 도구
  • NestFactory - NestJS 애플리케이션 만드는 공장
  • AppModule - 애플리케이션의 모든 부분 모아놓은 메인 모듈
async function bootstrap() {

비동기로 서버시작을 처리한다는 느낌으로 bootstrap은 애플리케이션을 부팅한다는 의미로 쓰임

const app = await NestFactory.create(AppModule);

NestFactory.create - NestJS 애플리케이션 인스턴스 생성

AppModule - 애플리케이션의 구조를 정의한 모듈 전달

app.enableCors({
origin: '[http://localhost:3000](http://localhost:3000/)',
methods: ['GET', 'POST'],
allowedHeaders: ['Content-Type'],
});
  • enableCors - 다른 도메인(ex-프론트엔드)에서 API 접근할 수 있게 해줌
  • origin - 어떤 웹 사이트가 API를 호출할 수 있는지 지정
  • methods - 허용할 HTTP 메서드(GET,POST만 허용)
  • allowedHeaders - 허용할 HTTP 헤더(Content-Type만 허용)
app.useGlobalPipes(new ValidationPipe());
  • useGlobalPipes - 모든 요청에 적용할 파이프(데이터 처리 도구)를 설정
  • ValidationPipe - 클라이언트가 보낸 데이터가 정의한 규칙인 DTO에 맞는지 확인
await app.listen(3000);

컨테이너 내부에서 3000포트에서 시행된다는 의미

app.useGlobalPipes(new ValidationPipe({
  whitelist: true, // DTO에 정의되지 않은 속성은 자동으로 제거
  transform: true,  // 자동으로 타입 변환 활성화
}));

앞에 말했던 부분 한 번 더 리마인드 하자면

  • whitelist: true

DTO에 없는 속성이 요청에 포함되면 자동 제거

ex- DTO에 name과 age만 있는데, 요청에 secret도 있으면 secret 제거시킴

  • transform:true

문자열 등을 적절한 타입으로 자동 변환

await app.listen(process.env.PORT ?? 3001);
  • process.env.PORT - 환경 변수에서 포트 번호 읽음
  • ?? 3001: 환경 변수 없으면 3001 기본 값
await app.listen(3000, '0.0.0.0');
  • 0.0.0.0

모든 네트워크 인터페이스에서 연결을 받겠다는 의미

Docker에선 이 설정이 중요. 이게 없으면 컨테이너 외부에서 접근 불가

즉 main.ts는 NestJS 애플리케이션 설정하고 시작하는 파일

  1. 애플리케이션 인스턴스 생성
  2. 데이터 검증, CORS등의 전역 설정
  3. 서버 시작 및 포트 지정
profile
웹 개발자 되고 시포용

0개의 댓글