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로 바꾸도록하겠습니다.

Note

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

Note

graphql-request 라이브러리에서 불러온 request 함수의 인자값에 위와 같은 내용이 들어옵니다.

  • url: string, urlstring 형태로 들어옵니다.
  • document: 이 부분에 query가 오면됩니다.
  • variables?: 변수가 있을 경우에 variables에 넣습니다.

// 아래처럼 첫번째 인자: url
// 두번째 인자: query
await request('https://foo.bar/graphql', ``
    {
        query {
            users
        }
    }
)

client/pages/index.js

Note

위와 같이 수정합니다.

client/queryClient.js에선

consr URL = 'http://localhost:8000/graphql'
export const fetcher = (query, variables = {}) =&gt; 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_MESSAGESGET_USERS를 만들기 위해 graphql로 가야겠죠?

client/graphql/messages.js

Note

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
        }
    }
`

Note

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)
    }
`

Note

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

Note

client/pages/index.js에서 getServerSideProps 부분에서 smsgs를 그대로 받았더니 smsgs 안에 messages라는게 또 들어가있습니다.


여기서 정리한번!

GRAPHQL GET MESSAGES

GRAPHQL GET USERS


Note

위와 같이 messages로 구조분해할당을한 다음에 smsgs로 변수명을 바꿔줍니다.

Note

users 또한 users 안에 users가 또 들어가있는 것을 볼 수 있습니다.
messages와 동일하죠?

Note

그래서 users도 구조분해할당을 통해 담습니다.

위 상태에서 다시 새로고침을하면,

Note

이제 위와 같이 smsgs에 메시지들이 배열형태로 들어온 것을 확인할 수 있습니다.

Note

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;

위 상태에서 다시 새로고침을해보면,

Note

MsgList 컴포넌트까지 smsgs, users 정보들이 잘 들어와있는 것을 확인할 수 있습니다.

client/components/MsgList.js - GET

Note

REST API에서의 단점은 사용자가 직접 어떤 서버로의 API 통신을 호출하는 명령을 직접 구현해놓고(const getMessages = async () =&gt; {...})
그거를 어떤 상황이 처할 때마다 호출하게끔 작성을 해줬어야 했습니다. (useEffect로..)

그런데 GraphQL에서 제공하는 여러가지 써드파티 라이브러리.. 특히 그중에서도 많이 쓰이고있는 apolloreact-queryswr 같은 애들은
그렇게하지 않고 useQuery라는 hook을 이용해서, 적절한 시점에, 어떤 변수가 변경이 되었을 때 알아서 호출을 해주게끔 되어있습니다.
useQuery에서 지정한 variables의 값이 변하면 그때마다 새로 호출을 하게됩니다.
useQueryuseEffect 사용하는 느낌으로 똑같이 작성하면됩니다.

  • import {useQuery} from "react-query"; : useQuery를 사용하기 위해 react-query를 불러옵니다.

useQuery 작성을 위해 useEffect() =&gt; {...} 부분을 주석처리합니다.

Note
  • const {data, error, isError} = useQuery();: useQuery를 호출하면 data, error, isError 값들이 오게됩니다.

useQuery 함수의 반환값이 위와 같이 마우스 커서 올려놓으면 나오는데, UseQueryResult라는 타입입니다.
UseQueryResult 타입을 열어보겠습니다.

Note

UseQueryResult 타입을 타고들어가면 UseQueryResultUseBaseQueryResult 타입과 연결되어있다는 것을 알 수 있고
UseBaseQueryResultQueryObserverResult 타입과 연결되어있다는 것을 알 수 있습니다.
QueryObserverResult 타입엔

  1. data
  2. error
  3. isError
  4. isIdle
  5. isLoading
  6. isLoadingError
  7. isRefetchError
  8. isSuccess
  9. status

이런 애들이 있습니다.
얘네들이 useQuery()에 대한 응답값으로 오는애들입니다.
status 값으로는 어떤 종류들이 있는지도 알 수 있습니다.

Note

위와 같이 status 값으로 idle, loading, error, success 값이 있다는 것을 알 수 있습니다.

Note

우리가 필요한건 data, error(에러메시지), isError(현재 에러상태인지 아닌지) 이렇게 세개입니다.
isLoading은 로딩중인지 아닌지..
이런 값들 중에 원하는 값 가져다쓰시면됩니다.

일단은 const {data, error, isError} = useQuery(); 이렇게만 가지고오겠습니다.

Note

아래와 같이 GET_MESSAGESimport하고

import {GET_MESSAGES} from "../graphql/messages";

const {data, error, isError} = useQuery(GET_MESSAGES);

위와 같이 GET_MESSAGES를 인자로 넘겨줍니다.
useQuery를 날리는 방식이 여러가지가 있습니다.
이 부분도 다시 정의로 들어가서 보도록 하겠습니다.

Note

useQuery에는 위와 같이 방식이 3가지가 있습니다.

Note

위와 같이 값으로 들어가는걸보면, 처음에 options만 오는게 있고,
또 하나는 queryKeyoptions가 오는 것이 있고,
다른 하나는 queryKey, queryFn, options 이렇게 3개가 오는 것이 있습니다.

이 3가지 중에 한가지를 선택해서 쓰면 됩니다.

graphQL을 쓰는 경우에는 queryKey가 반드시 있어야됩니다. 이게 react-query에서 제공하는 방식이라서..

Note

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'
}

Note

import {fetcher, QueryKeys} from "../queryClient.js";

const {data, error, isError} = useQuery(QueryKeys.MESSAGES, GET_MESSAGES);

Note

위와 같이 수정해줍니다.

fetcher(GET_MESSAGES)라고만 작성하면 이미 request()를 날려버리는 것이기 때문에 그때그때마다 호출이되는 형태가 아니게되는겁니다.
따라서 위와 같이 함수형으로 작성해야됩니다.
함수의 결과가오면 안됩니다.

Note

data를 한번 출력해보도록 하겠습니다.
클라이언트에서 요청하는 MESSAGES에 대한 data.

Note

fetchMoreEl이 정의되지 않았다는 에러입니다.

Note

현재 무한 스크롤은 구현 안했으므로 위 부분을 주석처리합니다.

Note

user.nickname

nickname이란 프로퍼티를 읽을 수 없다는 에러입니다.

Note

REST API에선 users{} 객체였습니다.
GraphQL에선 users[] 배열로 들어오도록 되어있습니다.

REST API에선 user={users[x.userId]}로 넘겼다면,

Note

GraphQL에선 user={usrs.find(x =&gt; userId === x.id)}로 넘겨야됩니다.


// GraphQL에선 users 데이터가 아래와 같은 배열 형태로 넘어오도록 했으므로 위와 같이 수정해야됩니다. 
[
  {"id": "roy", "nickname": "로이"},
  {"id": "jay", "nickname": "제이"}
]

Note

응? 알수없는 에러 발생…

Note

계속 주기적으로 graphql 요청이 발생함..
그리고 에러도나고..
에러나면 화면 렌더링이 위와 같이 바뀌고.. 음….

Note

여기서 다시 주의~! server/src/index.js에서 corsorigin에서 뒤에 / 슬래시 붙이면안돼!!

Note

휴 이제 접속 잘됩니다.

Note

현재는 위와 같이 Home 컴포넌트에 들어온 smsgs에 의해서

Note

위와 같이 메시지들이 화면에 뿌려졌습니다.

Note

최초 접속시 graphql 요청이 잘 들어갑니다.

Note

최초 접속시 보내는 GET_MESSAGES 요청은 Variables는 안보내므로 Variables에 빈객체{}가 넘어갑니다.

Note

data 응답값을 보면 messages로 한번 감싸져있고 들어온 데이터 형태는 REST API때랑 똑같습니다.

Note

그런데 마우스 커서로 화면을 찍거나(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

Note

refetchOnWindowFocus 옵션값을 false로 바꿉니다.

Note

위와 같이 옵션값을 수정하면 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
        }
    }
`

Note

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

Note

아까 GET 메소드를 만들때 QueryKeys.MESSAGES를 이용한 것처럼 여기서도 queryKeys를 사용합니다.

Note

위의 스크린샷은 잘못된 내용입니다!!
useQueryClient() 여기 소괄호 안에 작성하는 것이 아닙니다!!!
const {mutate: onCreate} = ... 여기의 client.setQueryData() 여기 소괄호에 작성해야되는겁니다.
잘못된 내용이므로 감안하고 보셔야됩니다~!!!!!!


여튼 client.setQueryData() 소괄호 안에 QueryKeys.MESSAGES를 첫번째 인자로 넣어주고,
QueryKeys.MESSAGES에 들어올 데이터는
const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () =&gt; fetcher(GET_MESSAGES));data에 들어오는 형태랑 똑같습니다.
data라는 곳에 messages가 있었죠?


// 이렇게 함수를 두번째인자로 넣어줍니다.
// 아래 QueryKeys.MESSAGES에 들어올 데이터 형태에 맞춰 return 값을 설정해줍니다.
old => {
    return {
        messages: 
    }
}

Note

위 스크린샷도 잘못된것!!!! useQueryClient 소괄호 안의 내용이 onCreatesetQueryData() 여기로 들어가야됨!!!


// 기존 데이터를 ...old.messages로 펼쳐놓고
// 새로들어올 메시지를 createMessage로 넣습니다.
// 아까 GET_MESSAGES할 때 QueryKeys.MESSAGES 했잖아. 즉, QueryKeys.MESSAGES를 첫번째인자로하면 GET_MESSAGES가 실행되는 느낌인가?
// 그래서 그걸 가져와서 아래와 같이 create를 수행하는거지!
client.setQueryData(QueryKeys.MESSAGES, old => {
    return {
        messages: [createMessage, ...old.messages]
    }
})

Note

현재로써는 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(() => {

    })
    // ...
}

Note

아직 data에 대해 state 관리하는게 없습니다.
그래서 이 부분은 우선 useEffect로 구현해놓겠습니다.

그런데 현재 REST API서버사이드 렌더링을 해놓은 상태에잖아요?
그 상태에서 smsgs가 들어왔지만.. REST API때에도 구현 안했었지만,
Hydrate를 통해 넘어온 데이터를 client 캐시에도 저장을 하도록 하겠습니다.

하이드레이트를 구현해보도록 하겠습니다.

하이드레이트 구현

Note

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

Note

위와 같이 Hydrate를 통해 서버사이드 렌더링을 하면, 서버사이드에서 온 데이터 smsgs라는 정보가 이미 있죠?
그리고

const {data, error, isError} = useQuery(QueryKeys.MESSAGES, () =&gt; fetcher(GET_MESSAGES));

최초 접속시 useQuery를 통해서 날리는 위 query에서..
query가 서버에 요청은 하긴하지만, 실제로 dataclient에 있는 것을 먼저쓰고.. 이를 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가 변경되는지 아닌지 감시
    // ...
}

Note

저희가 지금 create를 하다가말고 메시지 data에 대해서 useEffect를 하고있는데, 이러한 에러가 발생했습니다.
onCreate가 이미 선언되었다는 에러입니다.

Note

기존 REST APIonCreate 메소드를 지워줍니다.

Note

콘솔을 보시면 msgs changed가 두번 호출이되었습니다.
useEffect안의 내용이 두번 실행되었다는 것입니다.

Note

data 자체가 없을 때 한번 호출되고,
data에 응답값이 들어왔을 때, 한번 더 호출이됩니다.
useEffect[data?.messages]를 감시하고있기 때문에 위와 같이 호출되는 거 같습니다.

Note

if (!data?.messages) return : data.messages에 아무것도 없다면 return해버리고
data?.messages에 값이 있을 때만 setMsgs()를 하도록 합니다.

Note

그러면 위와 같이 한번만 호출됩니다.

Note

텍스트를 추가해보도록 하겠습니다.

Note

텍스트를 추가하려고 완료 버튼을 눌렀는데 위와 같은 에러가 발생했습니다.
$textString! 타입으로 제공되어야하는데, 그렇게 제공되지 않았다는 에러가뜹니다.

Note

여기서 코드를 잘못 작성했다는거 알아챔..
코드 수정해주고~!

Note

아까 위의 에러를 해결해보자.
위의 MsgList 컴포넌트를 보면 onCreateuseMutation의 첫번째인자, 즉, 함수의 인자값으로 text를 전달했다.
그런데 이 text를 객체분해할당{text}으로 받도록했는데,

Note

위와 같이 text로 수정해줍시다.
이렇게 해줘도되는지 확인하는 방법은 MsgInput 컴포넌트를 보면됩니다.

Note

MsgInput 컴포넌트를 보면 mutate(text, id); 이렇게 값을 넘기는 것을 볼 수 있습니다.

Note

그런데 앞으로 또 바뀌어야하니깐 차라리 MsgInputmutate(text, id)mutate({text, id})로 아예 객체로 보내도록 수정합시다.

Note

위에도 다시 {text} 이렇게 객체로 바꿔줍니다.

Note

이제 다시 새글을 등록(create)하면 위와 같이 입력되는 것을 볼 수 있습니다.

Note

위에서 컴포넌트를 살펴보면 Home 컴포넌트의 smsgs를 보면…
hydrate가 일어나는 과정에서 Homesmsgs
로이, graphql playground에서 작성.. 여기까지만 서버에서 데이터가 왔다는 것을 볼 수 있죠?

Note

MsgList에선 새로 추가된 것까지 들어와있는게 확인됩니다.

Note

이 상태에서 새로고침을하면 캐시에서 더 빠르게 정보를 가지고와서

Note

캐시에서 더 빠르게 정보를 가져오긴하지만 graphql 요청을 하긴 합니다.
여튼 그래도 캐시에있는 정보를 더 우선적으로 적용을해서 더 빠르게 보여집니다.

GraphQL에서 create mutation까지 하는걸 확인했고, update를 이어서 보겠습니다.

client/components/MsgList.js - GraphQL UPDATE

Note

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;

Note

수정(put(update))을 하려고 수정하고 완료버튼을 눌렀는데, ID! 타입인 $userId가 제공되지 않았다는 에러메시지가 떴습니다.

Note

MsgInput 컴포넌트에서 값으로 {text, id}만 넘겨주므로 받기는 textid만 받으면되지만,
query를 보낼때 userId 정보도 넘겨줘야합니다.
useIdlocalhost:3000/?userId=jay 여기서 jay를 추출한걸 말합니다.

Note

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;

Note

위와 같은 실수 하지말자!!!!

Note

위 코드작성 실수한거 수정했더니 이제서야 id 에러가 뜬다.

Note

다시 비동기 콜백으로 실행되는 부분이기에 idupdateMessage.id로 수정하고 다시 실행!

Note

수정 아주 잘된다.

client/components/MsgList.js - GraphQL DELETE

Note

deleteput(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;

된거 같습니다. 확인해볼게요.

Note

삭제버튼을 누르면 msgs changed가 발생하면서 잘 삭제가 됩니다.

Note

응답을 보면 deleteMessageid가 잘 담겨서 오는 것을 볼 수 있습니다.

삭제가 잘 됩니다.

Note

GET 메소드는 지웁니다.
REST API에서 쓰던거므로..