데이터베이스에 저장하고 저장한 데이터를 읽어와서 수정까지 진행
productCategory.module.ts, productCategory.resolver.ts, productCategory.service.ts(핵심로직) 파일 총 3개 필요
// productCategory.module.ts
import { Module } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import { ProductCategory } from './entities/productCategory.entity';
import { ProductCategoryResolver } from './productCategory.resolver';
import { ProductCategoryService } from './productCategory.service';
@Module({
imports: [TypeOrmModule.forFeature([ProductCategory])],
providers: [
ProductCategoryResolver, //
ProductCategoryService,
],
})
export class ProductCategoryModule {}
- import 옵션에
TypeOrmModule.forFeature([ProductCategory])
을 추가
TypeOrmModule.forFeature
메서드는 엔티티를 전달.
만약 import 하지 않으면 typeorm을 이용한 데이터베이스 연동이 안됨
// productCategory.resolver.ts
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { ProductCategory } from './entities/productCategory.entity';
import { ProductCategoryService } from './productCategory.service';
@Resolver()
export class ProductCategoryResolver {
constructor(
private readonly productCategoryService: ProductCategoryService,
) {}
@Mutation(() => ProductCategory)
createProductCategory(
@Args('name') name: string, // 이름을 만들겠다는것?
) {
return this.productCategoryService.create({ name }); //구조분해할당처럼
}
}
- ProductCategoryResolver 클래스에 생성자로 비즈니스 로직을 의존성주입
- graphql의
@Mutation
을 사용해서 return 받을 graphql의 결과 타입을 알려줌- graphql의
@Args
을 사용해서 받고싶은 graphql의 인자 타입을 알려줌- 의존성 주입을 해 놓은 비즈니스 로직의 create 메서드를
this.productCategoryService.create()
를 이용하여 연결시켜서 함수를 실행시켜 줌return
: 비즈니스 로직으로부터 받은 결과를 프론트 또는 사용자에게 돌려주게 되는 것
실제 핵심 로직들 즉, 비즈니스 로직들이 존재하는 파일 => service.ts
// productCategory.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ProductCategory } from './entities/productCategory.entity';
@Injectable()
export class ProductCategoryService {
constructor(
@InjectRepository(ProductCategory)
private readonly productCategoryRepository: Repository<ProductCategory>,
) {}
async create({ name }) {
const result = await this.productCategoryRepository.save({ name });
console.log(result); // { name: "전자제품" }
return result;
}
}
- ProductCategoryService 클래스에 생성자에서 Repository를 의존성주입
@InjectRepository
를 사용해서 생성자를 주입private
으로 생성자를 선언하게 되면 인스턴스 생성이 불가능하게 됨 즉 외부에서 접근이 불가능하게 해줌으로써, 사용한 클래스 내부에서만 수정이 가능하고 외부에서는 수정이 불가능 하여 안전하게 사용가능readonly
를 통해 생성자를 선언하면 선언 당시 또는 생성자에서 초기화된 후 값이 변경되지 X , 따라서 readonly까지 작성하게 되면 클래스 내부에서도 수정이 불가능Repository<ProductCategory>
는 productCategory 테이블과 비즈니스 로직을 연동시켜 주는 역활this.productCategoryRepository.save
.create
: 데이터 한 줄 등록.save
: 데이터를 한 줄 등록하고, 입력한 결과를 찾아오기- 우리는 데이터를 한 줄 등록하고 무슨 데이터가 등록되었는지 알려주기 위해
.save
를 사용async ~ await
: 등록( 데이터가 저장 )이 잘 이루어졌다고 응답이 올 때 까지 기다려줘야하기에 사용return
:productCategory.resolver.ts
의this.productCategoryService.save()
로 등록한 데이터 객체를 return 해주게 된 것
// productCategory.entity.ts
import { Field, ObjectType } from '@nestjs/graphql';
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity()
@ObjectType()
export class ProductCategory {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)
id: string;
@Column({ unique: true })
@Field(() => String)
name: string;
}
@ObjectType()
, @Field(() => String)
추가, 이 데코레이터들은 graphql 에게 무슨 타입인지 알려주는 역활@Field(() => String)
부분을 주석처리하면 graphql 에 보이지 않게됨@Column({ unique: true })
옵션 값을 추가에 유일한 값으로 지정각각의 모듈을 최종적으로 app.module.ts 파일로 조립
//app.module.ts
import { ApolloDriver, ApolloDriverConfig } from '@nestjs/apollo';
import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { GraphQLModule } from '@nestjs/graphql';
import { TypeOrmModule } from '@nestjs/typeorm';
import { BoardModule } from './apis/boards/board.module';
import { ProductCategoriesModule } from './apis/productsCategories/productCategories.module';
@Module({
imports: [
BoardModule,
ProductCategoriesModule,// 모듈 추가
ConfigModule.forRoot(),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
autoSchemaFile: 'src/commons/graphql/schema.gql',
}),
TypeOrmModule.forRoot({
type: process.env.DATABASE_TYPE as 'mysql',
host: process.env.DATABASE_HOST,
port: Number(process.env.DATABASE_PORT),
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD,
database: process.env.DATABASE_DATABASE,
entities: [__dirname + '/apis/**/*.entity.*'],
synchronize: true,
logging: true,
}),
],
})
export class AppModule {}
네트워크 간에 데이터를 어떤 식으로 보낼지를 정의한 객체를 정의하기 위해서 DTO 적용
=> createProduct.input.ts
// createProduct.input.ts
import { Field, InputType, Int } from '@nestjs/graphql';
import { Min } from 'class-validator';
@InputType()
export class CreateProductInput {
@Field(() => String)
name: string;
@Field(() => String)
description: string;
@Min(0) //음수 값이 나올수 없기때문에 최소값 설정(Min(0))
@Field(() => Int)
price: number;
}
// product.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product) //프로덕트 entity
private readonly productRepository: Repository<Product>,
) {}
async create({ createProductInput }) {
const result = await this.productRepository.save({
...createProductInput, // 스프레드연산자 사용하기
// 코드 간결의 장점 존재
// 하나하나 직접 나열하는 방식
// name: createProductInput.name,
// description: createProductInput.description,
// price: createProductInput.price,
});
return result
}
}
async ~ await
: SQL Query문으로 변환되어 DB로 들어가 저장될 때까지 기다려 줘야하기에 async ~ await를 사용- 등록된 상품 객체를 result 변수에 담아서 브라우저에 다시 전달해주기 위해서는 함수가 실행되는
product.resolver.ts
파일로 돌아가야하기에return
을 사용
// product.entity.ts
@Entity()
@ObjectType() //graphql에 보여줌
export class Product {
@PrimaryGeneratedColumn('uuid')
@Field(() => String)//graphql에 보여줌
id: string;
@Column()
@Field(() => String)
name: string;
@Column()
@Field(() => String)
description: string;
@Column()
@Field(() => Int)
price: number;
@Column({ default: false }) // 고정값
@Field(() => Boolean)
isSoldout: boolean;
@JoinColumn()
@OneToOne(() => ProductSaleslocation)
@Field(() => ProductSaleslocation)
productSaleslocation: ProductSaleslocation;
@ManyToOne(() => ProductCategory)
@Field(() => ProductCategory)
productCategory: ProductCategory;
@ManyToOne(() => User)
@Field(() => User)
user: User;
@JoinTable()
@ManyToMany(() => ProductTag, (productTags) => productTags.products)
@Field(() => [ProductTag]) //graphql 방식
productTags: ProductTag[]; //typescript 방식
}
- GraphQL을 위한
@ObjectType()
,@Field
추가isSoldOut
컬럼은 판매여부를 기록하는 컬럼이기에, 기본 고정 값( default 값 )을 판매가 되지 않은 상태인 false로 지정하기 위해서@Column({ default: false })
추가
- 데이터를 저장할 때 초기값을 false로 자동으로 등록
- typescript에서는 배열 타입을
ProductTag[]
로 작성해주었지만,
grahql에서는 배열타입을[ProductTag]
로 작성
// product.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/createProduct.input';
import { Product } from './entities/product.entity';
import { ProductService } from './product.service';
@Resolver()
export class ProductResolver {
constructor(private readonly productService: ProductService) {}
@Query(() => Product) //entity 타입
fetchProduct(
@Args('productId') productId: string, //
) {
return this.productService.findOne({ productId }); // 1개 조회
}
@Mutation(() => Product)
createProduct(
@Args('createProductInput') createProductInput: CreateProductInput,
) {
return this.productService.create({ createProductInput });
}
}
@Query
를 import 할 때에는 '@nestjs/graphql' 에서 import 되어야함
‘@nestjs/common’ 에서 import 가 된 것은 아닌지 꼭 확인!- 비즈니스 로직에서 한 개 가져오는
findOne
을 사용
// product.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async findOne({ productId }) {
return await this.productRepository.findOne({ where: { id: productId } });
}
async create({ createProductInput }) {
const result = await this.productRepository.save({
...createProductInput,
// 하나하나 직접 나열하는 방식
// name: createProductInput.name,
// description: createProductInput.description,
// price: createProductInput.price,
});
return result;
}
}
- TypeOrm의
findOne
메서드를 사용해서 product 테이블에 매개변수로 받은 productId 에 해당하는 상품을 1개 조회하는 비즈니스 로직을 추가
findOne
메소드는 조건에 where을 명시해야만 함
{ where: { id: productId } }
: where을 통해 조회하고자 하는 조건
async ~ await
: findOne 이 이루어질 때까지 기다려 줘야함
// product.resolver.ts
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
import { CreateProductInput } from './dto/createProduct.input';
import { Product } from './entities/product.entity';
import { ProductService } from './product.service';
@Resolver()
export class ProductResolver {
constructor(private readonly productService: ProductService) {}
@Query(() => [Product]) // 배열 안에 객체의 타입으로 지정
fetchProducts() {
return this.productService.findAll(); // 리스트 조회
}
@Query(() => Product)
fetchProduct(
@Args('productId') productId: string, //
) {
return this.productService.findOne({ productId });
}
@Mutation(() => Product)
createProduct(
@Args('createProductInput') createProductInput: CreateProductInput,
) {
return this.productService.create({ createProductInput });
}
}
// product.service.ts
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
//Repository 사용을 위해 @InjectRepository() 추가
import { Repository } from 'typeorm';
import { Product } from './entities/product.entity';
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async findAll() {
return await this.productRepository.find();
} //find 메서드를 사용해서 product 테이블의 존재하는 모든 데이터를 조회
async findOne({ productId }) {
return await this.productRepository.findOne({ where: { id: productId } });
}
async create({ createProductInput }) {
const result = await this.productRepository.save({
...createProductInput,
// 하나하나 직접 나열하는 방식
// name: createProductInput.name,
// description: createProductInput.description,
// price: createProductInput.price,
});
return result;
}
}
// updateProduct.input.ts
import { InputType, Field, Int } from '@nestjs/graphql';
import { Min } from 'class-validator';
@InputType()
export class UpdateProductInput {
@Field(() => String, { nullable: true })
name: string;
@Field(() => String, { nullable: true })
description: string;
@Min(0)
@Field(() => Int, { nullable: true })
price: number;
} //아래 방식 추천
- 모든 데이터의 수정이 일어날 수 있지만, 특정 데이터만 수정을 해야할 때도 존재 =>
{ nullable: true }
를 사용하여 해당 값을 꼭 입력하지 않아도 API이 실행이 가능하게 만들어줌
// updateProduct.input.ts
import { InputType, PartialType, OmitType, PickType } from '@nestjs/graphql';
import { CreateProductInput } from './createProduct.input';
@InputType()
export class UpdateProductInput extends PartialType(CreateProductInput) {}
// PickType(CreateProductInput, ["name", "price"])
// OmitType(CreateProductInput, ["description"])
- CreateProductInput => UpdateProductInput로 바꿔주는 방법
번거롭게 다시 한번 나열해서 작성X- extends를 사용하여 CreateProductInput을 상속
GraphQL Type
PartialType
: 모든 컬럼을 선택사항으로 바꿔주는 역할을 하므로, { nullable : true } 와 같은 역할PickType
: 원하는 컬럼만을 뽑아서 사용
- PickType(CreateProductInput, ["name", "price"]) : CreateProductInput의 name, price 컬럼만 사용하겠다는 의미
OmitType
: 원하는 컬럼만 제거하여 사용하고 싶을 때 사용
- OmitType(CreateProductInput, ["description"]) : CreateProductInput의 description 컬럼을 제외한 나머지 컬럼들은 모두 사용
// product.resolver.ts
@Mutation(() => Product)
async updateProduct(
@Args('productId') productId: string,
@Args('updateProductInput') updateProductInput: UpdateProductInput,
) {
return this.productService.update({ productId, updateProductInput });
}
- 어떤 상품이 수정이 되었는지 리턴 받아야하기에 Product 를 리턴 타입으로 지정
@Args
를 사용해 productId 를 받아서 해당 productId 를 가진 product 를 수정 => productid는 수정 조건@Args
를 사용해 updateProductInput 를 받아서 수정하고 싶은 값 들을 수정 => updateProductInput은 수정 대상
// product.service.ts
async update({ productId, updateProductInput }) {
const myproduct = await this.productRepository.findOne({
where: { id: productId },
});
const newProduct = {
...myproduct,
id: productId,
...updateProductInput,
};
return await this.productRepository.save(newProduct);
} // .save 사용
- 타입스크립트의 인터페이스를 사용해서 데이터 수정에 필요한 요청값의 타입의 유효성을 검사
this.productRepository.findOne
: 데이터를 수정하기 위해 DB에서 수정할 데이터를 찾아와야함
- 내가 수정한 컬럼에 대해서만 조회할때는 findOne을 해주지 않아도 되지만, 수정한 컬럼을 포함한 해당 상품의 다른 컬럼들을 함께 조회할 때는 findOne을 통해서 데이터를 먼저 찾아와 주어야 함
- 즉, return 받는 값에 수정한 컬럼을 포함한 다른 컬럼들이 존재해야 한다면 findOne을 먼저 해줘야함
this.productRepository.save
.update
: 데이터를 수정할 때 사용.save
: 데이터를 수정하고, 수정한 결과를 찾아오기 수정할 때 .save 를 사용하기 위해서는 먼저 데이터를 찾아와야지 사용 가능- 데이터를 수정하고 무슨 데이터가 수정 되었는지 프론트에게 알려주기 위해
.save
를 사용
update({ productId, updateProductInput }){
this.productRepository.update( //.update 사용
{ id: productId }, // 무엇에 대해서 (조건)
{ ...updateProductInput } // 무엇을 수정 할 것인지 (대상)
)
return "수정이 완료되었습니다!"
}
먼저 상품이 판매가 되었는지 확인하는 유효성 검사를 했을 때 판매가 완료되었다면 상품 데이터를 수정할 필요가 없으므로, 이때 에러를 반환하는 방법
// product.resolver.ts
@Resolver()
export class ProductResolver {
constructor(private readonly productService: ProductService) {}
@Mutation(() => Product)
async updateProduct(
@Args('productId') productId: string,
@Args('updateProductInput') updateProductInput: UpdateProductInput,
) {
// 판매 완료가 되었는지 확인해보기
await this.productService.checkSoldout({ productId });
// 수정하기
return await this.productService.update({ productId, updateProductInput });
}
}
// product.service.ts
@Injectable()
export class ProductService {
constructor(
@InjectRepository(Product)
private readonly productRepository: Repository<Product>,
) {}
async checkSoldout({ productId }) {
const product = await this.productRepository.findOne({
where: { id: productId },
});
if (product.isSoldout)
throw new UnprocessableEntityException('이미 판매 완료된 상품입니다.');
// if (product.isSoldout) {
// throw new HttpException(
// '이미 판매 완료된 상품입니다.',
// HttpStatus.UNPROCESSABLE_ENTITY,
// );
// }
}
- productRepository 에서 해당 product 데이터를 찾아온 뒤, 그 상품이 이미 판매가 된 상품이라면
throw new UnprocessableEntityException
을 통해 에러를 만들어서 프론트로 에러메세지를 전달throw new HttpException(에러메세지, 에러상태코드)
: 비즈니스 로직을 작성할 때 발생할 일들에 대해 예측할 수 있는 에러들, 기획에 맞지 않는 에러들이 발생하는 경우 처리할 수 없음을 나타내는 에러를 만들때 사용- 422 에러상태코드 대신에, 조금 더 보기 쉽게 사용할 수 있도록 NestJS에서 제공해주는
HttpStatus.UNPROCESSABLE_ENTITY
을 사용
❗️상태코드
- 200 번대 : 성공 상태 코드
- 400 번대 : 프론트에서 요청을 잘못했을 때 발생하는 에러 상태 코드
- 500 번대 : 백엔드 서버에서 문제가 생겼을 때 발생하는 에러 상태코드
예기치 못한 일에 대한 에러 방지
try
문 안에서 에러가 발생시, 에러 발생 시점부터 즉시 중단되어 아래 로직들은 실행이 되지않고 바로 catch
문으로 넘어감catch
문 안에서는 console.log
를 통해 어떤 에러가 발생했는지 확인해 볼 수 있으며, throw error
를 통해 프론트로 에러를 반환// product.service.ts
async create({ createProductInput }: ICreate) {
try {
await this.productRepository.save({
...createProductInput, // 스프레드연산자 사용하기
});
console.log('안녕하세요!!')
console.log('안녕하세요!!')
console.log('안녕하세요!!')
} catch(error) { //에러반환
throw error
console.log(error)
}
}
try ~ catch 문을 하나하나 작성하지 않아도 모든 에러는 ExceptionFilter
로 자동 들어옴
// http-exception.filter.ts
import { Catch, ExceptionFilter, HttpException } from '@nestjs/common';
@Catch(HttpException)
export class HttpExceptionFilter implements ExceptionFilter {
catch(exception: HttpException) {
const status = exception.getStatus();
const message = exception.message;
console.log('===========================');
console.log('예외가 발생했어요!!');
console.log('예외내용:', message);
console.log('예외코드:', status);
console.log('===========================');
}
}
// main.ts
import { ValidationPipe } from '@nestjs/common';
import { NestFactory } from '@nestjs/core';
import { AppModule } from './app.module';
import { HttpExceptionFilter } from './commons/filter/http-exception.filter';
async function bootstrap() {
const app = await NestFactory.create(AppModule);
app.useGlobalPipes(new ValidationPipe());
app.useGlobalFilters(new HttpExceptionFilter()); //삽입
await app.listen(3000);
}
bootstrap();
@Catch(HttpException)
: 에러 관련된 내용이 들어가 있음을 NestJS 에게 알려주기 위한 데코레이터를 사용catch
: Exception 상황 발생시 비즈니스 로직에 try ~ catch 문이 없더라도 자동으로 에러가 catch 문으로 들어옴exception: HttpException
: HttpException type 으로 정의implements ExceptionFilter
: class 에서 타입을 정의 해주기 위해 implements 사용ExceptionFilter
: 인터페이스로 catch 함수가 반드시 존재해야하는 타입 => catch 문을 작성하지 않으면 에러가 발생exception.getStatus()
: 에러가 발생하면 나타나는 상태코드를 가져옴exception.message
: 에러가 발생시 나타나는 에러 메세지