[프로젝트 로그] 1-2. 개발환경 설정 - 백엔드편

이승준·2023년 2월 24일
0
post-thumbnail

🗂️ 디렉토리 구조

지난번에 Monorepo 설정을 이후로 백엔드 설정을 준비해야합니다. 그러기 위해서 현재 진행할 줍줍이라는 프로젝트의 디렉토리 구조를 먼저 간단하게 정의해보죠.

mono_velog
|- /apps
   |- /zoop_frontend
   |- /zoop_backend
   |- /shared
   |- .env
   |- docker-compose.yml

앞서 모노레포 설정을 다룰 때, mono_velog/apps/**의 프로젝트들은 package로서 다뤄지게 설정을 해뒀습니다.
그래서 zoop_frontend, zoop_backend 는 각각 nextJs, nestJs 프로젝트로서 하나의 패키지가 될 예정이죠.

shared 디렉토리에는 데이터베이스와 관련된 도커파일과 백엔드에서 ORM으로 사용할 Prisma의 schema.prisma 파일을 둘 예정입니다.

.env 파일은 데이터베이스 유저 정보와 데이터베이스 URL같은 여러 프로젝트에서 사용할 정보들을 담아두겠습니다.

docker-compose.yml 도커로 컨테이너화 시킬 여러 프로젝트들을 일괄적으로 관리 및 실행시키기 위해 apps/ 디렉토리에 생성하겠습니다.

일단은 백엔드 환경부터 다룰 예정이기 때문에, zoop_frontend폴더는 다음에 만들도록 하겠습니다.

🧹 기술 스택에 대한 짧은 정리

1. NestJs

NestJs는 서버 어플리케이션을 구축하기 위한 Node.js 기반 프레임워크입니다. Express에서 자주 사용하는 시스템들을 쉽게 이용할 수 있도록 각종 모듈들을 지원합니다.

2. Prisma

Prisma는 ORM(Object Relational Mapping)의 한 종류입니다.
여기서 ORM은 데이터베이스의 Schema를 객체로 매핑을 해주는 것인데요, NestJs에서 CRUD를 구현하기 위해 별도의 데이터베이스 명령어를 사용할 필요가 없다는 편의성을 제공합니다.
또, schema.prisma를 통해 각 모델을 정의하고, prisma generate 명령어를 통해 해당 schema 파일을 기반으로 NestJs에서 사용할 수 있는 메서드 및 타입에 대한 정의들을 제공합니다. 따라서 개발 과정에 있어서도 상당한 편의를 제공해주죠.

3. Docker

Docker는 이미지화된 어플리케이션을 컨테이너에서 실행시킬 수 있게 해주는 가상화 플랫폼인데요, 여기서 어플리케이션의 Dockerfile을 통해 이미지를 만듭니다.

Docker 이미지 안에는 어플리케이션을 실행시키기 위한 파일들을 담게되어, 도커의 컨테이너에서 실행시킬 수 있습니다. 덕분에 OS X에서 개발한 어플리케이션을 Linux 환경에 베포해도 결국 Docker의 컨테이너에서 작동하는 것이기 때문에 큰 문제없이 작동합니다.

🐳 Docker와 함께 PostgreSQL, Prisma 설정

Prisma, schema 설정

본격적으로 데이터베이스에 대한 설정을 마치기 전에, 데이터베이스에 생성될 모델들에 대한 정의를 schema.prisma파일에 작성해줘야 합니다.

그러기 위해선 Zoop Zoop을 위한 모델들을 설계할 필요가 있겠죠.

우선 제가 생각한 ZoopZoop을 위한 모델들은 다음과 같습니다:

User

model User {
    id           Int          @id @default(autoincrement())
    email        String       @unique
    username     String
    refreshToken String?
    expressions  Expression[]
}

우선 Zoop에서는 OAuth 기반의 로그인만 지원하여 사용자의 회원가입에서 불필요한 고민을 줄일 예정입니다. 여기서 추가로 호칭을 위한 username을 추가로 입력받아 데이터베이스에 저장합니다.

그리고 refreshToken을 저장해서 token이 refresh가 필요할 때 검증 후 Update하도록 구현할 예정입니다.

그리고 각 User는 각자가 주운 표현, Expression[]을 생성할 수 있습니다.

Expression

model Expression {
    id     Int  @id @default(autoincrement())
    userId Int
    user   User @relation(fields: [userId], references: [id])

    content   String
    meaning   String?
    media     String?
    category  Category? @default(en)
    createdAt DateTime  @default(now())
    updatedAt DateTime  @updatedAt

    cases Case[]

    userNyam UserNyam[]
}

Expression은 사용자가 줍줍한 표현에 대한 데이터입니다. 각 표현은 어떠한 표현을 주웠는지에 대한 content, 그 표현에 대한 뜻 meaning를 가장 기본적으로 가지게 됩니다.

추가적으로 있으면 좋겠다 생각한 정보들은
media 어떤 매체를 통해 해당 표현을 주웠는지.
category 어떠한 언어 종류인지, (영어, 일본어, 스페인어, 중국어, 여자어(?) 등)
정도가 되겠네요.

여기서 category필드의 타입으로 쓰인 Categoryenum 타입으로 맨 아래에서 정의하도록 하겠습니다.

createdAt은 오늘 줍줍한 표현을 필터하고, 나중에 구현하고싶은 data visualization파트에서 사용될 수 있을것 같습니다.

cases는 유저가 줍줍한 표현을 실생활에서 써먹은 문장과 뜻을 담기위한 필드입니다. 아래에서 Case모델을 생성할 예정입니다.

userNyam은 여태 주워들은 표현들을 대상으로 "오늘 표현을 익히겠다!" 라고 생각해서 선택받은 표현들과, 그 케이스를 저장할 model입니다. 마찬가지로 아래에서 구현 예정입니다.

위쪽의 userId, userUser모델과의 관계를 위해 정의된 필드입니다.

Case

model Case {
    id           Int        @id @default(autoincrement())
    expressionId Int
    expression   Expression @relation(fields: [expressionId], references: [id])

    content String
    meaning String

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    userNyam UserNyam[]
}

Case 모델입니다. 마찬가지로 중요한 필드는 content, meaning입니다. 각각 User가 주운 표현을 써먹은 문장과 그 문장의 의미를 나타냅니다.

그 외의 정보는 Expression모델과의 관계, UserNyam모델과의 관계를 위해 사용됩니다.

UserNyam

model UserNyam {
    id           Int        @id @default(autoincrement())
    expressionId Int
    expression   Expression @relation(fields: [expressionId], references: [id])
    caseId       Int?
    case         Case?      @relation(fields: [caseId], references: [id])

    createdAt DateTime @default(now())
}

UserNyam 모델입니다. Expression과의 관계를 메인으로 해서 생성되고, Case 데이터가 연결되면 해당 도전은 성공으로 판단됩니다.

Category, enum 데이터 타입

enum Category {
    en
    jp
    tr
    wo
}

저는 Categroy enum을 위와같이 정의했는데요, en은 영어, jp는 일본어, tr은 유행어, wo 여자어(?) 정도로 간단하게 정의내렸습니다.

이제 밑에서 다룰 datasource와 generator를 적절하게 설정해주면, prisma generate 명령어를 통해 프로젝트 node_modules@prisma/client라는 모듈을 추가할 수 있게 됩니다.
해당 모듈에는 schema.prisma에서 정의내린 model에 대한 object들을 타입을 담고 있기에, 개발 과정을 편하게 만들어줍니다.

Enum 데이터 타입의 지양

최근에 알게된 사실인데, typescript에서는 enum 타입의 사용을 지양하고 있다고 합니다.

이는 javascript에 존재하지 않는 문법임에도 불구하고 번들러에서 Tree shaking이 되지 않기 때문인데요, 그 때문에 객체에 as const를 적용하여 Union 타입을 추출해 사용하는것을 권장한다고 합니다.

이와 관련해서는 다른 포스팅에서 자세히 다뤄보도록 하겠습니다.

datasource와 generator

datasource는 매핑할 데이터베이스의 정보를 의미합니다. 저는 postgresql을 사용할 예정이고, Database URL은 env파일에 저장된 url을 불러오도록 하겠습니다.

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

generator는 앞서 prisma generate 명령어를 통해 생성될 Prisma Client를 생성하기 위한 설정입니다.

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

기본적으로 prisma-client-js를 provider로 사용하고, 직접 구현한 provider를 사용할 수도 있습니다.

현재 generator의 기본적인 설정은 위와같이 했지만, 두가지 옵션을 더 가집니다.

generator client {
    provider      = "prisma-client-js"
  	output        = "node_modules/@prisma/client"
    binaryTargets = ["native"]
}

output은 client 모듈이 생성될 경로입니다. 저는 모노레포를 통해 다른 디렉토리에 schema.prisma파일이 있으므로 재설정의 필요성도 보입니다.

binaryTargets는 prisma client와 OS와의 호환을 위해 어플리케이션이 돌아갈 OS를 지정해줍니다. 다만 native라는 값으로 지정하면 prisma가 알아서 지정한다고 합니다.

위에 작성된 output, binaryTargets의 값은 모두 default값으로, 굳이 위처럼 작성하지 않아도 됩니다.

Prisma 최종

model, datasource, generator에 대한 설정을 마친schema.prisma 파일은 다음과 같습니다:

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

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

model User {
    id           Int          @id @default(autoincrement())
    email        String       @unique
    username     String
    refreshToken String?
    expressions  Expression[]
}

model Expression {
    id     Int  @id @default(autoincrement())
    userId Int
    user   User @relation(fields: [userId], references: [id])

    content   String
    meaning   String?
    media     String?
    category  Category? @default(en)
    createdAt DateTime  @default(now())
    updatedAt DateTime  @updatedAt

    cases Case[]

    userNyam UserNyam[]
}

model Case {
    id           Int        @id @default(autoincrement())
    expressionId Int
    expression   Expression @relation(fields: [expressionId], references: [id])

    content String
    meaning String

    createdAt DateTime @default(now())
    updatedAt DateTime @updatedAt

    userNyam UserNyam[]
}

model UserNyam {
    id           Int        @id @default(autoincrement())
    expressionId Int
    expression   Expression @relation(fields: [expressionId], references: [id])
    caseId       Int?
    case         Case?      @relation(fields: [caseId], references: [id])

    createdAt DateTime @default(now())
}

enum Category {
    en
    jp
    tr
    wo
}

PostgreSQL 설정(Dockerfile)

PostgreSQL은 Docker의 postgres 이미지를 이용하여 컨테이너로 이용할 예정입니다.

단, container 안에서 prisma의 원활한 사용을 위해 postgres이미지에 prisma를 설치합니다. 그리고 환경변수로 env파일에 있는 데이터베이스 URL 정보를 넘기고, schema.prisma를 컨테이너로 복제하는 과정까지 Dockerfile에 작성하도록 하겠습니다.

FROM postgres:latest 

# Install prisma
RUN apt-get update && apt-get install -y curl
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get install -y nodejs
RUN npm install -g prisma

# Set environment variable
ENV DATABASE_URL={DATABASE_URL}

# Copy schema.prisma.
COPY schema.prisma ./

우선 postgres 이미지를 베이스 이미지로 사용하고, 컨테이너 내부에 prisma를 전역으로 설치하는 코드를 실행시킵니다.

다음으로는 컨테이너 내부의 DATABASE_URL 환경변수를 env 파일의 DATABASE_URL 값으로 설정하고, 같은 폴더(docker build context 기준)에 있는 schema.prisma파일을 컨테이너 내부로 복제해줍니다.

build context

여기서 docker build context 기준으로 같은 폴더에 있다고 했습니다.
docker에서는 context를 기준으로 복사할 파일을 설정할 수 있습니다.

가령 부모 디렉토리에 있는 파일이나. 형제 디렉토리에 있는 파일을 컨테이너로 복사하고 싶을 때, build context를 설정해줌으로써 복사를 가능하게 할 수 있죠.

🌅 마무리

이렇게 Prisma와 PostgreSQL의 Dockerfile 설정을 완료했습니다.
다음시간에는 zoop_backend에 nestjs를 설치하고 Dockerfiledocker-compose.yml 파일을 작성하여 데이터베이스와 같이 도커로 베포하는 과정을 다루겠습니다.

잘못된 정보나 지식, 오탈자 제보는 언제든지 환영합니다 😎

profile
인터랙티브 웹부터 풀스택, web3 등 다양한 분야에 관심을 가지고 다양한 일을 이루기위한 수단으로서 개발합니다.

0개의 댓글