본문 바로가기
GraphQL

Fullstack part 8 #2 React and GraphQL

by kicksky 2021. 3. 9.

- GraphQL은 기본적으로 HTTP POST 요청으로 값을 얻는다.

- 따라서 Postman을 활용할 수 있다. http://localhost:4000/graphql로 query 키 값을 보내면 된다.

- 혹은 axios를 통해 리액트와 GraphQL 사이에서 데이터를 교환하는 것도 가능한다.

 

그렇지만

☞  번거로우니 불필요한 디테일을 알아서 처리해주는 higher order library를 씁시다. 

두 가지 옵션: Apollo Client와 페이스북의 Relay

 

Apollo client

npm install @apollo/client graphql
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'

import { ApolloClient, HttpLink, InMemoryCache, gql } from '@apollo/client'

// 새로운 client 객체 생성 - 쿼리를 서버로 보내는 데 쓰인다.
const client = new ApolloClient({
  cache: new InMemoryCache(),
  link: new HttpLink({
    uri: 'http://localhost:4000',
  })
})

const query = gql`
query {
  allPersons  {
    name,
    phone,
    address {
      street,
      city
    }
    id
  }
}
`

client.query({ query })
  .then((response) => {
    console.log(response.data)
  })

ReactDOM.render(<App />, document.getElementById('root'))

어플리케이션은 client 객체를 사용해서 GraphQL 서버와 통신할 수 있다.

 

ReactDOM.render(
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>,
  document.getElementById('root')
)

App 컴포넌트를 ApolloProvider로 감싸면 어플리케이션의 모든 컴포넌트에서 client에 접근할 수 있다.

 

query 생성

useQuery Hook

- query를 파라미터로 받는다

- 리턴되는 객체 값의 필드 : data, previousData, loading, error, variables ... 

( www.apollographql.com/docs/react/api/react/hooks/#result )

import React from 'react'
import { gql, useQuery } from '@apollo/client';

const ALL_PERSONS = gql`
query {
  allPersons  {
    name
    phone
    id
  }
}
`

const App = () => {
  const result = useQuery(ALL_PERSONS)

  if (result.loading)  {
    return <div>loading...</div>
  }

  return (
    <div>
      {result.data.allPersons.map(p => p.name).join(', ')}
    </div>
  )
}

export default App

Name 쿼리와 변수

- GraphQL 변수 ! ( graphql.org/learn/queries/#variables )

- query에 동적으로 파라미터를 넘겨줄 수 있다.

// "Arto Hellas"에 대한 정보만 가져올 수 있다.
query {
  findPerson(name: "Arto Hellas") {
    phone 
    city 
    street
    id
  }
}

// 변수 !!!
query findPersonByName($nameToSearch: String!) {
  findPerson(name: $nameToSearch) {
    name
    phone 
    address {
      street
      city
    }
  }
}

- $nameToSearch

- GraphQL Playground에서도 화면 하단에서 변수 넘겨볼 수 있음

 

useQuery

컴포넌트가 렌더됐을 때 쿼리가 끝나는 상황에 적합

 

useLazyQuery

요청할 때에만 쿼리가 실행되는 상황에 적합 ( Executing queries manually )

예) 유저가 특정한 인물에 대한 디테일을 원할 때에만 쿼리가 필요한 경우

import { useState, useEffect } from 'react'
import { gql, useLazyQuery } from '@apollo/client';

const FIND_PERSON = gql`
  query findPersonByName($nameToSearch: String!) {
    findPerson(name: $nameToSearch) {
      name
      phone 
      id
      address {
        street
        city
      }
    }
  }
`

export const Persons = ({ persons }) => {
    const [getPerson, result] = useLazyQuery(FIND_PERSON) 
    const [person, setPerson] = useState(null)

	// (2) 쿼리로 변수 넘겨서 데이터 가져오기
    const showPerson = (name) => {
      getPerson({ variables: { nameToSearch: name } })
    }
    
    // (3) result 값이 들어오면 state 업데이트
    useEffect(() => {
      if (result.data) {
        setPerson(result.data.findPerson)
      }
    }, [result])
    
    // (4) person 값이 업데이트 되면서 렌더링
    // close 누르면 state 값 비우기
    if (person) {
      return(
        <div>
          <h2>{person.name}</h2>
          <div>{person.address.street} {person.address.city}</div>
          <div>{person.phone}</div>
          <button onClick={() => setPerson(null)}>close</button>
        </div>
      )
    }

    return (
      <div>
        <h2>Persons</h2>
        {persons.map(p =>
          <div key={p.name}>
            {p.name} {p.phone}
            {/* (1) 버튼 클릭시 showPerson 호출 */}
            <button onClick={() => showPerson(p.name)} >
              show address
            </button> 
          </div>  
        )}
      </div>
    )
  }

 

캐시

동일한 요청을 하는 쿼리는 캐싱되기 때문에 네트워크 요청을 여러번 보내지 않는다ID 타입의 id 필드가 있기 때문에 쿼리를 여러 번 하여 같은 객체 값을 리턴하는 경우에 아폴로는 이 쿼리들을 하나로 결합시킨다. Hellas의 주소를 가져오는 findPerson 쿼리를 하면 allPersons 쿼리에 대한 주소도 업데이트 하는 것. 

( 캐시 원리는 한번 더 설명을 봐야할 듯 )

크롬에서는 아폴로 개발툴로 확인할 수 있다.

 

mutation 하기

useMutation

- 변수를 사용하여 addPerson 하기

const CREATE_PERSON = gql`
mutation createPerson($name: String!, $street: String!, $city: String!, $phone: String) {
  addPerson(
    name: $name,
    street: $street,
    city: $city,
    phone: $phone
  ) {
    name
    phone
    id
    address {
      street
      city
    }
  }
}
`
const PersonForm = () => {
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')
  const [street, setStreet] = useState('')
  const [city, setCity] = useState('')

  // mutation !!! - 배열을 리턴
  const [ createPerson ] = useMutation(CREATE_PERSON)

  const submit = (event) => {
    event.preventDefault()

	// mutation !!!
    createPerson({  variables: { name, phone, street, city } })

    setName('')
    setPhone('')
    setStreet('')
    setCity('')
  }

  return (
    <div>
      <h2>create new</h2>
      <form onSubmit={submit}>
        <div>
          name <input value={name}
            onChange={({ target }) => setName(target.value)}
          />
        </div>
        <div>
          phone <input value={phone}
            onChange={({ target }) => setPhone(target.value)}
          />
        </div>
        <div>
          street <input value={street}
            onChange={({ target }) => setStreet(target.value)}
          />
        </div>
        <div>
          city <input value={city}
            onChange={({ target }) => setCity(target.value)}
          />
        </div>
        <button type='submit'>add!</button>
      </form>
    </div>
  )
}

export default PersonForm

- useMutation 훅: 배열 리턴. [0] - mutation 호출하는 함수.

 

그런데 추가해도 화면 변화가 없다. Apollo Client가 자동으로 캐시를 업데이트 되지 않기 때문이다. 여전히 mutation 전의 state를 갖고 있다.

새로고침하면 캐시가 비워지므로 화면이 변화되는 것을 확인할 수 있다. 하지만 더 나은 방법 찾자.

 

캐시 업데이트

방법 #1 Poll

모든 persons에 대한 쿼리를 서버로 POLL ( www.apollographql.com/docs/react/data/queries/#polling혹은 반복해서 쿼리를 생성하기

- 2초마다 poll

const App = () => {
  const result = useQuery(ALL_PERSONS, {
    pollInterval: 2000
  })

  if (result.loading)  {
    return <div>loading...</div>
  }

  return (
    <div>
      <Persons persons = {result.data.allPersons}/>
      <PersonForm />
    </div>
  )
}

export default App

+ 유저가 새로운 사람을 추가할 때마다 마치 화면에 즉시 반영되는 것처럼 보인다.

- 불필요한 웹 트래픽

 

방법 #2 useMutationrefetchQueries 파라미터 설정

새로운 person이 생성될 때마다 모든 persons를 가져오는 쿼리가 실행된다.

const ALL_PERSONS = gql`
  query  {
    allPersons  {
      name
      phone
      id
    }
  }
`

const PersonForm = (props) => {
  // ...

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [ { query: ALL_PERSONS } ]
  })

+ 추가 웹 트래픽 없다.

- 유저 한명이 서버의 state를 업데이트 하면 다른 유저들에게 그 변화가 보이지 않는다.

 

mutation 에러 핸들링

useMutation 훅의 onError 옵션 설정

const PersonForm = ({ setError }) => {
  // ... 

  const [ createPerson ] = useMutation(CREATE_PERSON, {
    refetchQueries: [  {query: ALL_PERSONS } ],
    onError: (error) => {
      setError(error.graphQLErrors[0].message)
    }
  })

  // ...
}
const App = () => {
  const [errorMessage, setErrorMessage] = useState(null)

  const result = useQuery(ALL_PERSONS)

  if (result.loading)  {
    return <div>loading...</div>
  }

  const notify = (message) => {
    setErrorMessage(message)
    setTimeout(() => {
      setErrorMessage(null)
    }, 10000)
  }

  return (
    <div>
      <Notify errorMessage={errorMessage} />
      <Persons persons = {result.data.allPersons} />
      <PersonForm setError={notify} />
    </div>
  )
}

const Notify = ({errorMessage}) => {
  if ( !errorMessage ) {
    return null
  }
  return (
    <div style={{color: 'red'}}>
    {errorMessage}
    </div>
  )
}

 

phone number 업데이트

쿼리 추가

export const EDIT_NUMBER = gql`
  mutation editNumber($name: String!, $phone: String!) {
    editNumber(name: $name, phone: $phone)  {
      name
      phone
      address {
        street
        city
      }
      id
    }
  }
`
import React, { useState } from 'react'
import { useMutation } from '@apollo/client'

import { EDIT_NUMBER } from '../queries'

const PhoneForm = () => {
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')

  // useMutation Hook 추가
  const [ changeNumber ] = useMutation(EDIT_NUMBER)

  const submit = (event) => {
    event.preventDefault()

	// mutation 함수 호출
    changeNumber({ variables: { name, phone } })

    setName('')
    setPhone('')
  }

  return (
    <div>
      <h2>change number</h2>

      <form onSubmit={submit}>
        <div>
          name <input
            value={name}
            onChange={({ target }) => setName(target.value)}
          />
        </div>
        <div>
          phone <input
            value={phone}
            onChange={({ target }) => setPhone(target.value)}
          />
        </div>
        <button type='submit'>change number</button>
      </form>
    </div>
  )
}

export default PhoneForm

- 느낌표 안 찍어서 계속 에러났다..

- 형식을 정말 잘 맞추어줘야 함

- 기존 정보가 수정되면 Persons 컴포넌트가 리스트를 렌더링하면서 자동으로 반영된다. 이는 각 person이 ID 타입 필드를 갖고 있기 때문이다. 따라서 mutation 통해 정보가 업데이트 되면 캐시에 저장된 person의 세부사항이 자동으로 업데이트된다.

 

# 존재하지 않은 사람의 전화번호를 수정해도 아무 반응이 없는 문제

- mutation 응답이 null

=> useMutation의 리턴 값인 result 필드 값을 가지고 error 메세지를 만들면 된다.

const PhoneForm = ({ setError }) => {
  const [name, setName] = useState('')
  const [phone, setPhone] = useState('')

  const [ changeNumber, result ] = useMutation(EDIT_NUMBER)

  const submit = (event) => {
    // ...
  }

  useEffect(() => {
    if (result.data && result.data.editNumber === null) {
      setError('person not found')
    }
  }, [result.data])

  // ...
}

(However this solution does not work if the notify-function is not wrapped to a useCallback-function. If it's not, this results to an endless loop. When the App component is rerendered after a notification is removed, a new version of notify gets created which causes the effect function to be executed which causes a new notification and so on an so on...)

 

Apollo Client와 어플리케이션의 state

위의 예시에서 어플리케이션의 state는 주로 Apollo Client가 관리한다. 이는 GraphQL 어플리케이션의 전형적인 솔루션이기도 하다. 이 예시에서 리액트 컴포넌트의 state는 form state를 관리하고 error를 보여주기 위해서만 사용한다. 이처럼 GraphQL을 사용하여 어플리케이션의 state를 관리하면 굳이 Redux를 쓸 이유는 없다. 

필요한 경우에는 Apollo가 어플리케이션의 로컬 state를 Apollo cache에 저장하도록 한다.

'GraphQL' 카테고리의 다른 글

Fullstack Part 8 #5 Fragments and subscriptions  (0) 2021.03.12
Fullstack Part 8 #4 Login & Cache update  (0) 2021.03.11
Fullstack Part 8 #3 DB  (0) 2021.03.10
Fullstack Part 8 #1 GraphQL-server  (0) 2021.03.08
REST와 GraphQL  (0) 2021.03.08

댓글