우선 SQL은 들어봤어도 SDL은 또 뭔지 처음 들어보고 헷갈렸었다.. GraphQL을 접하기 전까진 익숙치 않은 개념이었다.
가장 먼저 스키마Schema
는 필드를 포함하는 객체 유형의 모음이다.(데이터 타입의 집합) 각 필드에는 고유한 유형은 GraphQL
에서 스칼라 유형이거나 다른 객체 유형일 수 있다.
type Student {
_id: String,
FirstName: String,
LastName: String,
Standard: Int,
}
type Query {
getAllStudents:[Student],
getStudentById(id:String):Student
}
필드 이름은(camelCase), 콜론, 필드 유형(스칼라 또는 객체)로 선언된다. 필드가 null이 아니어야 하는 경우 해당 유형 뒤에 항상 !
를 추가한다.
이렇게 스키마를 정의하면, 스키마의 정의는 API 문서같은 역할을 하며, 백엔드 개발자는 어떤 데이터를 전달해야하는지, 프론트엔드 개발자는 인터페이스 작업 시 필요한 데이터를 정의할 수 있다.
이 두개념은 디자인 방법론 중의 하나이다. Schema First Developmeent
는 개발 시 스키마를 우선으로 개발한다.
스키마 우선 접근 방식에서의 진실된 소스는 GraqhQL SDL(Schema Definision Language) 파일이다.
모든 프로그래밍 언어와 독립적이며, 통합되는 언어이고, NestJS
에서는 GraphQL 스키마를 TypeScript
의 클래스 및 인터페이스 형식으로 구현한다.
type Student {
_id: String,
FirstName: String,
LastName: String,
Standard: Int,
}
type Query {
getAllStudents:[Student],
getStudentById(id:String):Student
}
특히, 스키마 우선 작업방식은 .gql
혹은 .graphql
파일을 생성하여 그곳에서 작성해준다.
TypeScript
로만 작성하고, 문법 간 Context Switch를 피하는 경우에 사용한다. GraphQL의 스키마를 자동으로 생성하는 Decorater
를 사용한다.
import { Field, ID, Int, ObjectType } from '@nestjs/graphql';
@ObjectType()
export class Student {
@Field(() => ID)
_id: string,
@Field()
FirstName: String,
@Field()
LastName: String,
@Field(() => Int)
Standard: Int,
}
student.resolver || controller.ts
@Resolver(() => Student)
export class StudentResolver {
constructor(private readonly studentService: StudentService) {}
@Query(() => Student, { name: 'student', nullable: true })
getStudent(@Args() getStudent: GetStudent): Student {
return this.usersService.getStudent(getStudent);
}
NestJS 프로젝트를 시작했을때, 가장 기본적으로 생성되는 파일/폴더의 구조이다.
프로젝트의 시작점이며, 서버를 실행하는 코드를 가진다.
핵심중 하나이며, NestFactory
를 사용하여 Nest 애플리케이션 인스턴스를 생성하는 엔트리 파일이다.
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
await app.listen(3000);
}
bootstrap();
bootstrap() 라는 기본 비동기 함수는 await인 AppModule을 호출하며, 이 어플리케이션에 포트 3000
을 리스닝 한다.
함수 이름은 임의로 지정할 수 있다.
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 {}
AppModule은Class
형태라는 것을 파악할 수있다.
여기서 @Module()
라는 데코레이터를 사용해주었다. 이는 Nest 애플리케이션의 구조를 구성하는데 사용하는 메타데이터를 제공하며, 반드시 필요한 데코레이터이다.
Graphql
의 URL이 /graphql
로 하나의 URL로 묶여서 동작하는 것처럼, NestJS
의 각각의 모듈들은 최종적으로 하나로 취합한다.
@Module({
imports: [
//환경변수 설정
ConfigModule.forRoot({
isGlobal: true,
ignoreEnvFile: true,
validationSchema: Joi.object({
NODE_ENV: Joi.string(),
}),
}),
//typeOrm, 데이터베이스 모듈
TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.MYSQL_HOST,
port: +process.env.MYSQL_PORT,
username: process.env.MYSQL_USER,
password: process.env.MYSQL_PASS,
database: process.env.MYSQL_DB,
synchronize: true,
logging: false,
entities: [USER_TB],
}),
GraphQLModule.forRoot({
playground: true,
autoSchemaFile: true,
}),
AuthModule,
UserModule,
CommonModule,
PostModule,
],
controllers: [],
providers: [CommonService, PostService, PostResolver],
})
export class AppModule {}
결국 모든 모듈을 활용하기 위해선 루트 모듈인 App.module
에 통합하며, main.ts
에서 NestFactory를 통해 생성된 app이 실행된다.
Provider은 Nest의 기본 개념이고, 대부분의 기본 Nest Class인services
, repositories
, factories
, helpers
등이 provider
로 취급될 수 있다.
Provider
구분하는 방법은 쉬운데 단순히 @Injectable()
데코레이터가 달린 클래스를 Provider
라고한다.
AppService
라는 클래스는 Provider
이다.
import { Injectable } from '@nestjs/common';
@Injectable()
export class AppService {
getHello(): string {
return 'Hello World!';
}
}
Imports
: 이 모듈에 필요한 프로바이더를 Provider
: @Injectable()
데코레이터가 달린 클래스는 모두 Provider
이라고 하는데, Nest injector에 의해 인스턴스화되고, 현재 모듈에서 공유될 Provider
의 집합이다.imports
: 현재 모듈에서 필요한 Provider
들을 내보내는 import
로 가져온 모듈 목록이다.exports
: 이 모듈에서 제공하며, 이 모듈을 import
하는 다른 모듈에서 사용할 수 있게 해주는 Provider
의 하위집합이다.controller
: 인스턴스화 해야하는, 현재 모듈에 정의된 Controller의 집합모듈은 기본적으로 프로바이더를 캡슐화
하기때문에, providers
에 등록하지 않았거나import
로 가져온 모듈에서 export
하지 않는 Provider
를 Inject
하는 행위는 불가능하다.
해당 파일은 GraphQL이 각 요청을 수행하는 코드를 작성하는 파일이다.
GraphQL
에서 CRUD
를 담당하는 @Query()
와 @Mutation()
데코레이터를 작성하여 해당 요청에 대한 응답을 처리한다.
@Resolver((of) => User)
export class UserResolver {
constructor(private readonly userService: UserService) {}
@Query(() => InputDTO)
async action1(@Args('data') id:number){
return await this.userService.getID(id)
}
@Mutation(() => OutPutDTO2)
async action1(@Args('data') createData:CreaeDataInputDTO){
return await this.userService.action2(createData)
}
}
실제 비즈니스 로직이 작성된 Service
파일에 접근하기 위해서는 constructor
함수의 매개변수로 넣어주어야 한다.
이후 클래스 내부에 작성된 각 함수들에선 this
키워들르 통해 service
에 접근이 가능하다.
@Query()
Query
는 CRUD의 READ를 수행하며, 우리가 입력한 어떠한 객체를 반환할 것을 명시한다. 아래의 코드의 경우 InputDTO에 작성된 객체들을 반환한다.
@Query(() => InputDTO)
async action1(@Args('data') id:number){
return await this.userService.action1(id)
}
플레이그라운드를 통해 확인할 수 있다.
query {
action1(id:1) {
id
username
email
}
}
@Mutation()
Mutation
은 CRUD의 CUD를 수행하며, 어떠한 정보를 입력, 수정, 삭제를 요청하며, 그에 따른 결과를 응답받을 수 있다.
@Mutation(() => OutPutDTO2)
async action1(@Args('data') createData:CreaeDataInputDTO){
return await this.userService.action2(createData)
}
플레이그라운드를 통해 확인할 수 있다.
mutation {
action1(data:{ //입력할 내용 작성
title: 'sdfasdfasdf'
content: '23213123213'
}) {
isWrite //입력에 따른 응답 작성(OutPutDTO2에 작성되었다면)
}
}
이곳이 실제로 데이터 저장 및 검색을 담당하는 파일이며, 주로 이런 데이터처리를 수행하는 것을 비즈니스 로직이라고 부른다.
DB의 각 테이블에 접근할수있도록 여기에 가져와 사용한다.
@Injectable()
export class UserServie {
constructor(
@InjectRepository(USER_TB)
private readonly uesrRepository: Repository<USER_TB>,
) {}
async createUser(user: USER_TB) {
try {
return await this.uesrRepository.save(this.uesrRepository.create(user));
} catch (error) {
console.log('에러:', error);
}
}
}
@Injectable()
는 위에서 설명한 것과같이 Moudule
파일에서 Providers
로 활용하기 위해서 이 데코레이터로 정의해준다.
@InjectRepository()
는 TypeORM.module
의 forFeature()
메서드를 통해 Import
받으면, Module
에 종속된다.
@Module({
imports: [TypeOrmModule.forFeature([USER_TB])],
providers: [UserServie],
exports: [],
})
export class UserModule {}
이제 Service
에서 @InjectRepository
를 통해 저장소를 정의하고 사용할 수 있는 저장소 패턴을 지원한다. service
에서는 당연히 비즈니스 로직, 데이터의 저장 수정 삭제 등등을 처리해야하므로 당연히 DB에 접근해야하기 때문에 필요한 부분만 가져와 사용하면 될 것 같다.