본문 바로가기
개발/Javascript

[GraphQL] GraphQL 실습해보기 (with NomadCoder)

by onethejay 2022. 5. 16.
728x90

노마드코더 GraphQL 강의를 보면서 정리합니다.

준비물

  • Node.js
  • Visual studio code

GraphQL?

  • GraphQL은 하나의 Specification이다.
  • 아이디어를 각각의 프로그래밍 언어를 통해 구현하는 것이다.
  • QL은 Query Language를 뜻한다.

GraphQL이 해결해주는 REST API의 문제점

  1. Over-fetching
    REST API를 통해 받은 데이터가 우리가 필요한 데이터보다 많을때를 뜻한다.
    이것은 backend 서버 혹은 database가 일을 더 많이 해야 한다는 것이다.
    ex) 어떤 API를 호출했을 때 제공되는 데이터는 10개이다. 그 중에 내가 필요한 데이터는 2개라면 8개의 사용하지 않는 데이터까지 온 것이다.

  2. Under-fetcing
    REST API를 통해 받은 데이터가 우리가 필요한 데이터보다 적을때를 뜻한다.
    필요한 데이터를 모두 받기 위해 각각 다른 API를 호출해서 데이터를 받아야 한다.
    이것 역시 backend 서버 혹은 database가 여러번 호출되어 데이터를 조합해야 하므로 일을 더 많이 하게 한다.
    ex) 영화 API에서 제공해주는 정보가 있을때, 장르에 대한 정보는 코드로만 주어진다면 코드에 대한 정보를 추가로 가져와야 한다.

GraphiQL 실습해보기

https://graphql.org/swapi-graphql/
GraphQL에서 제공해주는 api 실습 페이지이며 스타워즈와 관련된 데이터로 실습해볼 수 있다.
오른쪽에 Docs를 누르면 Documentation Explorer가 나타나며 호출할수 있는 데이터와 받을 수 있는 데이터가 정리되어있다.

아래처럼 데이터를 요청하면?

{
  allFilms {
    totalCount
  }
}

정확히 요청한 데이터만 받아온다.

{
  "data": {
    "allFilms": {
      "totalCount": 6
    }
  }
}

또한, films 안에 있는 데이터 중 title만 가져오고 싶을땐 아래 처럼 요청한다.

{
  allFilms {
    totalCount
    films {
      title
    }
  }
}

그러면 이렇게 데이터를 전달해준다.

{
  "data": {
    "allFilms": {
      "totalCount": 6,
      "films": [
        {
          "title": "A New Hope"
        },
        {
          "title": "The Empire Strikes Back"
        },
        {
          "title": "Return of the Jedi"
        },
        {
          "title": "The Phantom Menace"
        },
        {
          "title": "Attack of the Clones"
        },
        {
          "title": "Revenge of the Sith"
        }
      ]
    }
  }
}

위의 코드까지가 Over-fetching을 해결해줬다면 아래 코드는 Under-fetching을 해결한 요청이다.

{
  allFilms {
    totalCount
    films {
      title
    }
  }
  allPeople {
    people {
      name
      hairColor
      eyeColor
      birthYear
    }
  }
}

allFilms와 allPeople은 다른 데이터 유형이지만 하나의 요청에 두가지를 담아 보낼 수 있다.

{
  "data": {
    "allFilms": {
      "totalCount": 6,
      "films": [
        {
          "title": "A New Hope"
        },
        {
          "title": "The Empire Strikes Back"
        },
        {
          "title": "Return of the Jedi"
        },
        {
          "title": "The Phantom Menace"
        },
        {
          "title": "Attack of the Clones"
        },
        {
          "title": "Revenge of the Sith"
        }
      ]
    },
    "allPeople": {
      "people": [
        {
          "name": "Luke Skywalker",
          "hairColor": "blond",
          "eyeColor": "blue",
          "birthYear": "19BBY"
        },
        {
          "name": "C-3PO",
          "hairColor": "n/a",
          "eyeColor": "yellow",
          "birthYear": "112BBY"
        },
        {
          "name": "R2-D2",
          "hairColor": "n/a",
          "eyeColor": "red",
          "birthYear": "33BBY"
        },
        {
          "name": "Darth Vader",
          "hairColor": "none",
          "eyeColor": "yellow",
          "birthYear": "41.9BBY"
        }
        ...
      ]
    }
  }
}

요청 1번에 서로 다른 데이터 allFilms와 allPeople을 받았다.

Apollo Server 설치 및 설정

Apollo Server는 Node.js 미들웨어이다. API 서버를 호출하기 전에 앞에서 api 요청을 받아 처리한 후 서버에게 전달해준다.

실습할 폴더를 생성한다.

mkdir tweetql

vscode로 폴더를 연다.

code tweetql

vscode가 열리면 vscode의 terminal에서 node repository로 세팅한다.

npm init -y

이후 apollo-server와 graphql을 설치한다.

npm install apollo-server graphql

더 나은 개발경험을 위해 nodemon도 설치한다.

npm install nodemon -D

설치가 완료되었다면 server.js파일과 .gitignore파일을 생성한다.

.gitignore 파일에 node_modules 디렉토리를 추가한다.

추가했으면 git init을 설정한다.

git init .

package.json 파일의 scripts의 내용을 변경하고, 아래에 type: module을 추가한다.

{
  "name": "tweetql",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "dev": "nodemon server.js"  //여기 수정
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "apollo-server": "^3.7.0",
    "graphql": "^16.5.0"
  },
  "devDependencies": {
    "nodemon": "^2.0.16"
  },
  "type": "module"  //여기 추가
}

server.js에 Apollo-server와 gql을 import한다.
아래 방법은 package.json에 type을 module로 설정했기 때문에 가능하다.

import {ApolloServer, gql} from 'apollo-server';

type을 지정하지 않았다면 아래처럼 import 해야 한다.

const {ApolloServer, gql} = require("apollo-server");

import를 완료했다면 npm run dev 명령어로 서버가 잘 작동하는지 확인한다.

npm run dev 

확인 되었다면 server.js에 코드를 추가한다.

const server = new ApolloServer({})

server.listen().then(({url}) => {
    console.log(`Running on ${url}`);
})

그러면 에러가 발생한다. (Apollo-server는 존재하는 schema나 modules 또는 typeDefs를 가져야한다.)

Error: Apollo Server requires either an existing schema, modules or typeDefs
    at ApolloServer.constructSchema 

Apollo-server 에러 수정하기

에러가 나타난 이유는 graphql이 data의 shape을 미리 알고 있어야 하기 때문이다.

이제 소스를 수정하여 에러를 잡아보자.
먼저 server.js를 아래 코드처럼 수정한다. Query type을 작성해줘야 잘 작동한다.

import {ApolloServer, gql} from 'apollo-server';

const typeDefs = gql`
    type Query {
        text: String
        hello: String
    }

`

const server = new ApolloServer({typeDefs})

server.listen().then(({url}) => {
    console.log(`Running on ${url}`);
})

이상없이 서버가 작동하고 localhost 주소가 표시된다. 페이지로 접속해보자.

Apollo-server Studio에서 실습하기

페이지에 접속한 후 Query your server를 클릭하면 실습할 수 있는 페이지가 나타난다.

왼쪽 Documentation 부분에서 gql``에 작성했던 내용을 볼 수 있다.
Operation에서 캡쳐화면 혹은 아래처럼 입력하고 호출해보자.

{
    text
    hello
}

그러면 Response는 두개 데이터 모두 null로 응답한다.

{
  "data": {
    "text": null,
    "hello": null
  }
}

이제 text, hello부분을 지우고 트위터같은 graphql API를 만들어보자.
graphql의 기본 Scalar는 ID, String, Int, Boolean 등이 있다.

위의 Scalar를 이용해 schema를 작성한다.
메서드나 함수에 Argument를 입력하는 방식으로 필요한 값을 전달할 수 있다.

const typeDefs = gql`
    type User {
        id: ID
        username: String

    }

    type Tweet {
        id: ID
        text: String
        author: User
    }

    type Query {
        allTweets: [Tweet]
        tweet(id: ID): Tweet
    }
`

Variables에 입력하는 값은 $가 붙은 동일한 변수에 입력된다.

POST request 같은 것들은 Mutation Type에 입력한다.
즉, 서버 혹은 Database 등에 변경 작업(POST, PUT, DELETE)이 일어나는 것들은 Mutation에 입력되어야 한다.
데이터를 출력(GET)하는 작업등은 Query에 입력되어야 한다.

const typeDefs = gql`
    type User {
        id: ID
        username: String

    }

    type Tweet {
        id: ID
        text: String
        author: User
    }

    type Query {
        allTweets: [Tweet]
        tweet(id: ID): Tweet
    }

    type Mutation {
        postTweet(text: String, userId: ID): Tweet
    }
`

query는 앞에 붙이거나 붙이지 않아도 정상적으로 호출되지만

{
  allTweets {
    text
  }
  tweet(id: "1") {
    text
  }
}
query {
  allTweets {
    text
  }
  tweet(id: "1") {
    text
  }
}

mutation은 앞에 명시해주어야 한다.

mutation {
  postTweet(text: "Hello, first tweet", userId: "1") {
    text
  }
}

type의 Scalar에 !(느낌표)를 붙이면 Non Nullable Fields가 되어 Null이 허용되지 않는다.
따라서 필수값 혹은 항상 Null이 아닌 값으로 지정하려면 Scalar뒤에 !를 붙여주면 된다.

type Query {
    allTweets: [Tweet!]!
    tweet(id: ID!): Tweet!
}

Query Resolver 만들기

이제 요청에 대한 응답을 위해 Javascript 소스를 작성해보자
resolver내에 선언한 함수명과 graphql Schema의 이름은 같아야 한다.

const typeDefs = gql`    
    type Tweet {
        id: ID!
        text: String!
        author: User!
    }

    type Query {
        tweet(id: ID!): Tweet
    }
`
const resolvers = {
    Query: {
        tweet() {
            console.log(`I'm called`);
            return null;
        }
    }
}

Studio에서 호출하면 tweet에 null이 출력되어 이전과 같아보이지만…

서버의 console을 확인해보면 log가 찍혀있다.

가짜 데이터를 생성해서 allTweets의 resolver를 만든다.

const tweets = [
    {
        id: "1",
        text: "first one",
    },
    {
        id: "2",
        text: "second one",
    },
]

const resolvers = {
  Query: {
    allTweets() {
      return tweets;
    }
  }
}

Studio에서 allTweets를 호출해본다.

굳!

이어서 tweet의 resolver도 만든다.
단, resolver에 argument를 추가 할 때 맨 앞은 root가 필수이고 그 다음에 추가해야 한다.

const resolvers = {
    Query: {       
        allTweets() {
            return tweets;
        },
        tweet(root, {id}) {            
            return tweets.find((tweet) => tweet.id === id);
        }
    }
}


이어서 mutation에 대한 resolver를 만든다.

const resolvers = {
    Query: {       
        allTweets() {
            return tweets;
        },
        tweet(root, args) {
            console.log(args)
            return tweets.find((tweet) => tweet.id === args.id);
        }
    },
    Mutation: {
        postTweet(_, {text, userId}) {
            const newTweet = {
                id: tweets.length + 1,
                text
            };
            tweets.push(newTweet);
            return newTweet;
        },
    }
}

Studio에서 postTweet을 호출해본다.

이어서 allTweet query를 호출해서 데이터를 확인해본다.

deleteTweet까지 구현하고 데이터를 확인해본다. const tweet은 let으로 변경한다.

const resolvers = {
    Query: {       
        allTweets() {
            return tweets;
        },
        tweet(root, args) {
            console.log(args)
            return tweets.find((tweet) => tweet.id === args.id);
        }
    },
    Mutation: {
        postTweet(_, {text, userId}) {
            const newTweet = {
                id: tweets.length + 1,
                text
            };
            tweets.push(newTweet);
            return newTweet;
        },
        deleteTweet(_, {id}) {
            const tweet = tweets.find((tweet) => tweet.id === id)
            if(!tweet) return false;
            tweets = tweets.filter((tweet) => tweet.id !== id)
            return true;
        }
    }
}


이어서 type에 대한 resolver를 만든다. dynamic field를 이용한다.

let users = [
    {
        id: "1",
        firstName: "Jay",
        lastName: "Jang",
    },
]

const typeDefs = gql`
    type User {
        id: ID!
        firstName: String!
        lastName: String!
        fullName: String!
    }
`
const resolvers = {
  Query: {
    allTweets() {
      return tweets;
    },
    tweet(root, args) {
      console.log(args);
      return tweets.find((tweet) => tweet.id === args.id);
    },
    allUsers() {
      console.log("allUsers called");
      return users;
    }
  },
  Mutation: {
    postTweet(_, {text, userId}) {
      const newTweet = {
        id: tweets.length + 1,
        text
      };
      tweets.push(newTweet);
      return newTweet;
    },
    deleteTweet(_, {id}) {
      const tweet = tweets.find((tweet) => tweet.id === id)
      if(!tweet) return false;
      tweets = tweets.filter((tweet) => tweet.id !== id)
      return true;
    }
  },
  User: {
    fullName() {
      console.log("fullName called");
      return "Hello";
    }
  }
}

users의 데이터에는 fullName이 없으나 type resolver에 의해 데이터가 출력된다.
field에 찾으려는 데이터가 없으면 type resolver를 찾아 데이터를 가져오기 때문이다.

이어서 Users와 Tweets를 연결해보도록 하자.
type resolver의 첫번째 argument는 return되고 있는 object의 data를 준다.

Tweet의 type resolver를 생성한다.

const resolvers = {
    Query: {       
        allTweets() {
            return tweets;
        },
        tweet(root, args) {
            console.log(args);
            return tweets.find((tweet) => tweet.id === args.id);
        },
        allUsers() {
            console.log("allUsers called");
            return users;
        }
    },
    Mutation: {
        postTweet(_, {text, userId}) {
            const newTweet = {
                id: tweets.length + 1,
                text
            };
            tweets.push(newTweet);
            return newTweet;
        },
        deleteTweet(_, {id}) {
            const tweet = tweets.find((tweet) => tweet.id === id)
            if(!tweet) return false;
            tweets = tweets.filter((tweet) => tweet.id !== id)
            return true;
        }
    },
    User: {        
        fullName({firstName, lastName}) {
            return `${firstName} ${lastName}`;
        }
    },
    Tweet: {
        author({userId}) {
            return users.find((user) => user.id === userId);
        }
    }
}

graphql에서 resolver를 통해 데이터를 가져온다.

Documantation

각 Schema에 대한 설명을 작성할 수 있다.
쌍따옴표를 6개 작성하고 3개의 반 사이에 내용을 추가하면 된다.
타입이든 scalar든 추가해서 설명을 추가할 수 있다.

REST API 서버를 GraphQL로 변경하기

기존의 REST API 서버에서 제공하는 json의 type을 작성해야 한다.

type Movie {
    id: Integer!
    title: String!
    rating: Float!
    ...
}

이어서 Query Resolver를 추가한다.

const typeDefs = gql`    
    type Query {
        allMovies: [Movie!]!
    }
`
const resolvers = {
  Query: {
    allMovies() {
      //Database 혹은 다른 API를 호출
      return fetch("https://yts.mx/api/v2/list_movies.json")
        .then((r) => r.json())
        .then((json) => json.data.movies);
    },
  }
}

이렇게 하면 backend 서버 앞단에서 graphQL 요청을 처리할 수 있게 된다.

끝!!

728x90

댓글