[Apollo Server] GraphQL API 만들기, 서버 연결하기

이주희·2023년 2월 3일
0

BackEnd

목록 보기
14/14
post-thumbnail

GraphQL API를 만들어보자!!!!

[Apollo-server]

0. TypeORM 설치 및 DB 연동

1. ApolloServer & graphQL 설치

yarn add apollo-server graphql

2. [Docs]에서 내용 가져오기

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 연결 실패ㅜ"));

서버 열기 완!

3. entity 생성

  • Entity(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(중복되지 않는 고유한 아이디가 자동으로 만들어진다.)

4. API 만들기

모두 조회하기 API

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의 타입을 적어준다.

한 개만 조회하기 API

한 개만 조회할 때는 findOne()을 사용하고, 조건을 추가해주셔야 합니다.

// resolvers

const resolvers = {
  Query: {
    fetchBoard: async () => {
      const resultOne = await Board.findOne({
        where: { number: 3 },
      });
      return resultOne;
    },
  },
};

typeDefs에 API-DOCS도 만들어주세요!

생성하기 API

조회를 제외한 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
  }
`;

수정하기 API

update(조건, 수정할 내용) 조건과 수정할 내용을 순서에 맞게 작성합니다.

// resolvers

const resolvers = {
  Mutation: {
    updateBoard: async () => {
      // update(조건, 수정할내용)
      // 👇🏻 3번 게시글을 영희로 바꿔줘!
      await Board.update({ number: 3 }, { writer: "영희" });
    },
  },
};

typeDefs에 API-DOCS도 만들어주세요!

삭제하기 API - 참고용

삭제하기 delete(삭제할 조건) 가 있지만 실무에서는 사용하지 않습니다. 참고만 해주세요!

// resolvers

const resolvers = {
  Mutation: {
    deleteBoard: async () => {
      // delete(삭제할 조건)
      // 👇🏻 3번 게시글을 삭제해줘!
      await Board.delete({ number: 3 });
    },
  },
};

삭제하기 API - 실무용 ( Soft Delete )

실무에서 사용하는 삭제 방법입니다.

data를 삭제하면 돌이킬 수 없기 때문에 실수 방지 및 추후 데이터가 필요할 상황을 대비하여
실무에서는 실제로 데이터를 지우는 경우가 거의 없습니다.

대신에 Soft Delete라고 불리는 방식을 사용합니다.

Soft Delete

  • 삭제 여부를 담는 column( deletedAt )을 만들고, update를 이용해 삭제 여부를 입력합니다.
  • 이 방법을 사용할 경우에는 데이터 조회(fetchBoards) 시 deletedAt이 NULL인 행(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;
}

5. 서버 실행하고 접속하기

package.json 실행할 파일 지정

"dev": "ts-node-dev index.ts"

  • 위 내용을 입력한 파일명이 입력되어 있는지 확인한다.

터미널에서 yarn dev로 실행

yarn dev

  • liste() 안에 입력한 포트번호로 실행된다.

브라우저에서 접속


전체 코드
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, "연결 실패ㅜ"));
profile
🍓e-juhee.tistory.com 👈🏻 이사중

0개의 댓글