GraphQL Server 구성

by Dany


Posted on May. 22th 2019



GraphQL 이란?

GraphQL은 기존의 RestApi의 단점을 보완하고자 나온 서버구성 및 언어로, Endpoint를 URI와 파라미터가 아닌 일종의 쿼리를 사용하여 데이터를 주고받는 형태입니다.

SPA 클라이언트에서 API서버로 데이터를 요청함에 있어서, 좀더 주도적으로 데이터를 요청하고 처리할 수 있다는 장점이 있습니다.

Rest API

RestAPI는 기본적으로 4가지의 Method와 URL로 요청하는 데이터를 표현합니다.

예를들어, 한명의 User 데이터를 요청하고자 한다면

GET /user/1

User 데이터를 username=홍길동, password=123 으로 수정하고자 한다면

POST /user/1

body: {
    username: '홍길동',
    password: '123'
}

이와 같은 형태로 Request가 구성되겠죠. 현재까지 대부분의 API 서버가 RestAPI로 구성되어 있는만큼, 이 방식은 분명하면서도 효율적인 방식으로 애용되어 왔습니다.

하지만 이런 RestAPI도 분명 단점이 존재하고, 이 문제점을 해결할 수 있도록 해주는게 바로 GraphQL 입니다.

Underfetch & Overfetch

RestAPI에는 underfetch / overfetch 두가지 문제점이 있는데, 먼저 underfetch에 대해 알아보겠습니다.

만약 클라이언트가 하나의 블로그 페이지를 구성하는데 상단에 로그인유저 정보를 필요로 하고, 본문에 포스팅된 포스트, 본문 한쪽에 카테고리들이 필요하다고 가정해보겠습니다.

그럼 다음과 같이 세번의 Request가 요청되어야 할겁니다.

GET /user/1
GET /posts
GET /categories

이걸 한번에 보내고 한번에 받겠지만, 기존의 RestAPI에서는 데이터 요청 API를 그렇게 되도록 표현하기가 상당히 난감합니다.
이를 underfetch라고 합니다.

overfetch는, 반대로 필요 이상의 데이터를 요청하게 되는 경우입니다. 만약 똑같이 한명의 User 정보를 요청하는데

  1. User의 Profile 페이지에서는 좀더 자세힌 내용이 필요하고,
  2. 위에서 처럼 상단에 표시할 간단한 내용이 필요한 경우,
GET /user/1

이렇게 Request가 나가먄 Response도 동일하게 됩니다. 그럼 1번의 경우에는 결과값을 그대로 바인딩하면 되겠지만, 2번의 경우에는 필요없는 데이터 까지 받기때문에, 결과값을 필요한 값만 골라 핸들링해서 바인딩해야 할 것입니다.

이를 Overfetch라고 합니다.

GraphQL

GraphQL에서는, 위와 같은 문제를 해결하기 위해 클라이언트가 주도적으로 쿼리를 작성해서 서버에게 필요한데이터 를 선택해서 요청합니다. 따라서 GraphQL서버의 EndPoint는 한곳이며, 클라이언트는 그 곳에 어떤 쿼리를 날리느냐에 따라 다른 데이터를 받게 됩니다.

그럼 본격적으로 GraphQL 서버 세팅을 시작해보도록 하겠습니다.

먼저 EndPoint가 될 Node.js 서버를 하나 만들겠습니다.
그리고 graphql-yoga라는 프로젝트를 이용할겁니다. 이는 편하게 GraphQL서버를 구성할 수 있도록 만들어져 있는 패키지, 또는 일종의 스켈레톤 프로젝트라고 생각하시면 되겠습니다.

npm install graphql-yoga --save

그리고 다음과 같이 EndPoint를 작성합니다.

  • app.js
const {GraphQLServer} = require('graphql-yoga/dist/index');
const schema = require('./graphql/schema');
const logger = require('morgan');

const graphql_server = new GraphQLServer({
    schema
})

graphql_server.express.use(logger('dev'))

graphql_server.start({
    port: PORT
}, () => {
    console.log(`graphql_server running on ${PORT}`)
})

이렇게 편하게 GraphQL Endpoint를 작성해보았습니다. 하지만 현재 이 서버는 작동하지 않을겁니다. 왜냐하면 GraphQL에는 스키마라는 것이 정의되어 있어야 하는데, 그게 없기때문이죠. 물론 코드에서 현재 있지도 않은 파일을 require하고 있으니 당연히 안되겠죠

다음 단계를 진행하기 위해, 스키마와 GraphQL문법을 설명하겠습니다.

여기부터는 디렉터리 구조를 맞춰야 문제없이 작동될겁니다.

  • 먼저 graphql 이라는 폴더를 하나 만들겠습니다.
  • /graphql 안에 api라는 폴더를 하나 더 만들겠습니다.
  • /graphql/api 안에 model.graphql 이라는 graphql파일을 만들겠습니다.
  • /graphql 안에 schema.js 라는 파일을 만들겠습니다.

먼저 model.graphql 파일을 열고, 스키마부터 정의하겠습니다.

  • model.graphql
#data for render
type PostDetail{
    post: Post!
    login_user: User!
}

#model
type User{
    id: Int
    user_id:String!
    user_name: String!
    create_dt: String
}

type Post{
    id: Int
    post_writer: User
    title: String!
    content: String!
    create_dt: String
}

Javascript object와 구조는 대충 비슷하지만, 문법은 확실히 다릅니다.
type으로 객체를 정의하고, 객체는 프로퍼티를 쓰고 : 뒤에 프로퍼티의 타입을 정의합니다. 이때 !는 클라이언트가 이 프로퍼티를 요청했을때, 해당 프로퍼티의 데이터는 Null로 나갈 수 없음을 의미합니다.
String, Int, Float등의 일반적인 데이터 타입뿐 아니라 객체가 데이터타입으로 정의될 수 있으며, 이 값들은 배열로 정의될 수 있음을 볼 수 있습니다. PostDetail의 pages가 int 데이터타입의 attribute들을 가지는 배열로 정의되어 있음을 알 수 있죠.

GraphQL의 자세한 문법은 여기를 참고하세요.

이렇게 정의된 스키마는, 기본적으로 GraphQL API의 리턴타입을 정의하게 됩니다.

이 스키마는 API Resolver와 연결되어야 합니다.

  • /graphql/api 안에 getPost 라는 폴더를 만들고,
  • /graphql/api/getPost 안에 getPost.graphql, getPost.js 두개의 파일을 만들겠습니다.

그리고 getPost.graphql 안에 해당 API Resolver를 정의합니다.

*getPost.graphql

type Query{
    getPost(post_id: Int!, jwt_token: String!):PostDetail!
}

GraphQL에는 기본적으로는 Query, Mutation 두가지 타입이 있습니다. RestAPI에 빗대자면, Query는 GET, Mutation은 POST / PUT / DELETE등을 생각하시면 됩니다.

getPost는 API 리졸버의 이름입니다. 괄호안에는 파라미터의 이름과 그 타입이 들어가고, !는 마찬가지로 해당 파라미터는 Null일수 없음을 의미합니다. Resolver 이름과 파라미터의 내용을 지키지 않고 클라이언트에서 Request를 하게 되면, 404 Error가 발생합니다. 마지막으로 getPost의 리턴 타입이 PostDetail이라고 정의되어있습니다. 이는 위에서 언급한바와 같이 model.graphql에 정의된 타입입니다.

이제 getPost.js 파일에 해당 Resolver 비즈니스로직을 구현합니다.

  • getPost.js
module.exports = {
    Query:{
        getPost: (_, args) => {
            const {post_id, jwt_token} = args;
            //데이터베이스에서 post정보를 얻으세요.
            const post = getPostByDatabase(post_id);
            //토큰으로 부터 login user정보 id를 얻으세요.
            const loginUser_id = getUserIdByJwtToken(jwt_token);
            //데이터베이스에서 loginUser 정보를 얻으세요.
            const loginUser = getUserByDatabase(loginUser_id);
            
            //return 오브젝트 프로퍼티와 값이 정의한 PostDetail 타입과 맞아야 한다. 이 리졸버는 PostDetail을 리턴한다고 리졸버정의에서 선언했기때문.
            
            //객체를 리턴하는 경우에는 그 리턴되는 객체의 프로퍼티도 맞아야 한다.
            return {
                post: post
                login_user: loginUser
            }
        }
    }
}

이렇게 스키마와 API 리졸버가 다 선언되었으면, 이 정의된 내용들을 위에서 만든 schema.js파일에서 합쳐주겠습니다. 그전에, 이를 합치기 위한 라이브러리를 설치하겠습니다.

npm install graphql-tools merge-graphql-schemas --save
  • schema.js
const {makeExecutableSchema} = require("graphql-tools/dist/index");

const {fileLoader, mergeResolvers, mergeTypes} =  require('merge-graphql-schemas');


const path = require('path');

const allTypes = fileLoader(path.join(__dirname, "/api/**/**/*.graphql"));
const allResolvers = fileLoader(path.join(__dirname, "/api/**/**/*.js"));

module.exports = makeExecutableSchema({
    typeDefs: mergeTypes(allTypes),
    resolvers: mergeResolvers(allResolvers)
})

코드를 간단히 설명하자면, 선언된 graphql 타입과 resolver들을 node에서 기본으로 지원하는 path모듈을 통해 불러와서 합쳐준 후, 이를 export하는 것입니다.

이렇게 export된 schema가

app.js 에서 이렇게 GraphQL Server 객체를 생성하는데 사용됩니다.

const schema = require('./graphql/schema');

const graphql_server = new GraphQLServer({
    schema
})

자 이제 완성입니다! 서버가 정상적으로 작동된다면 브라우저에서 http://localhost:4000 으로 접속해보세요! GraphQL API를 화면에서 테스트할 수 있는 PlayGround화면이 나올것입니다. 다만 요청하는 방식이 좀 다를거에요.

제가 아까 GraphQL에 대해서 설명드릴때 뭐라고 했는지 기억 나시나요?

GraphQL에서는 클라이언트는 쿼리를 사용해 데이터를 요청한다고 했습니다.

이제 그 쿼리를 만들어야 할때 입니다.

그 내용은 API Resolver 선언할때 썼던 GraphQL 문법과 거의 다르지 않습니다. 다만, 객체를 받을때 클라이언트는 어떤 데이터를 요청할지 주도적으로 선택해서 받을 수 있기 때문에, 어떤 데이터를 원하는지 입력해줘야 합니다.

Query{
    getPost(post_id:1, jwt_token: "jwt token blah blah"){
        post{
            title
            content
            post_writer{
                user_id
                user_name
            }
        }
        login_user{
            user_id
            usesr_name
        }
    }
}

getPost는 Post를 타입으로 하는 post와 User를 타입으로 하는 login_user 두개의 프로퍼티를 가진 객체를 리턴하는데, post는 또 User를 타입으로 하는 post_writer 프로퍼티가 있습니다.

이렇게 선언된 리턴타입중, 클라이언트가 원하는 값만 넣어서 쿼리를 만들어 요청을 하는 것이죠. 만약 헷갈린다면, PlayGround 오른쪽에 있는 DOCS와 SCHEMA를 참고하셔서 쿼리를 만들어 테스트하시면 됩니다.

한번쯤 사용해보세요. SPA에서는 분명히 장점이 있습니다. 물론 처음에는 어색하고 개발도 오래걸리지만, 만들어놓고나면 꽤 효율적인것 같습니다.

수고하셨습니다!