- 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 useMutation의 refetchQueries 파라미터 설정
새로운 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 |
댓글