이번 글에선 Apollo 서버를 활용해 GraphQL API를 제공하는 서버를 개발하려고 한다. Apollo 서버는 GraphQL API를 제공하는 서버를 개발할 수 있게 도와주는 패키지로서 기존에 Node.js에서 사용하는 Express와 역할이 비슷하다. 사전에 Node.js는 필수로 설치해야 하고, Yarn과 vscode는 설치를 권장한다. 서버 개발에 사용하는 기술은 아래와 같다.
Node.js 설치
https://velog.io/@gwak2837/Node.js-설치
Apollo Server 공식 문서
https://www.apollographql.com/docs/apollo-server/getting-started/
GraphQL은 서버로부터 어떤 데이터를 요청할지 표현한 언어다. GraphQL과 비슷한 것으로는 REST API가 있다. 아래 사이트는 GraphQL 공식 사이트인데 GraphQL 처리 과정이 시각적으로 잘 표현되어 있다.
GraphQL 공식 사이트
https://graphql.org/
GraphQL과 REST API의 차이점
https://velog.io/@gwak2837/React에-Apollo-Client-연결하기
> mkdir server-app-name
cd server-app-name
yarn init -y
mkdir ...
: 프로젝트 폴더를 생성하고,
cd ...
: 해당 폴더로 이동하고,
yarn init
: package.json
파일을 생성한다. -y
옵션을 줘서 모든 답변을 기본값(공백)으로 설정할 수 있다.
> 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
{
"presets" : ["@babel/preset-env"]
}
babel 패키지의 설정 파일인 .babelrc
파일을 프로젝트 폴더에 새로 생성한다.
// package.json
{
...
"scripts": {
"start": "nodemon --exec babel-node src/index.js"
}
}
package.json
에 script
항목을 추가한다. 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) 중 name
과 rating
을 파라미터로 가진 addMovie
가 요청되면, 데이터베이스에 영화를 추가한다.
각 리졸버 항목(movies
, movie
, addMovie
)의 매개변수 4개까지 있는데 다음과 같다.
parent
: 부모 타입 리졸버에서 반환된 결과를 가진 객체 args
: 쿼리 요청 시 전달된 파라미터를 가진 객체context
: GraphQL의 모든 리졸버가 공유하는 객체로서 로그인 인증, 데이터베이스 접근 권한 등에 사용한다.info
: 명령 실행 상태 정보를 가진 객체 프로젝트가 커지다보면 나중에는 리졸버 구현이 복잡해질텐데, 이를 해결하기 위해 리졸버 단계에 prisma
나 TypeORM
등 데이터베이스 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를 테스트할 수 있다.
query {
movies {
name
}
}
위와 같이 입력하고 실행하면 모든 영화의 이름이 반환되는 것을 볼 수 있다. 여기서 query
키워드는 생략할 수 있다.
mutation {
addMovie(name:"인셉션", rating: 8) {
name
}
}
위와 같이 새로운 영화를 추가하는 요청을 보낼 수도 있다.
그 후에 데이터베이스를 조회하면 성공적으로 추가된 것을 확인할 수 있다.