CRUD 기능을 REST API로 구현해보았고, 이제 GraphQL을 통해서 통신을 구현해보자. Server에는 GraphQL에 대한 Resolver, Schema가 필요할 것이고, Client에서는 React Query를 통해 데이터를 관리하여 GraphQL 통신을 요청할 것이다.
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에 요청해서 데이터를 가져올 수 있다.
1️⃣ Apollo Server에 대한 Object
를 생성
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...') })
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는 4가지 인수
존재.
💡 자세히
👉 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
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 }); } }, } ]
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 }); } }, }, ]
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 }); } }, }, ]
✅ 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?
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)
}
`
✅ 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; }); };
// 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; };