Apollo Server로 GraphQL API 서버 개발하기 (1)

곽태욱·2020년 1월 22일
25

깃허브 : https://github.com/rmfpdlxmtidl/movie-server

이번 글에선 Apollo 서버를 활용해 GraphQL API를 제공하는 서버를 개발하려고 한다. Apollo 서버는 GraphQL API를 제공하는 서버를 개발할 수 있게 도와주는 패키지로서 기존에 Node.js에서 사용하는 Express와 역할이 비슷하다. 사전에 Node.js는 필수로 설치해야 하고, Yarn과 vscode는 설치를 권장한다. 서버 개발에 사용하는 기술은 아래와 같다.

  • GraphQL
  • Apollo Server
  • TypeScript
  • Nodemon, Babel

Node.js 설치
https://velog.io/@gwak2837/Node.js-설치

Apollo Server 공식 문서
https://www.apollographql.com/docs/apollo-server/getting-started/

GraphQL이란

GraphQL은 서버로부터 어떤 데이터를 요청할지 표현한 언어다. GraphQL과 비슷한 것으로는 REST API가 있다. 아래 사이트는 GraphQL 공식 사이트인데 GraphQL 처리 과정이 시각적으로 잘 표현되어 있다.

GraphQL 공식 사이트
https://graphql.org/

GraphQL과 REST API의 차이점
https://velog.io/@gwak2837/React에-Apollo-Client-연결하기

GraphQL 환경 설정

프로젝트 폴더 생성

> mkdir server-app-name
cd server-app-name
yarn init -y

mkdir ... : 프로젝트 폴더를 생성하고,
cd ... : 해당 폴더로 이동하고,
yarn init : package.json 파일을 생성한다. -y 옵션을 줘서 모든 답변을 기본값(공백)으로 설정할 수 있다.

Apollo Server 및 GraphQL 설치

> yarn add apollo-server graphql

Apollo Server는 GraphQL이 적용된 서버를 생성할 수 있는 클래스를 제공한다. 비유하자면 create-react-app 패키지와 비슷한 역할이다.

개발 도구 설치 (선택)

> yarn add nodemon @babel/core @babel/node @babel/preset-env --dev
  • nodemon
    실행 중인 Javascript 파일 변경을 감지하고, 파일이 변경되면 Node를 재실행해 변경 사항이 자동으로 반영되게 도와준다. 많이 쓰는 패키지라서 yarn global add nodemon으로 설치하면 좋다.

  • babel
    우리가 Javascript 최신 문법을 사용하면 자동으로 각 브라우저가 지원하는 Javascript 버전에 맞게 문법을 바꿔준다.

.babelrc 파일 생성 (선택)

// .babelrc
{
    "presets" : ["@babel/preset-env"]
}

babel 패키지의 설정 파일인 .babelrc 파일을 프로젝트 폴더에 새로 생성한다.

package.json 파일 수정

// package.json
{
  ...
  "scripts": {
    "start": "nodemon --exec babel-node src/index.js"
  }
}

package.jsonscript 항목을 추가한다. babel과 nodemon을 설치하지 않았으면 위 스크립트 대신 "start": "node index.js"를 추가한다.

데이터베이스 생성

// src/database/movies.js
const movies = [
  {
    id: 1,
    name: '백두산',
    rating: 7
  },
  {
    id: 2,
    name: '히트맨',
    rating: 7
  },
  {
    id: 3,
    name: '남산의 부장들',
    rating: 9
  },
  {
    id: 4,
    name: '겨울왕국2',
    rating: 7
  }
];

export default movies;

나중에 만들 리졸버 파일 안에서 데이터베이스를 처리해도 되지만 명확한 구분을 위해 따로 폴더를 만들어 json 파일 형태로 간단한 데이터베이스를 만들었다.

데이터베이스는 외부에서 가져와도 되고, MySQL, mongoDB 등 여러가지 형태로 직접 생성해도 된다. GraphQL은 서버와 클라이언트 사이에서 오고 가는 쿼리 언어이기 때문에 데이터베이스 형태에 제약이 없다.

스키마와 리졸버 설정

스키마

// src/graphql/typeDefs.js
import { gql } from 'apollo-server';

const typeDefs = gql`
  type Movie {
    id: Int!
    name: String!
    rating: Int!
  }

  type Query {
    movies: [Movie!]!
    movie(id: Int!): Movie
  }

  type Mutation {
    addMovie(name: String!, rating: Int!): Movie!
  }
`;

export default typeDefs;

스키마는 서버에 어떻게 데이터를 요청할지 정의한 파일이다. 요청 시 어떤 데이터를 얼마나 요청할지, 각각의 데이터의 자료형이 무엇이고, 어떤 데이터를 필수로 요청할지에 대한 정보가 담긴다. 즉, 사용자는 반드시 스키마에 정의된 형태로 서버에 요청해야 한다.

  • Query : 데이터베이스에서 데이터를 읽는 요청
  • Mutation : 데이터베이스를 수정하는 요청

스키마엔 Query, Mutation와 같이 2가지 요청이 있다. 이렇게 요청을 구분한 이유는 데이터베이스 읽기 요청은 무한정으로 동시에 수행될 수 있지만, 데이터베이스 수정 요청은 순차적으로 수행되야 하기 때문이다. 성능 향상을 위해 편의상 구분했을 것이라고 생각하지만 정확히는 모른다..

스키마엔 Movie의 구조와 자료형도 정의해야 한다. 그래야 GraphQL 서버에서 데이터베이스 구조를 알고 처리할 수 있다.

위 파일은 다음을 의미한다. 서버에 Query 형태로 movies를 요청하면 Movie의 배열이 반드시 반환된다. 서버에 Mutation 형태로 파라미터와 함께 addMovie를 요청하면 Movie가 반드시 반환된다.

  • ! : Not Nullable. 데이터가 꼭 있어야 한다.
  • [] : 배열

리졸버

// src/graphql/resolvers.js
import movies from '../database/movies';

const resolvers = {
  Query: {
    movies: () => movies,
    movie: (_, { id }) => {
      return movies.filter(movie => movie.id === id)[0];
    }
  },
  Mutation: {
    addMovie: (_, { name, rating }) => {
      // 영화 제목 중복 검사
      if (movies.find(movie => movie.name === name)) return null;
      
      // 데이터베이스에 추가
      const newMovie = {
        id: movies.length + 1,
        name,
        rating
      };
      movies.push(newMovie);
      return newMovie;
    }
  }
};

export default resolvers;

리졸버는 사용자가 쿼리를 요청했을 때 이를 서버가 어떻게 처리할지 정의한 파일이다. 리졸버는 요청에 대해 단순히 데이터를 반환할 수도 있지만, 직접 데이터베이스를 찾거나, 메모리에 접근하거나, 다른 API에 요청해서 데이터를 가져올 수 있다.

위 파일은 다음을 의미한다. 데이터베이스를 읽는 요청(Query) 중 movies가 요청되면 ../database/Movies.js에 있는 movies 데이터를 반환한다. 데이터베이스를 수정하는 요청(Mutation) 중 namerating을 파라미터로 가진 addMovie가 요청되면, 데이터베이스에 영화를 추가한다.

각 리졸버 항목(movies, movie, addMovie)의 매개변수 4개까지 있는데 다음과 같다.

  1. parent : 부모 타입 리졸버에서 반환된 결과를 가진 객체 잘 모름
  2. args : 쿼리 요청 시 전달된 파라미터를 가진 객체
  3. context : GraphQL의 모든 리졸버가 공유하는 객체로서 로그인 인증, 데이터베이스 접근 권한 등에 사용한다.
  4. info : 명령 실행 상태 정보를 가진 객체 잘 모름

프로젝트가 커지다보면 나중에는 리졸버 구현이 복잡해질텐데, 이를 해결하기 위해 리졸버 단계에 prismaTypeORM 등 데이터베이스 ORM를 사용하기도 한다.

서버 생성

// src/index.js
import { ApolloServer } from 'apollo-server';
import resolvers from './graphql/resolvers';
import typeDefs from './graphql/typeDefs';

// ApolloServer는 스키마와 리졸버가 반드시 필요함
const server = new ApolloServer({
  typeDefs,
  resolvers
});

// listen 함수로 웹 서버 실행
server.listen().then(({ url }) => {
  console.log(`🚀  Server ready at ${url}`);
});

프로젝트 src 폴더에 index.js 파일을 새로 생성한다.

프로젝트 구조

여기까지 따라왔으면 프로젝트 구조는 대략 아래와 같을 것이다.

서버 실행

> yarn start
🚀 Server ready at localhost:4000

터미널에 해당 명령어를 입력하면 서버가 실행된다. 서버 실행 후 http://localhost:4000/으로 접속하면 GraphQL Playground가 나온다. 여기서 자신이 만든 서버 API를 테스트할 수 있다.

GraphQL API 테스트

Query

query {
  movies {
    name
  }
}

위와 같이 입력하고 실행하면 모든 영화의 이름이 반환되는 것을 볼 수 있다. 여기서 query 키워드는 생략할 수 있다.

Mutation

mutation {
  addMovie(name:"인셉션", rating: 8) {
    name
  }
}

위와 같이 새로운 영화를 추가하는 요청을 보낼 수도 있다.

그 후에 데이터베이스를 조회하면 성공적으로 추가된 것을 확인할 수 있다.

profile
이유와 방법을 알려주는 메모장 겸 블로그. 블로그 내용에 대한 토의나 질문은 언제나 환영합니다.

0개의 댓글