본문 바로가기

개발바닥/GraphQL

[개발] GraphQL - Vue + Apollo + GraphQL

안녕하세요 devport 입니다. 이번 포스팅에서는 지난 포스팅에서 작성한 Apollo 서버와 통신하는 클라이언트 웹페이지를 만들어 보려고 합니다. github 프로젝트로 등록되어 있는 vue-apollo-graphql-boilerplate 기반으로하여 저자와 책의 목록을 추가, 삭제, 검색할 수 있는 프로젝트를 구현해 보겠습니다.

 

vue-apollo-graphql-boilerplate 설치

기본적으로 git이 설치가 되어 있어야 보일러플레이트를 가져올 수 있습니다. 쉘에서 아래 명령어를 실행합니다.

git clone https://github.com/guillaumeduhan/vue-apollo-graphql-boilerplate.git

 

vue-apollo-graphql-boilerplate의 폴더가 생성이 되는데 저는 client라는 디렉토리명으로 변경하였습니다.

cd client
npm i

client 디렉토리로 이동하여 관련 의존성들을 가져오게됩니다.

설치되는 모듈들은

  • vue cli - Vue.js 개발을 위한 도구
  • apollo - Apollo는 GraphQL을 사용하는데 도움이 되는 클라이언트와 서버를 제공해주고 있습니다.
  • vuex - vuex는 vue.js 애플리케이션을 위한 상태관리 라이브러리 입니다.
  • vue-Router - Vue Router는 vue.js의 공식 라우터 입니다.

설치가 완료되었다면 아래 명령어를 실행 해봅니다.

npm run serve

명령어를 실행하면 개발 서버가 동작하게 됩니다. http://localhost:8080 으로 접속하게 되면 

 

위와 같은 페이지가 출력되는 것을 알 수 있습니다.

 

Schema

type Author {
    id: ID
    name: String
    books: [Book]
  }
  type Book {
    id: ID
    title: String
    author: String
  }
  type Query {
    allAuthor: [Author]
    allBook: [Book]
    findAuthor(id: String!): Author
    findBook(id: String!): Book
  }
  type Mutation {
    createAuthor(name: String!): Author
    createBook(title: String!, author: String!): Book
    deleteAuthor(id: ID!): Author
    deleteBook(id: ID!): Book
  }

서버측에서는 GraphQL 스키마 구조로 서버가 준비되어 있습니다. Author, Book에 대한 목록 정보와 관계를 표현할 수 있도록 구현 해보도록 하겠습니다.

 

Port 설정하기

Client에서 서버의 Target Port를 설정하기 위해서 main.js 파일을 열어 준비한 서버 경로를 지정하여 네트워크 요청이 수행될 수 있도록 합니다.

// HTTP connection to the API
const httpLink = createHttpLink({
  // You should use an absolute URL here
  uri: 'http://localhost:4000/graphql',
})

Vue-apollo

main.js 파일에서 vue-apollo 모듈을 통해 apollo Provider를 vue 컴포넌트에 주입 해줌으로써 apollo 모듈의 기능을 사용할 수 있게 됩니다.

import VueApollo from 'vue-apollo'

const apolloClient = new ApolloClient({
  uri: 'graphql',
})

Vue.use(VueApollo)

const apolloProvider = new VueApollo({
  defaultClient: apolloClient,
})

new Vue({
  apolloProvider,
  render: (h) => h(App),
}).$mount('#app')

 

Home.vue

<template>
  <div style="text-align: left; font-family: sans-serif; padding: 0 20px;">
    <div style="height:50px"></div>
    <div>
      <div class="flex-container">
        <div>Authors</div>
        <div class="flex-space"/>
        <div class="flex-container">
          <input
            v-model="authorForm.name"
            type="text"
            class="input"
            placeholder="Author Name"
            @keydown.enter="addAuthor"
          />
          <button class="button button4" @click="addAuthor">add</button>
        </div>
      </div>
    </div>
    <div class="flex-container" style="min-height: 80px; margin-bottom: 10px;">
      <author-card 
        v-for="(author, index) in allAuthor"
        :key="index"
        :author="author"
        @del="delAuthor"
      />
    </div>
    <div class="flex-container">
      <div>Books</div>
      <div class="flex-space" />
      <div class="flex-container">
        <input
          v-model="bookForm.title"
          type="text"
          class="input"
          placeholder="Title"
        />
        <input
          v-model="bookForm.author"
          type="text"
          class="input"
          placeholder="Author Name"
          @keydown.enter="addBook"
        />
        <button class="button button4" @click="addBook">add</button>
      </div>
    </div>
    <div class="flex-container" style="hegiht: 80px;">
      <book-card
        v-for="(book, index) in allBook"
        :key="index"
        :book="book"
        @del="delBook"
      />
    </div>
  </div>
</template>

<script>
import gql from 'graphql-tag'
import pjson from '../../package.json'
// Components
import AuthorCard from './AuthorCard'
import BookCard from './BookCard'

export default {
  name: 'Home',
  components: {
    AuthorCard,
    BookCard
  },
  data() {
    return {
      pjson,
      authorList: [
        {id: 0, name: 'Tom', books: []},
        {id: 1, name: 'Jack', books: []}
      ],
      bookList: [
        {id: 0, title: 'Hello World', author: 'Tom'}
      ],
      authorForm: { name: '' },
      bookForm: {title: '', author: ''}
    }
  },
  apollo: {
    allAuthor: gql`query {
      allAuthor {
        id
        name
        books {
          title
          author
        }
      }
    }`,
    allBook: gql`query{
      allBook {
        id
        title
        author
      }
    }`
  },
  methods: {
    // form 비우기
    clearAuthorForm () {
      this.authorForm.name = ''
    },
    clearBookForm () {
      this.bookForm.title = ''
      this.bookForm.author = ''
    },
    // 추가 요청
    async addAuthor () {
      if (this.authorForm.name == '') return
      const {data} = await this.$apollo.mutate({ 
        // mutation
        mutation: gql`mutation ($name: String!) {
          createAuthor(name: $name) {
            id
          }
        }
        `,
        // parameter
        variables: {
          name: this.authorForm.name
        }
      })

      this.$apollo.queries.allAuthor.refetch()
      this.clearAuthorForm()
    },
    async addBook () {
      if (this.bookForm.title == '' || this.bookForm.author == '') return
      const {data} = await this.$apollo.mutate({
        mutation: gql`mutation ($title: String!, $author: String!) {
          createBook(title: $title, author: $author) {
            id
          }
        }`,
        variables: {
          title: this.bookForm.title,
          author: this.bookForm.author
        }
      })
      this.$apollo.queries.allAuthor.refetch()
      this.$apollo.queries.allBook.refetch()
      this.clearBookForm()
    },
    // 제거 요청
    async delAuthor (id) {
      const {data} = await this.$apollo.mutate({
        mutation: gql`mutation ($id: ID!) {
          deleteAuthor(id: $id) {
            id
          }
        }`,
        variables: { id }
      })
      this.$apollo.queries.allAuthor.refetch()
    },
    async delBook (id) {
      const {data} = await this.$apollo.mutate({
        mutation: gql`mutation ($id: ID!) {
          deleteBook(id: $id) {
            id
          }
        }`,
        variables: { id } 
      })
      this.$apollo.queries.allBook.refetch()
      this.$apollo.queries.allAuthor.refetch()
    }
  }
}
</script>

'graphql-tag' 모듈로 Graphql 형식으로 쿼리를 사용할 수 있게되는데 gql`` 형식으로 작성할 수 있게 됩니다.

import gql from 'graphql-tag'

페이지가 생성되며 apollo 하위의 쿼리를 실행하게 됩니다.

apollo: {
  allAuthor: gql`query {
    allAuthor {
      id
      name
      books {
        title
        author
      }
    }
  }`,
  allBook: gql`query{
    allBook {
      id
      title
      author
    }
  }`
}

저자의 목록 정보가 allAuthor에 책의 목록정보가 allBook에 쿼리한 결과의 목록정보가 주입되게 됩니다.

apollo 하위의 쿼리를 재갱신하기 위하여 요청하기 위해서는 this.$apollo.queries.<name>.refetch()을 사용하여 쿼리를 다시 실행할 수 있습니다. 자세하게 설정하는 방법을 알고 싶다면 vue-apollo Query 문서를 확인해보시면 됩니다.

 

저자의 이름을 추가요청을 수행 완료하게 되면 저자의 목록을 쿼리하여 재갱신 하도록 요청을 하였습니다.

마찬가지로 책을 추가했을경우에는 저자의 하위 책목록도 같이 갱신하기 위해서 책과 저자의 목록을 함께 요청하는 구조입니다.

// 추가 요청
async addAuthor () {
  if (this.authorForm.name == '') return
  const {data} = await this.$apollo.mutate({ 
	// mutation
	mutation: gql`mutation ($name: String!) {
	  createAuthor(name: $name) {
		id
	  }
	}
	`,
	// parameter
	variables: {
	  name: this.authorForm.name
	}
  })

  this.$apollo.queries.allAuthor.refetch()
  this.clearAuthorForm()
},

<저자 추가>

async addBook () {
  if (this.bookForm.title == '' || this.bookForm.author == '') return
  const {data} = await this.$apollo.mutate({
	mutation: gql`mutation ($title: String!, $author: String!) {
	  createBook(title: $title, author: $author) {
		id
	  }
	}`,
	variables: {
	  title: this.bookForm.title,
	  author: this.bookForm.author
	}
  })
  this.$apollo.queries.allAuthor.refetch()
  this.$apollo.queries.allBook.refetch()
  this.clearBookForm()
},

<책 추가>

카드의 X 버튼을 클릭하게 될때에 id를 인자로 전달하여 저자 또는 책의 목록을 제거하도록 하였습니다.

책의 목록이 제거 될때에는 저자의 목록도 함께 갱신하였습니다.

async delAuthor (id) {
  const {data} = await this.$apollo.mutate({
	mutation: gql`mutation ($id: ID!) {
	  deleteAuthor(id: $id) {
		id
	  }
	}`,
	variables: { id }
  })
  this.$apollo.queries.allAuthor.refetch()
},

<저자 삭제>

async delBook (id) {
  const {data} = await this.$apollo.mutate({
	mutation: gql`mutation ($id: ID!) {
	  deleteBook(id: $id) {
		id
	  }
	}`,
	variables: { id } 
  })
  this.$apollo.queries.allBook.refetch()
  this.$apollo.queries.allAuthor.refetch()
}

<책 삭제>

 

이번 예제 프로젝트는 programrubber/graphql-tutorial에서 확인해보실수 있습니다.