5 Client - GraphQL 통신
source: categories/study/react_restapi_graphql/react_restapi_graphql6.md
5.1 ReactQuery 환경세팅
지난시간에 GraphQL
server
쪽 작성한 것을 바탕으로 이번시간에 client
쪽에서 작업을 이어가도록 하겠습니다.
cd client/
yarn add graphql graphql-request graphql-tag react-query
graphql
graphql-request
: API 호출용 라이브러리graphql-tag
: graphql 언어를 javascript 언어로 치환해주는 라이브러리react-query
: graphql 관리용 라이브러리
react-query
를 사용하기위해선 초기화 작업이 필요합니다.
client/pages/_app.js
초기화 작업을 client/pages/_app.js
파일에서 해줍니다.
// client/pages/_app.js
+ import {QueryClient, QueryClientProvider} from "react-query";
import './index.scss'
import {useRef} from "react";
// next가 서버사이드 렌더링을 하기위해 필요한 컴포넌트입니다.
// 그래서 아래와 같은 기본 공식이 있습니다.
// 기본 공식 그대로 코드를 작성하시면됩니다.
// 이 App이라는 컴포넌트가 한번만 실행되고 말것이아니라 페이지 전환이 될 때마다 매번 호출이 될거기 때문에 그때마다 queryClient가 새로 만들어지면 낭비가됩니다.
const App = ({ Component, pageProps }) => {
// client/hooks에서 useInfiniteScroll 작업할 때와 마찬가지로 최초에 한번만 작성을하고 이후에는 그거를 재활용할 수 있게끔 ref를 이용해보도록 하겠습니다.
// 아래처럼 clientRef.current에 아무것도 없을 때만 new QueryClient()를 해줍니다.
- const queryClient = new QueryClient()
+ const clientRef = useRef(null)
+ const getClient = () => {
+ if (!clientRef.current) clientRef.current = new QueryClient()
+ return clientRef.current
+ }
return (
- <Component {...pageProps} />
+ <QueryClientProvider client={getClient()}>
+ <Component {...pageProps} />
+ </QueryClientProvider>
)
}
// ctx는 context의 줄임말입니다.
App.getInitialProps = async ({ctx, Component}) => {
// 각 컴포넌트별로 getInitialProps가 정의되어있을 때, ctx를 넘겨서
const pageProps = await Component.getInitialProps?.(ctx)
// 그거에대한 pageProps를 return합니다.
// 이 pageProps를 가지고 각각의 컴포넌트를 구성하는 형태인 것 같습니다.
return { pageProps }
}
export default App
client/fetcher.js
fetcher.js
의 파일명을 바꾸도록하겠습니다.
queryClient.js
로 바꾸도록하겠습니다.
fetcher.js
파일명을 위와 같이 queryClient.js
로 바꿔줍니다.
이름을 바꾼 이유는 예전에는 fetcher
기능만을 파일에서 사용했다면,
지금은 react query
를 위한 유틸리티성 기능들을 모아둘거기 때문입니다.
이름을 바꾼 이유는 예전에는 fetcher
기능만을 이 파일에서 사용했다면,
지금은 react query
를 위한 유틸리티성 기능들을 모아둘거기 때문입니다.
// client/queryClient.js
- import axios from "axios";
+ import {request} from "graphql-request"; // graphql-request의 request를 import 해줍니다.
// axios의 defaults의 baseURL에 'http://localhost:8000' <- 서버 url주소를 넣겠습니다.
// 이를 지정을 해줘야 뒤에 route만 지정해줘도 잘 인식합니다.
// 그렇지 않을 경우엔 axios.get('http://localhost:8000', ...) 이런식으로 해야됩니다.
// 그런데 baseURL을 지정하고나면 axios.get('', ...) 이렇게 위 부분이 없어도됩니다.
- axios.defaults.baseURL = 'http://localhost:8000'
+ const URL = 'http://localhost:8000/graphql'; // graphql에선 baseURL이 필요가 없죠? 오직 하나만 있으면됩니다.
// fetcher는 axios를 좀 더 편하게 쓰기위해서 만든것입니다.
- const fetcher = async (method, url, ...rest) => {
- const res = await axios[method](url, ...rest)
- return res.data
- }
// fetcher의 내용을 통으로 갈아엎읍시다.
// 인자로 query와 variables를 받아서 request를 호출하겠습니다.
+ export const fetcher = (query, variables = {}) => request()
// 위의 함수에서 3번째 인자인 ...rest의 의미는 다음과 같습니다.
/*
get: axios.get(url[, config]) // get과 delete는 첫번째 인자로 url을 받고 그 뒤에 옵션값 config를 받습니다.
delete: axios.delete(url[, config]) // config는 기타 설정에 대해 들어오는 부분이라고 보시면됩니다.
post: axios.post(url, data[, config]) // post나 put 같은 경우는 data를 update하거나 새로 create해야되기 때문에 새로운 값이 담긴 data가 반드시 와야합니다.
put: axios.put(url, data[, config]) // 그렇기 때문에 get이나 delete와 다르게 method의 파라미터로 들어가야할 인자값이 하나가 더 있는 것입니다.
*/
// 이런 2가지 경우를 모두 대비하기위해서 인자가 1개만 들어오거나 2개가 들어오거나 모두 처리할 수 있게끔 ...rest 인자로 설정해놓은 것입니다.
- export default fetcher
graphql-request
라이브러리에서 불러온 request
함수의 인자값에 위와 같은 내용이 들어옵니다.
url
:string
,url
이string
형태로 들어옵니다.document
: 이 부분에query
가 오면됩니다.variables?
: 변수가 있을 경우에variables
에 넣습니다.
// 아래처럼 첫번째 인자: url
// 두번째 인자: query
await request('https://foo.bar/graphql', ``
{
query {
users
}
}
)
client/pages/index.js
위와 같이 수정합니다.
client/queryClient.js
에선
consr URL = 'http://localhost:8000/graphql'
export const fetcher = (query, variables = {}) => request(URL, query, variables);
client/pages/index.js
에선
import {fetcher} from '../queryClient'
const smsgs = await fetcher(GET_MESSAGES);
const users = await fetcher(GET_USERS);
// client/pages/index.js
import MsgList from "../components/MsgList";
// 이 index 파일에서 데이터를 불러오기 위해 fetcher를 불러옵니다.
- import fetcher from "../fetcher";
+ import {fetcher} from "../queryClient";
// 아래와 같이 return하면 Home에서 smsgs 프롭을 받아올 수 있습니다.
const Home = ({smsgs, users}) => (
<>
<h1>SIMPLE SNS</h1>
<MsgList smsgs={smsgs} users={users} />
</>
)
export const getServerSideProps = async () => {
// 여기서는 cursor 안 보내도 된다.
- const smsgs = await fetcher('get', '/messages')
+ const smsgs = await fetcher(GET_MESSAGES)
- const users = await fetcher('get', '/users')
+ const users = await fetcher(GET_USERS)
return {
// 이렇게 return하면
props: {smsgs, users}
}
}
export default Home;
이제 GET_MESSAGES
와 GET_USERS
를 만들기 위해 graphql
로 가야겠죠?
client/graphql/messages.js
client/graphql/messages.js
파일을 생성합니다.
그리고 server
쪽의 schema
를 보면서 해당 모양대로 만듭니다.playground
에서 작성한 방식들도 참고하시면 될거같습니다.
// client/graphql/messages.js
// 서버쪽에서는 apollo-server-express 라이브러리를 불러왔었습니다.
// import {gql} from 'apollo-server-express'
// 위 라이브러리는 apollo server에서 쓰는 graphql 명령어를 자바스크립트 언어로 컨버팅해주는 역할을 합니다.
// client 쪽에서는 graphql-tag 라이브러리를 사용하겠습니다.
import {gql} from 'graphql-tag';
export const GET_MESSAGES = gql`
`
// client/graphql/messages.js
import {gql} from 'graphql-tag';
// query 다음엔 아무 이름이나 오면되는데, 보통은 변수 이름과 일치시켜줍니다.
// 아래는 GET_MESSAGES라는 변수명을 썼으므로 query 옆에도 GET_MESSAGES라고 작성합니다.
export const GET_MESSAGES = gql`
query GET_MESSAGES {
messages {
id
text
userId
timestamp
}
}
`
messages
의 반환값은 배열이지만 요청할 땐 배열 요소 하나의 정보에 대해서만 적어주면됩니다.
만약 timestamp
정보는 빼고싶다면, timestamp
는 안적으면됩니다.
이것이 REST API
와 가장 큰 차이점입니다.
GET - messages, message
// client/graphql/messages.js
// 서버 쪽에서는 아래 라이브러리를 불러왔었습니다.
// import {gql} from 'apollo-server-express'
// 위 라이브러리는 apollo server에서 쓰는 graphql 명령어를 자바스크립트 언어로 컨버팅해주는 역할을 합니다.
// client쪽에서는 graphql-tag 라이브러리를 사용하겠습니다.
import {gql} from 'graphql-tag';
// query 다음엔 아무 이름이나 오면되는데, 보통은 변수이름과 일치시켜줍니다.
export const GET_MESSAGES = gql`
query GET_MESSAGES {
messages {
id
text
userId
timestamp
}
}
`
export const GET_MESSAGE = gql`
query GET_MESSAGE($id: ID!) {
message(id: $id) {
id
text
userId
timestamp
}
}
`
// GET 요청 만든김에 Mutaion쪽도 다 만든 다음에 넘어가도록 하겠습니다.
POST - create
// client/graphql/messages.js
// 서버 쪽에서는 아래 라이브러리를 불러왔었습니다.
// import {gql} from 'apollo-server-express'
// 위 라이브러리는 apollo server에서 쓰는 graphql 명령어를 자바스크립트 언어로 컨버팅해주는 역할을 합니다.
// client쪽에서는 graphql-tag 라이브러리를 사용하겠습니다.
import {gql} from 'graphql-tag';
// query 다음엔 아무 이름이나 오면되는데, 보통은 변수이름과 일치시켜줍니다.
export const GET_MESSAGES = gql`
query GET_MESSAGES {
messages {
id
text
userId
timestamp
}
}
`
export const GET_MESSAGE = gql`
query GET_MESSAGE($id: ID!) {
message(id: $id) {
id
text
userId
timestamp
}
}
`
// GET 요청 만든김에 Mutaion쪽도 다 만든 다음에 넘어가도록 하겠습니다.
export const CREATE_MESSAGE = gql`
mutation CREATE_MESSAGE($text: String!, $userId: ID!) {
createMessage(text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
PUT - update
// client/graphql/messages.js
// 서버 쪽에서는 아래 라이브러리를 불러왔었습니다.
// import {gql} from 'apollo-server-express'
// 위 라이브러리는 apollo server에서 쓰는 graphql 명령어를 자바스크립트 언어로 컨버팅해주는 역할을 합니다.
// client쪽에서는 graphql-tag 라이브러리를 사용하겠습니다.
import {gql} from 'graphql-tag';
// query 다음엔 아무 이름이나 오면되는데, 보통은 변수이름과 일치시켜줍니다.
export const GET_MESSAGES = gql`
query GET_MESSAGES {
messages {
id
text
userId
timestamp
}
}
`
export const GET_MESSAGE = gql`
query GET_MESSAGE($id: ID!) {
message(id: $id) {
id
text
userId
timestamp
}
}
`
// GET 요청 만든김에 Mutaion쪽도 다 만든 다음에 넘어가도록 하겠습니다.
export const CREATE_MESSAGE = gql`
mutation CREATE_MESSAGE($text: String!, $userId: ID!) {
createMessage(text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
export const UPDATE_MESSAGE = gql`
mutation UPDATE_MESSAGE($id: ID!, $text: String!, $userId: ID!) {
updateMessage(id: $id, text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
DELETE
// client/graphql/messages.js
// 서버 쪽에서는 아래 라이브러리를 불러왔었습니다.
// import {gql} from 'apollo-server-express'
// 위 라이브러리는 apollo server에서 쓰는 graphql 명령어를 자바스크립트 언어로 컨버팅해주는 역할을 합니다.
// client쪽에서는 graphql-tag 라이브러리를 사용하겠습니다.
import {gql} from 'graphql-tag';
// query 다음엔 아무 이름이나 오면되는데, 보통은 변수이름과 일치시켜줍니다.
export const GET_MESSAGES = gql`
query GET_MESSAGES {
messages {
id
text
userId
timestamp
}
}
`
export const GET_MESSAGE = gql`
query GET_MESSAGE($id: ID!) {
message(id: $id) {
id
text
userId
timestamp
}
}
`
// GET 요청 만든김에 Mutaion쪽도 다 만든 다음에 넘어가도록 하겠습니다.
export const CREATE_MESSAGE = gql`
mutation CREATE_MESSAGE($text: String!, $userId: ID!) {
createMessage(text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
export const UPDATE_MESSAGE = gql`
mutation UPDATE_MESSAGE($id: ID!, $text: String!, $userId: ID!) {
updateMessage(id: $id, text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
export const DELETE_MESSAGE = gql`
mutation DELETE_MESSAGE($id: ID!, $userId: ID!) {
deleteMessage(id: $id, userId: $userId)
}
`
deleteMessage(id: $id, userId: $userId)
deleteMessage() 자체가
ID가 반환되도록
schema/message.js`에서 정의가 되어있으므로 반환값을 따로 객체형태로 정의 안해도됩니다.
client/graphql/users.js
// client/graphql/users.js
import {gql} from 'graphql-tag';
export const GET_USERS = gql`
query GET_USERS {
users {
id
nickname
}
}
`
export const GET_USER = gql`
query GET_USER($id: ID!) {
user(id: $id) {
id
nickname
}
}
`
5.2 GraphQL 통신 기능 구현
client/pages/index.js
// client/pages/index.js
import MsgList from "../components/MsgList";
// 이 index 파일에서 데이터를 불러오기 위해 fetcher를 불러옵니다.
// import fetcher from "../fetcher";
import {fetcher} from "../queryClient";
import {GET_MESSAGES} from "../graphql/messages";
import {GET_USERS} from "../graphql/users";
// 아래와 같이 return하면 Home에서 smsgs 프롭을 받아올 수 있습니다.
const Home = ({smsgs, users}) => (
<>
<h1>SIMPLE SNS</h1>
<MsgList smsgs={smsgs} users={users} />
</>
)
export const getServerSideProps = async () => {
// 여기서는 cursor 안 보내도 된다.
// const smsgs = await fetcher('get', '/messages')
const smsgs = await fetcher(GET_MESSAGES)
// const users = await fetcher('get', '/users')
const users = await fetcher(GET_USERS)
console.log({smsgs, users}) // 일단 어떤 데이터값이 나오는지 출력을 해보겠습니다.
return {
// 이렇게 return하면
props: {smsgs: [], users: {}} // 일단 어떤 데이터가 갈지 모르겠으니까, 일단 빈배열, 빈객체로 내려줘봅시다.
}
}
export default Home;
이 자체로 실행이될지 아닐지 모르겠어서 client/components/MsgList.js
파일을 우선 수정해보도록 하겠습니다.
client/components/MsgList.js
파일에 기존에 REST API
형태로 작성한 요청 코드들이 있어서 에러가 날 수도 있을거 같습니다.
그래서 아예 실행안되도록 아래와 같이 return null;
로 막아버리고 실행을 해보도록 하겠습니다.
// client/components/MsgList.js
import MsgItem from "./MsgItem";
import MsgInput from "./MsgInput";
import {useEffect, useRef, useState} from "react";
import {useRouter} from "next/router";
import fetcher from "../queryClient.js";
import useInfiniteScroll from "../hooks/useInfiniteScroll";
const MsgList = ({smsgs, users}) => {
return null;
const {query} = useRouter();
const userId = query.userId || query.userid || '';
const [msgs, setMsgs] = useState(smsgs);
const [editingId, setEditingId] = useState(null);
const [hasNext, setHasNext] = useState(true);
const fetchMoreEl = useRef(null);
const intersecting = useInfiniteScroll(fetchMoreEl)
const onCreate = async text => {
const newMsg = await fetcher('post', '/messages', {text, userId})
if (!newMsg) throw Error('something wrong')
setMsgs(msgs => ([newMsg, ...msgs]))
}
const onUpdate = async (text, id) => {
const newMsg = await fetcher('put', `/messages/${id}`, { text, userId })
if (!newMsg) throw Error('something wrong')
setMsgs(msgs => {
const targetIndex = msgs.findIndex(msg => msg.id === id);
if (targetIndex < 0) return msgs;
const newMsgs = [...msgs]
newMsgs.splice(targetIndex, 1, newMsg);
return newMsgs;
})
doneEdit();
}
const onDelete = async (id) => {
const receivedId = await fetcher('delete', `/messages/${id}`, { params: { userId }})
setMsgs(msgs => {
const targetIndex = msgs.findIndex(msg => msg.id === receivedId + '');
if (targetIndex < 0) return msgs;
const newMsgs = [...msgs]
newMsgs.splice(targetIndex, 1)
return newMsgs;
})
}
const doneEdit = () => setEditingId(null)
const getMessages = async () => {
const newMsgs = await fetcher('get', '/messages', {params: {cursor: msgs[msgs.length - 1]?.id || ''}})
if (newMsgs.length === 0) {
setHasNext(false);
return
}
setMsgs([...msgs, ...newMsgs])
}
useEffect(() => {
if (intersecting && hasNext) getMessages()
}, [intersecting])
console.log('render')
return (
<>
{userId && <MsgInput mutate={onCreate}/>}
<ul className='messages'>
{
msgs.map(x => <MsgItem key={x.id}
{...x}
onUpdate={onUpdate}
onDelete={() => onDelete(x.id)}
startEdit={() => setEditingId(x.id)}
isEditing={editingId === x.id}
myId={userId}
user={users[x.userId]}
/>)
}
</ul>
<div ref={fetchMoreEl} />
</>
)
}
export default MsgList;
터미널을 두개 열고 각각 root
폴더에서 클라이언트와 서버 모두 실행합니다.
yarn run client
yarn run server
client/pages/index.js
에서 getServerSideProps
부분에서 smsgs
를 그대로 받았더니 smsgs
안에 messages
라는게 또 들어가있습니다.
여기서 정리한번!
GRAPHQL GET MESSAGES
GRAPHQL GET USERS
위와 같이 messages
로 구조분해할당을한 다음에 smsgs
로 변수명을 바꿔줍니다.
users
또한 users
안에 users
가 또 들어가있는 것을 볼 수 있습니다.messages
와 동일하죠?
그래서 users
도 구조분해할당을 통해 담습니다.
위 상태에서 다시 새로고침을하면,
이제 위와 같이 smsgs
에 메시지들이 배열형태로 들어온 것을 확인할 수 있습니다.
users
도 위와 같이 배열 형태로 들어와있습니다.
이제 다시 client/pages/index.js
파일을 원래대로 돌려놓으면 됩니다.
// client/pages/index.js
import MsgList from "../components/MsgList";
// 이 index 파일에서 데이터를 불러오기 위해 fetcher를 불러옵니다.
// import fetcher from "../fetcher";
import {fetcher} from "../queryClient";
import {GET_MESSAGES} from "../graphql/messages";
import {GET_USERS} from "../graphql/users";
// 아래와 같이 return하면 Home에서 smsgs 프롭을 받아올 수 있습니다.
const Home = ({smsgs, users}) => (
<>
<h1>SIMPLE SNS</h1>
<MsgList smsgs={smsgs} users={users} />
</>
)
export const getServerSideProps = async () => {
// 여기서는 cursor 안 보내도 된다.
// const smsgs = await fetcher('get', '/messages')
const {messages: smsgs} = await fetcher(GET_MESSAGES)
// const users = await fetcher('get', '/users')
const {users} = await fetcher(GET_USERS)
return {
// 이렇게 return하면
props: {smsgs, users}
}
}
export default Home;
위 상태에서 다시 새로고침을해보면,
MsgList
컴포넌트까지 smsgs
, users
정보들이 잘 들어와있는 것을 확인할 수 있습니다.
client/components/MsgList.js - GET
REST API
에서의 단점은 사용자가 직접 어떤 서버로의 API 통신을 호출하는 명령을 직접 구현해놓고(const getMessages = async () => {...}
)
그거를 어떤 상황이 처할 때마다 호출하게끔 작성을 해줬어야 했습니다. (useEffect
로..)
그런데 GraphQL
에서 제공하는 여러가지 써드파티 라이브러리.. 특히 그중에서도 많이 쓰이고있는 apollo
나 react-query
나 swr
같은 애들은
그렇게하지 않고 useQuery
라는 hook
을 이용해서, 적절한 시점에, 어떤 변수가 변경이 되었을 때 알아서 호출을 해주게끔 되어있습니다.useQuery
에서 지정한 variables
의 값이 변하면 그때마다 새로 호출을 하게됩니다.useQuery
를 useEffect
사용하는 느낌으로 똑같이 작성하면됩니다.
import {useQuery} from "react-query";
:useQuery
를 사용하기 위해react-query
를 불러옵니다.
useQuery
작성을 위해 useEffect() => {...}
부분을 주석처리합니다.
const {data, error, isError} = useQuery();
:useQuery
를 호출하면data
,error
,isError
값들이 오게됩니다.
useQuery
함수의 반환값이 위와 같이 마우스 커서 올려놓으면 나오는데, UseQueryResult
라는 타입입니다.
이 UseQueryResult
타입을 열어보겠습니다.
UseQueryResult
타입을 타고들어가면 UseQueryResult
는 UseBaseQueryResult
타입과 연결되어있다는 것을 알 수 있고UseBaseQueryResult
는 QueryObserverResult
타입과 연결되어있다는 것을 알 수 있습니다.QueryObserverResult
타입엔
data
error
isError
isIdle
isLoading
isLoadingError
isRefetchError
isSuccess
status
이런 애들이 있습니다.
얘네들이 useQuery()
에 대한 응답값으로 오는애들입니다.status
값으로는 어떤 종류들이 있는지도 알 수 있습니다.
위와 같이 status
값으로 idle
, loading
, error
, success
값이 있다는 것을 알 수 있습니다.
우리가 필요한건 data
, error(에러메시지)
, isError(현재 에러상태인지 아닌지)
이렇게 세개입니다.isLoading
은 로딩중인지 아닌지..
이런 값들 중에 원하는 값 가져다쓰시면됩니다.
일단은 const {data, error, isError} = useQuery();
이렇게만 가지고오겠습니다.
아래와 같이 GET_MESSAGES
를 import
하고
import {GET_MESSAGES} from "../graphql/messages";
const {data, error, isError} = useQuery(GET_MESSAGES);
위와 같이 GET_MESSAGES
를 인자로 넘겨줍니다.useQuery
를 날리는 방식이 여러가지가 있습니다.
이 부분도 다시 정의로 들어가서 보도록 하겠습니다.
useQuery
에는 위와 같이 방식이 3가지가 있습니다.
위와 같이 값으로 들어가는걸보면, 처음에 options
만 오는게 있고,
또 하나는 queryKey
와 options
가 오는 것이 있고,
다른 하나는 queryKey
, queryFn
, options
이렇게 3개가 오는 것이 있습니다.
이 3가지 중에 한가지를 선택해서 쓰면 됩니다.
graphQL
을 쓰는 경우에는 queryKey
가 반드시 있어야됩니다. 이게 react-query
에서 제공하는 방식이라서..
const {data, error, isError} = useQuery('MESSAGES', GET_MESSAGES);
위의 useQuery
의 첫번째 인자로 넘긴 'MESSAGES'
문자열을 가지고 createMessage
, updateMessage
, deleteMessage
하도록 같은 queryKey
를 씁니다.
그러면 요청했다가 응답온것이 바로 'MESSAGES'
에 반영되게끔 할 수 있습니다.
이를 client/queryClient.js
파일에 작성해놓겠습니다.
// client/queryClient.js
// import axios from "axios";
import {request} from "graphql-request"; // graphql-request의 request를 import 해줍니다.
// axios의 defaults의 baseURL에 'http://localhost:8000' <- 서버 url주소를 넣겠습니다.
// 이를 지정을 해줘야 뒤에 route만 지정해줘도 잘 인식합니다.
// 그렇지 않을 경우엔 axios.get('http://localhost:8000', ...) 이런식으로 해야됩니다.
// 그런데 baseURL을 지정하고나면 axios.get('', ...) 이렇게 위 부분이 없어도됩니다.
// axios.defaults.baseURL = 'http://localhost:8000'
const URL = 'http://localhost:8000/graphql'; // graphql에선 baseURL이 필요가 없죠? 오직 하나만 있으면됩니다.
// fetcher는 axios를 좀 더 편하게 쓰기위해서 만든것입니다.
// const fetcher = async (method, url, ...rest) => {
// const res = await axios[method](url, ...rest)
// return res.data
// }
// fetcher의 내용을 통으로 갈아엎읍시다.
// 인자로 query와 variables를 받아서 request를 호출하겠습니다.
export const fetcher = (query, variables = {}) => request(URL, query, variables)
// 위의 함수에서 3번째 인자인 ...rest의 의미는 다음과 같습니다.
/*
get: axios.get(url[, config]) // get과 delete는 첫번째 인자로 url을 받고 그 뒤에 옵션값 config를 받습니다.
delete: axios.delete(url[, config]) // config는 기타 설정에 대해 들어오는 부분이라고 보시면됩니다.
post: axios.post(url, data[, config]) // post나 put 같은 경우는 data를 update하거나 새로 create해야되기 때문에 새로운 값이 담긴 data가 반드시 와야합니다.
put: axios.put(url, data[, config]) // 그렇기 때문에 get이나 delete와 다르게 method의 파라미터로 들어가야할 인자값이 하나가 더 있는 것입니다.
*/
// 이런 2가지 경우를 모두 대비하기위해서 인자가 1개만 들어오거나 2개가 들어오거나 모두 처리할 수 있게끔 ...rest 인자로 설정해놓은 것입니다.
// export default fetcher
// 아래와 같이 상수로 사용하기 위한 것들을 정의해둡니다.
export const QueryKeys = {
MESSAGES: 'MESSAGES',
MESSAGE: 'MESSAGE',
USERS: 'USERS',
USER: 'USER'
}
import {fetcher, QueryKeys} from "../queryClient.js";
const {data, error, isError} = useQuery(QueryKeys.MESSAGES, GET_MESSAGES);
위와 같이 수정해줍니다.
fetcher(GET_MESSAGES)
라고만 작성하면 이미 request()
를 날려버리는 것이기 때문에 그때그때마다 호출이되는 형태가 아니게되는겁니다.
따라서 위와 같이 함수형으로 작성해야됩니다.
함수의 결과가오면 안됩니다.
data
를 한번 출력해보도록 하겠습니다.
클라이언트에서 요청하는 MESSAGES
에 대한 data
.
fetchMoreEl
이 정의되지 않았다는 에러입니다.
현재 무한 스크롤은 구현 안했으므로 위 부분을 주석처리합니다.
user.nickname
nickname
이란 프로퍼티를 읽을 수 없다는 에러입니다.
REST API
에선 users
가 {}
객체였습니다.GraphQL
에선 users
가 []
배열로 들어오도록 되어있습니다.
REST API
에선 user={users[x.userId]}
로 넘겼다면,
GraphQL
에선 user={usrs.find(x => userId === x.id)}
로 넘겨야됩니다.
// GraphQL에선 users 데이터가 아래와 같은 배열 형태로 넘어오도록 했으므로 위와 같이 수정해야됩니다.
[
{"id": "roy", "nickname": "로이"},
{"id": "jay", "nickname": "제이"}
]
응? 알수없는 에러 발생…
계속 주기적으로 graphql
요청이 발생함..
그리고 에러도나고..
에러나면 화면 렌더링이 위와 같이 바뀌고.. 음….
여기서 다시 주의~! server/src/index.js
에서 cors
의 origin
에서 뒤에 /
슬래시 붙이면안돼!!
휴 이제 접속 잘됩니다.
현재는 위와 같이 Home
컴포넌트에 들어온 smsgs
에 의해서
위와 같이 메시지들이 화면에 뿌려졌습니다.
최초 접속시 graphql
요청이 잘 들어갑니다.
최초 접속시 보내는 GET_MESSAGES
요청은 Variables
는 안보내므로 Variables
에 빈객체{}
가 넘어갑니다.
data
응답값을 보면 messages
로 한번 감싸져있고 들어온 데이터 형태는 REST API
때랑 똑같습니다.
그런데 마우스 커서로 화면을 찍거나(window focus
) 다른 곳을 찍거나 그러면 graphql
요청이 계속해서 발생합니다.
이것이 react-query
에서 제공하는 여러 기능들중 하난데, window focus
시 다시 refetch
를 할것인지 말것인지 여부를 결정하는 건데,default
값이 true
로 되어있습니다.
이를 false
로 바꿔보겠습니다.
// client/pages/_app.js
import {QueryClient, QueryClientProvider} from "react-query";
import './index.scss'
// Hydrate는 서버사이드에서 받아온 프로퍼티를 데이터가없이 HTML만 남아있는 어떤 클라이언트쪽 HTML에다가 그 데이터 정보들을 부어준다라는 느낌으로 이해하시면 됩니다.
// Hydrate가 수분을 제공한다 라는 뜻인데, 그런 느낌으로 이해하시면됩니다.
import {Hydrate} from "react-query/hydration";
import {useRef} from "react";
// next가 서버사이드 렌더링을 하기위해 필요한 컴포넌트입니다.
// 그래서 아래와 같은 기본 공식이 있습니다.
// 기본 공식 그대로 코드를 작성하시면됩니다.
// 이 App이라는 컴포넌트가 한번만 실행되고 말것이아니라 페이지 전환이 될 때마다 매번 호출이 될거기 때문에 그때마다 queryClient가 새로 만들어지면 낭비가됩니다.
const App = ({ Component, pageProps }) => {
// client/hooks에서 useInfiniteScroll 작업할 때와 마찬가지로 최초에 한번만 작성을하고 이후에는 그거를 재활용할 수 있게끔 ref를 이용해보도록 하겠습니다.
// 아래처럼 clientRef.current에 아무것도 없을 때만 new QueryClient()를 해줍니다.
// const queryClient = new QueryClient()
const clientRef = useRef(null)
const getClient = () => {
// new QueryClient()에 인자로 옵션값을 줄 수 있습니다.
// window focus시 refetch하는 것이 default 값으로 true로 설정되어있는데,
// 이를 false로 바꿔보도록 하겠습니다.
if (!clientRef.current) clientRef.current = new QueryClient({
defaultOptions: {
queries: {
// 여기에 들어갈 수 있는 정보들이 아래와 같이 여러가지가 있습니다.
// 이에 대한 설명은 react-query 사이트가서 필요하신 것들을 찾아보시면됩니다.
// refetchInterval: 1000, // <- 이렇게주면 1초에 무조건 다시한번 가져오는 것입니다.
// refetchOnMount,
refetchOnWindowFocus: false, // <- 위에서 말한걸 설정하기위해서 여기를 false로 주면 될 거 같다.
// refetchOnReconnect, // <- 서버와의 연결이 끊겼다가 다시 연결되었을 때,
// staleTime
}
}
})
return clientRef.current
}
return (
<QueryClientProvider client={getClient()}>
<Hydrate state={pageProps.dehydrateState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
// ctx는 context의 줄임말입니다.
App.getInitialProps = async ({ctx, Component}) => {
// 각 컴포넌트별로 getInitialProps가 정의되어있을 때, ctx를 넘겨서
const pageProps = await Component.getInitialProps?.(ctx)
// 그거에대한 pageProps를 return합니다.
// 이 pageProps를 가지고 각각의 컴포넌트를 구성하는 형태인 것 같습니다.
return { pageProps }
}
export default App
refetchOnWindowFocus
옵션값을 false
로 바꿉니다.
위와 같이 옵션값을 수정하면 window focus
가 발생해도 더이상 graphql
요청을 하지 않습니다.
MsgList
에서 message list
불러오는 거 까지 성공했습니다.
client/components/MsgList.js - GraphQL CREATE
create
, update
, delete
도 마찬가지로 REST API
처럼 onCreate
, onUpdate
, onDelte
이렇게 메소드를 만들어서 호출하는 형식이 아니고 Mutation
에 대한 명령어를 미리 만들어놓고 Mutation
명령어를 onCreate
라고해서 내려주는 방식으로 작성합니다.
// client/components/MsgList.js
// ...
import {useMutation, useQuery, useQueryClient} from "react-query";
// ...
const MsgList = ({smsgs, users}) => {
const client = useQueryClient();
// ...
const {mutate: onCreate} = useMutation(({text}) => fetcher(CREATE_MESSAGE, {text, userId}), {
// create 요청이 성공했을 때 createMessage 값이 반환됩니다.
// createMessage가 응답으로 들어오게되는데, createMessage를 가지고 graphql이 클라이언트에서 들고있는 캐시 정보에다가
// createMessage 이 정보를 업데이트해주는 방식입니다.
// 그래서 이 클라이언트 정보를 가지고와서 거기에서 캐시 정보를 업데이트하는 명령을 내리기 위해서 '클라이언트를 가져와야합니다.'
onSuccess: ({createMessage}) => {
// useQueryClient를 이용해 아래와 같이 작성할 수 있습니다.
// 이때 queryKeys를 여기서 사용합니다.
client.setQueryData(QueryKeys.MESSAGES, old => {
// 새로운 데이터를 넘겨주면됩니다.
return {
// 이렇게 기존 데이터를 [...old.messages]로 펼쳐놓고
// 새로 들어올 메시지를 createMessage라고 넣겠습니다.
messages: [createMessage, ...old.messages]
}
})
}
})
}
// client/graphql/messages.js
// ...
// GET 요청 만든김에 Mutaion쪽도 다 만든 다음에 넘어가도록 하겠습니다.
export const CREATE_MESSAGE = gql`
mutation CREATE_MESSAGE($text: String!, $userId: ID!) {
createMessage(text: $text, userId: $userId) {
id
text
userId
timestamp
}
}
`
const client = useQueryClient()
그 QueryClient
에 접속하는 방법이 이 hook
을 이용하는 방법입니다.useQueryClient
를 써서 QueryClient
에 접속할 수 있습니다.
// client/pages/_app.js
import {QueryClient, QueryClientProvider} from "react-query";
import './index.scss'
// Hydrate는 서버사이드에서 받아온 프로퍼티를 데이터가없이 HTML만 남아있는 어떤 클라이언트쪽 HTML에다가 그 데이터 정보들을 부어준다라는 느낌으로 이해하시면 됩니다.
// Hydrate가 수분을 제공한다 라는 뜻인데, 그런 느낌으로 이해하시면됩니다.
import {Hydrate} from "react-query/hydration";
import {useRef} from "react";
// next가 서버사이드 렌더링을 하기위해 필요한 컴포넌트입니다.
// 그래서 아래와 같은 기본 공식이 있습니다.
// 기본 공식 그대로 코드를 작성하시면됩니다.
// 이 App이라는 컴포넌트가 한번만 실행되고 말것이아니라 페이지 전환이 될 때마다 매번 호출이 될거기 때문에 그때마다 queryClient가 새로 만들어지면 낭비가됩니다.
const App = ({ Component, pageProps }) => {
// client/hooks에서 useInfiniteScroll 작업할 때와 마찬가지로 최초에 한번만 작성을하고 이후에는 그거를 재활용할 수 있게끔 ref를 이용해보도록 하겠습니다.
// 아래처럼 clientRef.current에 아무것도 없을 때만 new QueryClient()를 해줍니다.
// const queryClient = new QueryClient()
const clientRef = useRef(null)
const getClient = () => {
// new QueryClient()에 인자로 옵션값을 줄 수 있습니다.
// window focus시 refetch하는 것이 default 값으로 true로 설정되어있는데,
// 이를 false로 바꿔보도록 하겠습니다.
if (!clientRef.current) clientRef.current = new QueryClient({
defaultOptions: {
queries: {
// 여기에 들어갈 수 있는 정보들이 아래와 같이 여러가지가 있습니다.
// 이에 대한 설명은 react-query 사이트가서 필요하신 것들을 찾아보시면됩니다.
// refetchInterval: 1000, // <- 이렇게주면 1초에 무조건 다시한번 가져오는 것입니다.
// refetchOnMount,
refetchOnWindowFocus: false, // <- 위에서 말한걸 설정하기위해서 여기를 false로 주면 될 거 같다.
// refetchOnReconnect, // <- 서버와의 연결이 끊겼다가 다시 연결되었을 때,
// staleTime
}
}
})
return clientRef.current
}
return (
<QueryClientProvider client={getClient()}>
<Hydrate state={pageProps.dehydrateState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
// ctx는 context의 줄임말입니다.
App.getInitialProps = async ({ctx, Component}) => {
// 각 컴포넌트별로 getInitialProps가 정의되어있을 때, ctx를 넘겨서
const pageProps = await Component.getInitialProps?.(ctx)
// 그거에대한 pageProps를 return합니다.
// 이 pageProps를 가지고 각각의 컴포넌트를 구성하는 형태인 것 같습니다.
return { pageProps }
}
export default App
아까 GET
메소드를 만들때 QueryKeys.MESSAGES
를 이용한 것처럼 여기서도 queryKeys
를 사용합니다.
위의 스크린샷은 잘못된 내용입니다!!useQueryClient()
여기 소괄호 안에 작성하는 것이 아닙니다!!!const {mutate: onCreate} = ...
여기의 client.setQueryData()
여기 소괄호에 작성해야되는겁니다.
잘못된 내용이므로 감안하고 보셔야됩니다~!!!!!!
여튼 client.setQueryData()
소괄호 안에 QueryKeys.MESSAGES
를 첫번째 인자로 넣어주고,QueryKeys.MESSAGES
에 들어올 데이터는const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () => fetcher(GET_MESSAGES));
의 data
에 들어오는 형태랑 똑같습니다.data
라는 곳에 messages
가 있었죠?
// 이렇게 함수를 두번째인자로 넣어줍니다.
// 아래 QueryKeys.MESSAGES에 들어올 데이터 형태에 맞춰 return 값을 설정해줍니다.
old => {
return {
messages:
}
}
위 스크린샷도 잘못된것!!!! useQueryClient
소괄호 안의 내용이 onCreate
의 setQueryData()
여기로 들어가야됨!!!
// 기존 데이터를 ...old.messages로 펼쳐놓고
// 새로들어올 메시지를 createMessage로 넣습니다.
// 아까 GET_MESSAGES할 때 QueryKeys.MESSAGES 했잖아. 즉, QueryKeys.MESSAGES를 첫번째인자로하면 GET_MESSAGES가 실행되는 느낌인가?
// 그래서 그걸 가져와서 아래와 같이 create를 수행하는거지!
client.setQueryData(QueryKeys.MESSAGES, old => {
return {
messages: [createMessage, ...old.messages]
}
})
현재로써는 data
값이 변경되었을 때, 그 변경된 data
를 반영하는 구문이 없습니다.
아직은 data
만 와있는 상태이고, data
가 변경되었을시 반영하도록 state
를 변경하도록 합니다.
const MsgList = ({smsgs, users}) => {
// ...
const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () => fetcher(GET_MESSAGES)); // useQuery를 호출하면 data, error, isError 값들이 오게됩니다.
// 아직 data에 대해 state 관리하는게 없습니다. 변경된 data를 반영하는 구문이 없다는 뜻.
// 그래서 이 부분은 우선 useEffect로 구현해놓겠습니다.
useEffect(() => {
})
// ...
}
아직 data에 대해 state 관리하는게 없습니다.
그래서 이 부분은 우선 useEffect
로 구현해놓겠습니다.
그런데 현재 REST API
에 서버사이드 렌더링을 해놓은 상태에잖아요?
그 상태에서 smsgs
가 들어왔지만.. REST API
때에도 구현 안했었지만,Hydrate
를 통해 넘어온 데이터를 client
캐시에도 저장을 하도록 하겠습니다.
하이드레이트를 구현해보도록 하겠습니다.
하이드레이트 구현
Hydrate
는 서버사이드에서 받아온 프로퍼티를 데이터가 없이 HTML
만 남아있는 어떤 클라이언트에다가 그 데이터 정보들을 부어준다 라는 느낌으로 이해하시면 됩니다.Hydrate
가 수분을 제공한다 라는 뜻인데, 그런 느낌으로 이해하시면 됩니다.
아래와 같이 하면 서버사이드 랜더링에서 데이터가 온것이 react-query
의 캐시 정보에도 Hydrate
를 통해서 저장이됩니다.
// client/pages/_app.js
import {QueryClient, QueryClientProvider} from "react-query";
import './index.scss'
// Hydrate는 서버사이드에서 받아온 프로퍼티를 데이터가없이 HTML만 남아있는 어떤 클라이언트쪽 HTML에다가 그 데이터 정보들을 부어준다라는 느낌으로 이해하시면 됩니다.
// Hydrate가 수분을 제공한다 라는 뜻인데, 그런 느낌으로 이해하시면됩니다.
import {Hydrate} from "react-query/hydration";
import {useRef} from "react";
// next가 서버사이드 렌더링을 하기위해 필요한 컴포넌트입니다.
// 그래서 아래와 같은 기본 공식이 있습니다.
// 기본 공식 그대로 코드를 작성하시면됩니다.
// 이 App이라는 컴포넌트가 한번만 실행되고 말것이아니라 페이지 전환이 될 때마다 매번 호출이 될거기 때문에 그때마다 queryClient가 새로 만들어지면 낭비가됩니다.
const App = ({ Component, pageProps }) => {
// client/hooks에서 useInfiniteScroll 작업할 때와 마찬가지로 최초에 한번만 작성을하고 이후에는 그거를 재활용할 수 있게끔 ref를 이용해보도록 하겠습니다.
// 아래처럼 clientRef.current에 아무것도 없을 때만 new QueryClient()를 해줍니다.
// const queryClient = new QueryClient()
const clientRef = useRef(null)
const getClient = () => {
// new QueryClient()에 인자로 옵션값을 줄 수 있습니다.
// window focus시 refetch하는 것이 default 값으로 true로 설정되어있는데,
// 이를 false로 바꿔보도록 하겠습니다.
if (!clientRef.current) clientRef.current = new QueryClient({
defaultOptions: {
queries: {
// 여기에 들어갈 수 있는 정보들이 아래와 같이 여러가지가 있습니다.
// 이에 대한 설명은 react-query 사이트가서 필요하신 것들을 찾아보시면됩니다.
// refetchInterval: 1000, // <- 이렇게주면 1초에 무조건 다시한번 가져오는 것입니다.
// refetchOnMount,
refetchOnWindowFocus: false, // <- 위에서 말한걸 설정하기위해서 여기를 false로 주면 될 거 같다.
// refetchOnReconnect, // <- 서버와의 연결이 끊겼다가 다시 연결되었을 때,
// staleTime
}
}
})
return clientRef.current
}
return (
<QueryClientProvider client={getClient()}>
<Hydrate state={pageProps.dehydrateState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
// ctx는 context의 줄임말입니다.
App.getInitialProps = async ({ctx, Component}) => {
// 각 컴포넌트별로 getInitialProps가 정의되어있을 때, ctx를 넘겨서
const pageProps = await Component.getInitialProps?.(ctx)
// 그거에대한 pageProps를 return합니다.
// 이 pageProps를 가지고 각각의 컴포넌트를 구성하는 형태인 것 같습니다.
return { pageProps }
}
export default App
위와 같이 Hydrate
를 통해 서버사이드 렌더링을 하면, 서버사이드에서 온 데이터 smsgs
라는 정보가 이미 있죠?
그리고
const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () => fetcher(GET_MESSAGES));
최초 접속시 useQuery
를 통해서 날리는 위 query
에서..
위 query
가 서버에 요청은 하긴하지만, 실제로 data
는 client
에 있는 것을 먼저쓰고.. 이를 stale
이라고 합니다.
stale
: 옛것. 미리 받아놓은 정보.
여튼 useQuery
를 통해 서버로 요청이가서 새로운 정보가 올거잖아요?
기존 정보, 서버사이드 렌더링 때 넘어온 smsgs
정보랑.. 새로 useQuery
를 통해 넘어오는 정보랑..
이 두 정보를 서로 비교를 합니다.
그때 다른점이 있다면 새걸로 갈아치우고 없다면 굳이 갈아치울 필요없이 옛날꺼(stale
)을 쓰면됩니다. 캐시에 있는 정보를.
그러니까 서버에서 가져온 정보가 이미 캐시에 담겨있는 상태에서
클라이언트에 접속하자마자 서버에서 다시 요청을 할 때,
그 정보는 요청은하고 응답은 받기는 하지만 캐시에 담겨있는 정보와 비교했을 때
이 data(useQuery로 새로 요청해서 받아온 것)
를 쓸 일은 현재로썬 없는 상태인겁니다.
// client/components/MsgList.js
const MsgList = ({smsgs, users}) => {
// ...
const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () => fetcher(GET_MESSAGES)); // useQuery를 호출하면 data, error, isError 값들이 오게됩니다.
// 아직 data에 대해 state 관리하는게 없습니다. 변경된 data를 반영하는 구문이 없다는 뜻.
// 그래서 이 부분은 우선 useEffect로 구현해놓겠습니다.
// 어쨌든 useEffect로 data에있는 messages가 변경이될 때,
useEffect(() => {
if (!data?.messages) return
console.log('msgs changed') // 이게 언제 호출이되는지 보도록 하겠습니다.
setMsgs(data?.messages) // setMsgs로 data의 messages 혹은 없을 때 빈배열[]을 넣도록 해주는 작업을 하겠습니다.
}, [data?.messages]) // data.messages가 변경되는지 아닌지 감시
// ...
}
저희가 지금 create
를 하다가말고 메시지 data
에 대해서 useEffect
를 하고있는데, 이러한 에러가 발생했습니다.onCreate
가 이미 선언되었다는 에러입니다.
기존 REST API
때 onCreate
메소드를 지워줍니다.
콘솔을 보시면 msgs changed
가 두번 호출이되었습니다.useEffect
안의 내용이 두번 실행되었다는 것입니다.
data
자체가 없을 때 한번 호출되고,data
에 응답값이 들어왔을 때, 한번 더 호출이됩니다.useEffect
로 [data?.messages]
를 감시하고있기 때문에 위와 같이 호출되는 거 같습니다.
if (!data?.messages) return
: data.messages
에 아무것도 없다면 return
해버리고data?.messages
에 값이 있을 때만 setMsgs()
를 하도록 합니다.
그러면 위와 같이 한번만 호출됩니다.
텍스트를 추가해보도록 하겠습니다.
텍스트를 추가하려고 완료 버튼을 눌렀는데 위와 같은 에러가 발생했습니다.$text
가 String!
타입으로 제공되어야하는데, 그렇게 제공되지 않았다는 에러가뜹니다.
여기서 코드를 잘못 작성했다는거 알아챔..
코드 수정해주고~!
아까 위의 에러를 해결해보자.
위의 MsgList
컴포넌트를 보면 onCreate
의 useMutation
의 첫번째인자, 즉, 함수의 인자값으로 text
를 전달했다.
그런데 이 text
를 객체분해할당{text}
으로 받도록했는데,
위와 같이 text
로 수정해줍시다.
이렇게 해줘도되는지 확인하는 방법은 MsgInput
컴포넌트를 보면됩니다.
MsgInput
컴포넌트를 보면 mutate(text, id);
이렇게 값을 넘기는 것을 볼 수 있습니다.
그런데 앞으로 또 바뀌어야하니깐 차라리 MsgInput
의 mutate(text, id)
를 mutate({text, id})
로 아예 객체로 보내도록 수정합시다.
위에도 다시 {text}
이렇게 객체로 바꿔줍니다.
이제 다시 새글을 등록(create
)하면 위와 같이 입력되는 것을 볼 수 있습니다.
위에서 컴포넌트를 살펴보면 Home
컴포넌트의 smsgs
를 보면…hydrate
가 일어나는 과정에서 Home
의 smsgs
는로이, graphql playground에서 작성..
여기까지만 서버에서 데이터가 왔다는 것을 볼 수 있죠?
MsgList
에선 새로 추가된 것까지 들어와있는게 확인됩니다.
이 상태에서 새로고침을하면 캐시에서 더 빠르게 정보를 가지고와서
캐시에서 더 빠르게 정보를 가져오긴하지만 graphql
요청을 하긴 합니다.
여튼 그래도 캐시에있는 정보를 더 우선적으로 적용을해서 더 빠르게 보여집니다.
GraphQL
에서 create mutation
까지 하는걸 확인했고, update
를 이어서 보겠습니다.
client/components/MsgList.js - GraphQL UPDATE
GraphQL
로 만든 put(update)
데이터 흐름, 구조도입니다.
// client/components/MsgList.js
// ...
const MsgList = ({smsgs, users}) => {
// ...
// update가 완료되었다는 것을 알려줍니다.
const doneEdit = () => setEditingId(null)
// 똑같이 mutate라는 명령어로 내려오게될텐데 이를 onUpdate라고 지정하면됩니다.
// 이때 필요한 정보는 text와 id였습니다. 이를 받아다가 fetcher로 똑같이 요청을 할겁니다.
// UPDATE_MESSAGE를 하고 이때 text와 id를 보냅니다.
const {mutate: onUpdate} = useMutation(({text, id}) => fetcher(UPDATE_MESSAGE, {text, id, userId}), {
// 이거에대해 성공했을 때 응답값이 schema에서 지정한대로 updateMessage라고하는 변수로 들어오게될겁니다.
onSuccess: ({updateMessage}) => {
// 마찬가지로 client에서 setQueryData를 해주면됩니다.
// 이때 똑같이 QueryKeys의 MESSAGES에 대해 업데이트해주면 됩니다.
client.setQueryData(QueryKeys.MESSAGES, old => {
// onSuccess가 콜백함수이므로 위의 id를 못 받아오기 때문에 아래와 같이 updateMessage.id라고 작성해준다.
// 흐음.. 그런데 아래와 같이 id해도 잘 받아오는거같은데.. 현재 내 문제는 그게아니다.
const targetIndex = old.messages.findIndex(msg => msg.id === updateMessage.id);
// targetIndex가 없을 경우 old를 반환해주면됩니다.
if (targetIndex < 0) return old;
const newMsgs = [...old.messages]
newMsgs.splice(targetIndex, 1, updateMessage);
return {messages: newMsgs} // 이렇게 반환해주면 되겠습니다.
})
doneEdit(); // 끝났을 때 doneEdit() 또한 기존처럼 호출하면 되겠습니다.
}
})
// ...
}
export default MsgList;
수정(put(update)
)을 하려고 수정하고 완료버튼을 눌렀는데, ID!
타입인 $userId
가 제공되지 않았다는 에러메시지가 떴습니다.
MsgInput
컴포넌트에서 값으로 {text, id}
만 넘겨주므로 받기는 text
와 id
만 받으면되지만,query
를 보낼때 userId
정보도 넘겨줘야합니다.useId
는 localhost:3000/?userId=jay
여기서 jay
를 추출한걸 말합니다.
id
값을 client.setQueryData()
에선 가져다쓰질 못합니다.
그 이유는 onSuccess
는 비동기 콜백 함수이기 때문입니다.
때문에 위와 같이 updateMessage.id
로 바꿔줍니다.
// client/components/MsgList.js
// ...
const MsgList = ({smsgs, users}) => {
// ...
// update가 완료되었다는 것을 알려줍니다.
const doneEdit = () => setEditingId(null)
// 똑같이 mutate라는 명령어로 내려오게될텐데 이를 onUpdate라고 지정하면됩니다.
// 이때 필요한 정보는 text와 id였습니다. 이를 받아다가 fetcher로 똑같이 요청을 할겁니다.
// UPDATE_MESSAGE를 하고 이때 text와 id를 보냅니다.
const {mutate: onUpdate} = useMutation(({text, id}) => fetcher(UPDATE_MESSAGE, {text, id, userId}), {
// 이거에대해 성공했을 때 응답값이 schema에서 지정한대로 updateMessage라고하는 변수로 들어오게될겁니다.
onSuccess: ({updateMessage}) => {
// 마찬가지로 client에서 setQueryData를 해주면됩니다.
// 이때 똑같이 QueryKeys의 MESSAGES에 대해 업데이트해주면 됩니다.
client.setQueryData(QueryKeys.MESSAGES, old => {
// onSuccess가 콜백함수이므로 위의 id를 못 받아오기 때문에 아래와 같이 updateMessage.id라고 작성해준다.
// 흐음.. 그런데 아래와 같이 id해도 잘 받아오는거같은데.. 현재 내 문제는 그게아니다.
const targetIndex = old.messages.findIndex(msg => msg.id === updateMessage.id);
// targetIndex가 없을 경우 old를 반환해주면됩니다.
if (targetIndex < 0) return old;
const newMsgs = [...old.messages]
newMsgs.splice(targetIndex, 1, updateMessage);
return {messages: newMsgs} // 이렇게 반환해주면 되겠습니다.
})
doneEdit(); // 끝났을 때 doneEdit() 또한 기존처럼 호출하면 되겠습니다.
}
})
// ...
}
export default MsgList;
위와 같은 실수 하지말자!!!!
위 코드작성 실수한거 수정했더니 이제서야 id
에러가 뜬다.
다시 비동기 콜백으로 실행되는 부분이기에 id
를 updateMessage.id
로 수정하고 다시 실행!
수정 아주 잘된다.
client/components/MsgList.js - GraphQL DELETE
delete
는 put(update)
와 거의 비슷합니다.
그대로 복붙하고 delete
에선 doneEdit()
은 없어도되니 삭제합니다.
// client/components/MsgList.js
// ...
const MsgList = ({smsgs, users}) => {
// ...
// update가 완료되었다는 것을 알려줍니다.
const doneEdit = () => setEditingId(null)
// 삭제 또한 onUpdate와 거의 같습니다.
// text는 필요 없을 것이고 id만 넘겨주고 fetcher함수에는 id와 userId를 모두 넘깁니다.
const {mutate: onDelete} = useMutation(id => fetcher(DELETE_MESSAGE, {id, userId}), {
// deleteMessage를 받아올텐데 이 이름을 deletedId라고 바꾸겠습니다.
onSuccess: ({deleteMessage: deletedId}) => {
client.setQueryData(QueryKeys.MESSAGES, old => {
const targetIndex = old.messages.findIndex(msg => msg.id === deletedId);
if (targetIndex < 0) return old; // 없으면 기존꺼 반환
const newMsgs = [...old.messages]
newMsgs.splice(targetIndex, 1); // 삭제
return {messages: newMsgs}
})
}
})
// ...
}
export default MsgList;
된거 같습니다. 확인해볼게요.
삭제버튼을 누르면 msgs changed
가 발생하면서 잘 삭제가 됩니다.
응답을 보면 deleteMessage
에 id
가 잘 담겨서 오는 것을 볼 수 있습니다.
삭제가 잘 됩니다.
GET
메소드는 지웁니다.REST API
에서 쓰던거므로..