brew install yarn
$ mkdir server
$ cd server
$ yarn init -y
$ yarn add apollo-server graphql
Apollo Server는 GraphQL이 적용된 서버를 생성할 수 있는 클래스를 제공한다. 비유하자면 create-react-app
패키지와 비슷한 역할이다.
$ yarn add nodemon @babel/core @babel/node @babel/preset-env --dev
nodemon
yarn global add nodemon
으로 설치하면 좋다.babel
// .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
파일을 새로 생성한다.