GraphQL 통신

박정호·2023년 1월 14일
0

API

목록 보기
3/6
post-thumbnail

🚀 Start

CRUD 기능을 REST API로 구현해보았고, 이제 GraphQL을 통해서 통신을 구현해보자. Server에는 GraphQL에 대한 Resolver, Schema가 필요할 것이고, Client에서는 React Query를 통해 데이터를 관리하여 GraphQL 통신을 요청할 것이다.



⭐️ Server

REST API의 경우 Route를 이용하여 사용자가 요청하는 Route에 따라서 그에 대응하는 response를 내려주는 형태였다.

반면, GraphQL의 경우 /graphql path 하나로 요청되어, GraphQL 내부에서 resolver를 통해서 자체적으로 나눠서 판단하게 된다.

💡 Apollo Server
: ApolloServer GraphQL을 사용하는 모든 클라이언트와 호환되며 오픈소스로된 GrpahQL 서버

👉 Apollo Server를 이용한 초간단 GraphQL 서버 개발
👉 Get started with Apollo Server

💡 Schema

: 스키마는 서버에 어떻게 데이터를 요청할지 정의한 파일.
요청 시 어떤 데이터를 얼마나 요청할지, 각각의 데이터의 자료형이 무엇이고, 어떤 데이터를 필수로 요청할지에 대한 정보가 담긴다. 즉, 사용자는 반드시 스키마에 정의된 형태로 서버에 요청해야 한다.

💡 Resolver
: 리졸버는 사용자가 쿼리를 요청했을 때 이를 서버가 어떻게 처리할지 정의한 파일.
리졸버는 요청에 대해 단순히 데이터를 반환할 수도 있지만, 직접 데이터베이스를 찾거나, 메모리에 접근하거나, 다른 API에 요청해서 데이터를 가져올 수 있다.



👉 Apollo Server Instance


1️⃣ Apollo Server에 대한 Object를 생성

  • 파라미터로 들어가는 Object {typeDefs, resolvers, context}
    • typeDefs: 서버의 GraphQL 스키마를 나타내는 유효한 SDL(스키마 정의 언어) 문자열, 문서 또는 문서
    • resolvers: Apollo 서버의 기본 필드 확인자를 대체할 사용자 지정 확인자
    • context: apollo server의 resolver에서 전역적으로 사용가능한 변수

2️⃣ Apollo 서버가 들어오는 작업을 처리할 준비를 하도록 지시

3️⃣ Apollo 서버에 express 와 같이 동작한다고 알려주는 메서드

// Server/src.index.js
const server = new ApolloServer({ // 1️⃣ 번
  typeDefs: schema,
  resolvers,
  context: {
    db: {
      messages: readDB("messages"),
      users: readDB("users"),
    },
  },
});

const app = express();
await server.start(); // 2️⃣ 번
server.applyMiddleware({ // 3️⃣ 번
  app,
  path: "/graphql",
  cors: {
    origin: ["http://localhost:3000", "https://studio.apollographql.com"],
    credentials: true,
  },
});

await app.listen({ port: 8000 });
console.log("server listening on 8000...");

기존 코드

const app = express()
app.use(express.urlencoded({ extended: true }))
app.use(express.json())

app.use(
  cors({
    origin: 'http://localhost:3000',
    credentials: true,
  }),
)

const routes = [...messagesRoute, ...usersRoute]
routes.forEach(({ method, route, handler }) => {
  app[method](route, handler)
})

app.listen(8000, () => {
  console.log('server listening on 8000...')
})


👉 Schema

1️⃣ Apollo-server-express에서 제공하는 gql은 뒤에 나오는 문자열을 graphQL로 인식하게 해주는 명령어 참고

2️⃣ graphQL만의 고유 타입 정의 (참고)

import { gql } from "apollo-server-express";

// message.js
const messageSchema = gql`
  type Message {
    id: ID!
    text: String!
    user: User!
    timeStamp: Float
  }
  extend type Query {
    message(cursor: ID): [Message!]!
    messages(id: ID!): Message!
  }

  extend type Mutation {
    createMessage(text: String!, userId: ID!): Message!
    update(id: ID!, text: String!, userId: ID!): Message!
    delete(id: ID!, userId: ID!): ID!
  }
`;

// user.js
const userSchema = gql`
  type User {
    id: ID!
    nickname: String!
  }
  extend type Query {
    users: [User!]!
    user(id: ID!): User
  }
`;


👉 Resolver

resolver는 4가지 인수 존재.

  • parent
  • args
  • contextValue
  • info

💡 자세히
👉 Resolvers(공식문서)
👉 GrpahQL resolver

Query

const messageResolver = {
  Query: {
    // GET MESSAGES
   messages: (parent, args, { db }) => {
      return db.messages
    },
    // GET MESSAGE
    message: (parent, { id = '' }, { db }) => {
      return db.messages.find(msg => msg.id === id)
    },
  }
}

기존 코드

const messagesRoute = [
  {
    // GET MESSAGES
    method: 'get',
    route: '/messages',
    handler: (req, res) => {
      const msgs = getMsgs()
      res.send(msgs)
    },
  },
  {
    // GET MESSAGE
    method: 'get',
    route: '/messages/:id',
    handler: ({ params: { id } }, res) => {
      try {
        const msgs = getMsgs()
        const msg = msgs.find(m => m.id === id)
        if (!msg) throw Error('not found')
        res.send(msg)
      } catch (err) {
        res.status(404).send({ error: err })
      }
    },
  },

Mutation

  • Create
const setMsgs = (data) => writeDB("messages", data);

const messageResolver = {
  Mutation: {
    createMessage: (parent, { text, userId }, { db }) => {
      if (!userId) throw Error("사용자가 없습니다.");
      const newMsg = {
        id: v4(),
        text,
        userId,
        timestamp: Date.now(),
      };
      db.messages.unshift(newMsg);
      setMsgs(db.messages);
      return newMsg;
    }
}

기존 코드

const messagesRoute = [
    {
    // CREATE MESSAGE
    method: "post",
    route: "/messages",
    handler: ({ body }, res) => {
      try {
        if (!body.userId) throw Error("no userId");
        const msgs = getMsgs();
        const newMsg = {
          id: v4(),
          text: body.text,
          userId: body.userId,
          timestamp: Date.now(),
        };
        msgs.unshift(newMsg);
        setMsgs(msgs);
        res.send(newMsg);
      } catch (err) {
        res.status(500).send({ error: err });
      }
    },
  }
 ] 
  • Update
const setMsgs = (data) => writeDB("messages", data);

const messageResolver = {
  Mutation: {
    updateMessage: (parent, { id, text, userId }, { db }) => {
      const targetIndex = db.messages.findIndex((msg) => msg.id === id);
      if (targetIndex < 0) throw Error("메시지가 없습니다.");
      if (db.messages[targetIndex].userId !== userId)
        throw Error("사용자가 다릅니다.");

      const newMsg = { ...db.messages[targetIndex], text };
      db.messages.splice(targetIndex, 1, newMsg);
      setMsgs(db.messages);
      return newMsg;
    }
}    

기존 코드

const messagesRoute = [
  {
    // UPDATE MESSAGE
    method: "put",
    route: "/messages/:id",
    handler: ({ body, params: { id } }, res) => {
      try {
        const msgs = getMsgs();
        const targetIndex = msgs.findIndex((msg) => msg.id === id);
        if (targetIndex < 0) throw "메시지가 없습니다.";
        if (msgs[targetIndex].userId !== body.userId)
          throw "사용자가 다릅니다.";

        const newMsg = { ...msgs[targetIndex], text: body.text };
        msgs.splice(targetIndex, 1, newMsg);
        setMsgs(msgs);
        res.send(newMsg);
      } catch (err) {
        res.status(500).send({ error: err });
      }
    },
  },
]  
  • Delete
const setMsgs = (data) => writeDB("messages", data);

const messageResolver = {
  Mutation: {
    deleteMessage: (parent, { id, userId }, { db }) => {
      const targetIndex = db.messages.findIndex((msg) => msg.id === id);
      if (targetIndex < 0) throw "메시지가 없습니다.";
      if (db.messages[targetIndex].userId !== userId)
        throw "사용자가 다릅니다.";
      db.messages.splice(targetIndex, 1);
      setMsgs(db.messages);
      return id;
    },
  },
};

기존 코드

const messagesRoute = [
	{
    // DELETE MESSAGE
    method: "delete",
    route: "/messages/:id",
    handler: ({ params: { id }, query: { userId } }, res) => {
      try {
        const msgs = getMsgs();
        const targetIndex = msgs.findIndex((msg) => msg.id === id);
        if (targetIndex < 0) throw "메시지가 없습니다.";
        if (msgs[targetIndex].userId !== userId) throw "사용자가 다릅니다.";
        msgs.splice(targetIndex, 1);
        setMsgs(msgs);
        res.send(id);
      } catch (err) {
        res.status(500).send({ error: err });
      }
    },
  },  
]


⭐️ Client

graphql-request

NodeJS에는 GraphQL API를 호출을 도와주는 여러가지 패키지들 중 가장 간단하다고 생각하는 graphql-request 패키지를 사용해서 GraphQL API를 호출

react-query

React Query의 경우 데이터를 쉽고 빠르게 검색하고 저장 및 새로고침할 수 있는 민첩한 서버 상태관리 라이브러리이다.
Apollo Client의 경우 GraphQL 서버와 더 쉽고 모든 기능을 갖췄다고 하지만, GraphQL에 최적화되어, REST API개발의 경우 이상적인 선택이 아닐 수 있다.
반면, React Query의 경우 아직 REST API를 사용하는 기업이 많고, GrpahQL개발에도 큰 문제없이 사용이 됨으로 React Query를 사용해보자.

💡 자세히
👉 React Query with GraphQL
👉 React Query vs Apollo Client: Which One Should You Use?



👉 graphql

import gql from "graphql-tag";
// gql: gql 템플릿 리터럴 태그는 표준 GraphQL AST로 구문 분석되는 
// GraphQL 쿼리를 간결하게 작성하는 데 사용

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

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


👉 통신 with react-query

Get

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

  useEffect(() => {
    if (!data?.messages) return
    setMsgs(data.messages)
  }, [data?.messages])

  if (isError) {
    console.error(error)
    return null;
  }

기존 코드

  const getMessages = async () => {
    const msgs = await fetcher("get", "/messages");
    setMsgs(msgs);
  };

  useEffect(() => {
    getMessages();
  }, []);

Create

  const { mutate: onCreate } = useMutation(({ text }) => fetcher(CREATE_MESSAGE, { text, userId }), {
    onSuccess: ({ createMessage }) => {
      client.setQueryData(QueryKeys.MESSAGES, old => {
        return {
          messages: [createMessage, ...old.messages],
        }
      })
    },
  })

기존 코드

 const onCreate = async (text) => {
    const newMsg = await fetcher("post", "/messages", { text, userId });
    if (!newMsg) throw Error("error");
    setMsgs((msgs) => [newMsg, ...msgs]);
  };

Update

 const { mutate: onUpdate } = useMutation(({ text, id }) => fetcher(UPDATE_MESSAGE, { text, id, userId }), {
    onSuccess: ({ updateMessage }) => {
      client.setQueryData(QueryKeys.MESSAGES, old => {
        const targetIndex = old.messages.findIndex(msg => msg.id === updateMessage.id)
        if (targetIndex < 0) return old
        const newMsgs = [...old.messages]
        newMsgs.splice(targetIndex, 1, updateMessage)
        return { messages: newMsgs }
      })
      doneEdit()
    },
  })

기존 코드

 const onUpdate = async (text, id) => {
    const newMsg = await fetcher("put", `/messages/${id}`, { text, userId });

    if (!newMsg) throw Error("error");
    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();
  };

Delete

 const { mutate: onUpdate } = useMutation(({ text, id }) => fetcher(UPDATE_MESSAGE, { text, id, userId }), {
    onSuccess: ({ updateMessage }) => {
      client.setQueryData(QueryKeys.MESSAGES, old => {
        const targetIndex = old.messages.findIndex(msg => msg.id === updateMessage.id)
        if (targetIndex < 0) return old
        const newMsgs = [...old.messages]
        newMsgs.splice(targetIndex, 1, updateMessage)
        return { messages: 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;
    });
  };


👉 fetcher

// queryClient.ts
import { request } from "graphql-request";

export const fetcher = (query, variables = {}) =>
  request(URL, query, variables);

export const QueryKeys = {
  MESSAGES: "MESSAGES",
  MESSAGE: "MESSAGE",
  USERS: "USERS",
  USER: "USER",
};

기존코드

//fetcher.js
axios.defaults.baseURL = "http://localhost:8000";

const fetcher = async (
  method: METHOD,
  url: string,
  ...rest: { [key: string]: any }[]
) => {
  const res = await axios[method](url, ...rest);
  return res.data;
};
profile
기록하여 기억하고, 계획하여 실천하자. will be a FE developer (HOME버튼을 클릭하여 Notion으로 놀러오세요!)

0개의 댓글