GraphQL API를 만들어보자!!!!
yarn add apollo-server graphql
Database를 연결한 index.ts 파일에 아래의 코드를 추가한다.
🚨 database를 연결하는 코드를 지우지 말고, import문 아래 나머지 코드 위에 추가한다.
index.ts
~~~
데이터베이스 연결하는 코드
~~~
import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';
// API Docs 만들기
const typeDefs = `#graphql
type Query {
hello: String
}
`;
// API 만들기
const resolvers = {
Query: {
// 해당 api를 요청하면 아래의 함수가 실행됩니다.
hello: () => 'world',
},
};
// ApolloServer를 생성합니다.
// 위에서 만든 Docs(typeDefs)와 API(resolvers)를 넣어줍니다.
const server = new ApolloServer({
typeDefs,
resolvers,
});
// 서버를 열어두고 접속을 기다립니다.
startStandaloneServer(server).then(() => {
console.log(`🚀 GraphQL 서버가 실행되었습니다.`); // port는 4000입니다.
});
~~~
데이터베이스 연결하는 코드
~~~
데이터베이스가 연결되고 나서 서버를 실행하도록 startStandaloneServer~~~
코드의 위치를 아래와 같이 옮겨준다.
AppDataSource.initialize()
.then(() => {
console.log("DB 연결 성공!");
startStandaloneServer(server).then(() => {
console.log(`🚀 GraphQL 서버가 실행되었습니다.`); // port: 4000
});
})
.catch((error) => console.log(error, "DB 연결 실패ㅜ"));
서버 열기 완!
Board.postgres.ts
)가 BaseEntity를 상속받고 있어야 insert, find 등을 사용할 수 있다.Board.postgres.ts
//Board.postgres.ts 파일에서 BaseEntity를 입력했는지 확인한다.
import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from "typeorm";
@Entity()
export class Board extends BaseEntity{
@PrimaryGeneratedColumn("increment")
number!: number;
@Column({ type: "text" })
wrtier!: string;
@Column({ type: "text" })
title!: string;
@Column({ type: "text" })
contents!: string;
}
- @: 데코레이터 함수, typeORM에게 테이블임을 알려준다.
- extends BaseEntity: 추가/삭제하는 기능을 포함한 데이터베이스 테이블이 된다.
{ type: "text" }
: postgres database의 column타입을 넣어준다.
int, text ...- PrimaryGeneratedColumn("increment"): 중복되지 않는 자동으로 증가하는 컬럼
- PrimaryGeneratedColumn("uuid"): universial unique id(중복되지 않는 고유한 아이디가 자동으로 만들어진다.)
resolvers 안에서 API를 만들어줍니다.
// resolvers
const resolvers = {
Query: {
fetchBoards: async () => {
const result = await Board.find();
console.log(result);
return result;
},
},
};
위쪽에 있는 typeDefs에서는 API-DOCS를 만들어줍니다.
# typeDefs
const typeDefs = `#graphql
# 객체의 타입을 지정해줍니다.
type MyBoard {
writer: String
number: Int
title: String
contents: String
}
type Query {
# 결과가 여러개이므로 배열에 담아서 보내줍니다.
# GraphQL에서는 배열 안의 객체를 [객체]로 표기합니다.
fetchBoards: [MyBoard]
}
`;
typeDefs(API type) 작성
index.ts
gql에는 graphQL 타입을 적어주고,
Boards.postgres.ts
Entity를 정의할 때는 TypeScript의 타입을 적어준다.
한 개만 조회할 때는 findOne()을 사용하고, 조건을 추가해주셔야 합니다.
// resolvers
const resolvers = {
Query: {
fetchBoard: async () => {
const resultOne = await Board.findOne({
where: { number: 3 },
});
return resultOne;
},
},
};
typeDefs에 API-DOCS도 만들어주세요!
조회를 제외한 create, update, delete는 Mutation 안에 작성해줍니다.
args를 하나의 객체(createBoardInput)으로 묶어서 받아오는 실무용 방식을 사용합니다.
// resolvers
const resolvers = {
Mutation: {
// parent vs args: 브라우저의 요청은 args로 인자를 받고, 다른 api의 요청은 parent로 인자를 받습니다.
createBoard: async (parent: any, args: any, context: any, info: any) => {
await Board.insert({
/* 1. 연습용(backend-example 방식) */
// writer: args.writer,
// title: args.title,
// contents: args.contents,
/* 2. 실무용(backend-practice 방식) */
...args.createBoardInput,
});
return "게시글 등록에 성공했습니다.";
},
},
};
🚨 인자로 들어가는 객체의 타입은 type이 아닌 input으로 작성합니다.
# typeDefs
const typeDefs = `#graphql
# 인자로 들어가는 객체의 타입은 type이 아닌 input으로 작성합니다.
input CreateBoardInput {
writer: String
title: String
contents: String
}
type Mutation {
# GraphQL에서는 문자열의 타입을 String으로 입력해주셔야 합니다.
# 필수값인 경우에는 콜론(:) 앞에 !를 붙여주세요.
# 1. 연습용(backend-example 방식)
# createBoard(writer: String, title: String, contents: String): String
# 2. 실무용(backend-practice 방식)
createBoard(createBoardInput: CreateBoardInput): String
}
`;
update(조건, 수정할 내용)
조건과 수정할 내용을 순서에 맞게 작성합니다.
// resolvers
const resolvers = {
Mutation: {
updateBoard: async () => {
// update(조건, 수정할내용)
// 👇🏻 3번 게시글을 영희로 바꿔줘!
await Board.update({ number: 3 }, { writer: "영희" });
},
},
};
typeDefs에 API-DOCS도 만들어주세요!
삭제하기 delete(삭제할 조건)
가 있지만 실무에서는 사용하지 않습니다. 참고만 해주세요!
// resolvers
const resolvers = {
Mutation: {
deleteBoard: async () => {
// delete(삭제할 조건)
// 👇🏻 3번 게시글을 삭제해줘!
await Board.delete({ number: 3 });
},
},
};
실무에서 사용하는 삭제 방법입니다.
data를 삭제하면 돌이킬 수 없기 때문에 실수 방지 및 추후 데이터가 필요할 상황을 대비하여
실무에서는 실제로 데이터를 지우는 경우가 거의 없습니다.
대신에 Soft Delete라고 불리는 방식을 사용합니다.
deletedAt
)을 만들고, update를 이용해 삭제 여부를 입력합니다.IsNull()
)만 조회한다는 조건을 추가해야 합니다.// resolvers
const resolvers = {
Query: {
fetchBoards: async () => {
const result = await Board.find({ where: { deletedAt: IsNull() } });
return result;
},
},
Mutation: {
deleteBoard: async () => {
await Board.update({ number: 3 }, { deletedAt: new @Column({ type: "timestamp", default: null, nullable: true })
deletedAt?: Date;Date() });
},
},
};
typeDefs에 API-DOCS도 만들어주세요!
Column을 추가해줍니다.
// Board.postgres.ts
import { column, Entity, primaryGeneratedColum, BaseEntity } from "typorm"
@Entity()
export class Board extends BaseEntity{
~~~
@Column({ type: "timestamp", default: null, nullable: true })
deletedAt?: Date;
}
"dev": "ts-node-dev index.ts"
yarn dev
전체 코드
class_backend/index.ts
import { ApolloServer } from "@apollo/server";
import { startStandaloneServer } from "@apollo/server/standalone";
// API-DOCS 만들기
const typeDefs = `#graphql
# 인자로 들어가는 객체의 타입은 type이 아닌 input으로 작성합니다.
input CreateBoardInput {
writer: String
title: String
contents: String
}
type MyBoard {
writer: String
number: Int
title: String
contents: String
deletedAt: String
}
type Query {
# 결과가 여러개이므로 배열에 담아서 보내줍니다.
# GraphQL에서는 배열 안의 객체를 [객체]로 표기합니다.
fetchBoards: [MyBoard]
}
type Mutation {
# GraphQL에서는 문자열의 타입을 String으로 입력해주셔야 합니다.
# 필수값인 경우에는 콜론(:) 앞에 !를 붙여주세요.
# 1. 연습용(backend-example 방식)
# createBoard(writer: String, title: String, contents: String): String
# 2. 실무용(backend-practice 방식)
createBoard(createBoardInput: CreateBoardInput): String
deleteBoard(number: Int): String
}
`;
// API 만들기
const resolvers = {
Query: {
fetchBoards: async () => {
/* 1. 모두 꺼내기 */
const result = await Board.find({ where: { deletedAt: IsNull() } });
console.log(result);
return result;
/* 2. 한 개만 꺼내기 */
// const resultOne = await Board.findOne({
// where: { number: 3 },
// });
// return resultOne;
},
},
Mutation: {
// parent vs args: 브라우저의 요청은 args로 인자를 받고, 다른 api의 요청은 parent로 인자를 받습니다.
createBoard: async (parent: any, args: any, context: any, info: any) => {
await Board.insert({
/* 1. 연습용(backend-example 방식) */
// writer: args.writer,
// title: args.title,
// contents: args.contents,
/* 2. 실무용(backend-practice 방식) */
...args.createBoardInput,
});
return "게시글 등록에 성공했습니다.";
},
// updateBoard: async () => {
// // update(조건, 수정할내용)
// // 👇🏻 3번 게시글을 영희로 바꿔줘!
// await Board.update({ number: 3 }, { writer: "영희" });
// },
deleteBoard: async (_: any, args: any) => {
// delete(삭제할 조건)
// 👇🏻 3번 게시글을 삭제해줘!
// await Board.delete({ number: 3 });
/* 실무용 삭제 방법 : Soft Delete */
// 실무에서는 보통 실제로 삭제를 하지는 않습니다.
// 삭제 여부를 담는 column을 만들고, 삭제 여부를 입력하는 update를 이용합니다.
// 이 방법을 사용할 경우에는 데이터 조회(fetchBoards) 시 deletedAt이 NULL인 행만 조회한다는 조건을 추가해야 합니다.
await Board.update({ number: args.number }, { deletedAt: new Date() });
},
},
};
// @ts-ignore
const server = new ApolloServer({
typeDefs,
resolvers,
});
import { DataSource, IsNull } from "typeorm";
import { Board } from "./Board.postgres";
const AppDataSource = new DataSource({
type: "postgres",
host: "34.64.244.122", // DB가 있는 컴퓨터의 IP 주소
port: 5014, // DB가 있는 컴퓨터의 port
username: "postgres",
password: "postgres2022",
database: "postgres",
entities: [Board],
synchronize: true, // 동기화
logging: true, // 명령어를 볼 수 있다.
});
AppDataSource.initialize()
.then(() => {
console.log("연결 성공!");
startStandaloneServer(server).then(() => {
console.log(`🚀 GraphQL 서버가 실행되었습니다.`); // port: 4000
});
})
.catch((error) => console.log(error, "연결 실패ㅜ"));