4 Server - GraphQL

source: categories/study/react_restapi_graphql/react_restapi_graphql5.md

4.1 GraphQL 환경세팅 및 schema 정의

4.1.1 GraphQL 환경세팅

이번 시간에는 REST API로 작성한 서버를 GraphQL로 변환시켜보겠습니다.
server 폴더로 이동해서 설치를 먼저 해보겠습니다.


yarn add apollo-server apollo-server-express graphql

설치가 다 되었다면 src/index.js 파일을 열어보겠습니다.


// server/src/index.js
import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
import cors from 'cors'
import messagesRoute from "./routes/messages.js";
import usersRoute from "./routes/users.js";

const app = express();
// ApolloServer를 사용하면 아래 2줄 코드는 필요가 없게됩니다.
// app.use(express.urlencoded({extended: true}))
// app.use(express.json()) // express에서 json 형태로 사용하겠다.
app.use(cors({
    origin: 'http://localhost:3000', // 클라이언트 서버
    credentials: true,
}))

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

// 서버 경로는 8000번
app.listen(8000, () => {
    console.log('server listening on 8000...')
})

graphQLREST API의 가장 큰 차이점은

  • REST API 서버는 route를 이용해서 사용자가 요청한 route에 따라서 그에 대응하는 response를 내려주는 형태였다면
  • graphQL 서버는 오직 graphQL이라는 path 하나로.. route로치면 /graphql이라는 path 하나로 다 들어와서 그 안에서 graphQL 내부에서 자체적으로 판단해서 나누도록 되어있습니다.

즉, graphQLREST API에서 route로 처리하던 것을 이제는 resolvers라는 개념을 통해 처리합니다.
resolvers라는 것을 이용해서 graphQL 요청한 데이터들이 나눠지게됩니다.


// server/src/index.js
import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
import cors from 'cors'
import messagesRoute from "./routes/messages.js";
import usersRoute from "./routes/users.js";

const app = express();
// ApolloServer를 사용하면 아래 2줄 코드는 필요가 없게됩니다.
// app.use(express.urlencoded({extended: true}))
// app.use(express.json()) // express에서 json 형태로 사용하겠다.
app.use(cors({
    origin: 'http://localhost:3000', // 클라이언트 서버
    credentials: true,
}))

// ApolloServer 서버를 생성합니다.
const server = new ApolloServer({
    // typeDefs 라는 것이 필요하고, schema(스키마)라는게 필요합니다.
    typeDefs: schema,
    // 그리고 resolvers이라는 정보가 필요하게됩니다.
    resolvers,
    // 그리고 resolvers가 참조할 데이터.. 즉, DB 역할을하는 models 부분이 정의가 되어있어야 합니다.
    context: {
        models: {
            // 아래 정의에 대해선 이따 하도록 하겠습니다.
            messages: '',
            users: '',
        }
    }
});

// 위 정의가 다 되고나면, server.applyMiddleware() 함수를 통해서 아래와 같이 코드를 작성합니다.
server.applyMiddleware({ app, path: '/graphql' })

// 서버 경로는 8000번
app.listen(8000, () => {
    console.log('server listening on 8000...')
})

Note

REST API에서 route가 하던 역할을 GraphQL에선 resolvers가 한다는 느낌으로 가져가시면됩니다.

4.1.2 schema 정의

4.1.2.1 messages 스키마 정의

Note

schema 폴더 및 messages.js 파일 생성


// server/src/schema/messages.js
import {gql} from "apollo-server-express";

// ------------------------------------------------------------------------------------------------
// -------------------------------- 공통 설명
// gql은 아래와같이 태그드 탬플릿 문법을 사용합니다.
// `` 문자열 탬플릿 내용을 gql(graphql)로 인식합니다.
// `` 문자열 탬플릿 안에는 gql 관련 명령어를 넣어주면됩니다.
// 그럼 `` 문자열 탬플릿 안에 있는 gql 명령어를 gql이 인식해서 자바스크립트 언어로 치환을 해줍니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- type Message { ... } 설명
// 문자열 안에서 type을 정의해줄게요.
// 여기서는 typescript와는 다르게 graphQL 고유의 타입 정의가 있습니다.
// ! 느낌표는 반드시 들어가야된다 라는 뜻입니다.
// ID는 고유값, 유니크한 값이라는 것을 명시해주는 것입니다.
// text는 String, S <- 대문자로 들어갑니다.
// user는 User!
// timestamp는 13자리 숫자였거든요. graphQL에서는 13자리 정수형 숫자를 인식하질 못합니다.
// 그래서 Float이라고 적겠습니다. 정수형 자릿수의 제한이 있기 때문에 Float이라고 지정하는 겁니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- extend type Query { ... } 설명
// extend type Query의 messages에서 cursor값이 없을수도 있기 때문에 ID값 뒤에 ! 느낌표를 붙이지 않았습니다.
// Message가 반드시 와야돼고(!) 그 Message가 배열 형태로 이루어져있고 또 그 배열이 반드시 와야된다(!). 그런 데이터 형태다!
// 그리고 message 단독 요청일 경우! ID값이 반드시 와야됩니다.(!)
// 위와 같은 Query문을 날리게 될겁니다.
// 위 Query문은 REST API로 치자면 GET 메소드라고 생각하시면됩니다.

// messages Query문을 날리면 기존의 getMessages 개념과 같고,
// message는 getMessage 개념과 같다고 보시면됩니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- extend type Mutation { ... } 설명
// Mutation이란 의미는 '변화를 일으키다'라는 의미입니다.
// create, update, delete가 모두 여기에 해당합니다.
// create: 새로운 글을 쓸테니까 text: String!, 그리고 user 정보가 필요하니까 ID! 그리고 이때 응답값은 Message!가 될겁니다.
// update: id를 지정해줘야되니까 ID!, 수정할 내용 text: String!, 그리고 userId: ID!, 똑같이 Message!가 응답값으로 넘어올겁니다.
// delete: delete 메소드 같은 경우는 id만 있으면돼고, userId: ID! 가 필요하겠죠? 그리고 이때의 응답값은 Message의 ID! 값이었습니다.
const messageSchema = gql`
    type Message {
        id: ID!
        text: String!
        user: User!
        timestamp: Float
    }
    
    extend type Query {
        messages(cursor: ID): [Message!]!
        message(id: ID!): Message!
    }
    
    extend type Mutation {
        createMessage(text: String!, userId: ID!): Message!
        updateMessage(id: ID!, text: String!, userId: ID!): Message!
        deleteMessage(id: ID!, userId: ID!): ID!
    }
`

// id는 uuid의 v4() 함수로 생성한 고유한 id
// userId는 jay, roy 이런거

export default messageSchema

// 이러한 형태로 schema를 지정을 했습니다.
// 이 schema에 있는 데이터 정보, 형태 그대로 graphQL 요청에 의한 응답값이 오게됩니다.
// 미리 이런 정의를 해놓은 상태에서, 위와 같은 형태의 응답값이 오게끔 resolvers를 만들어주면됩니다.
// resolvers에서 만들어준 내용이 변경이 필요하면, resolvers도 변경하면서 같이 위 schema도 맞춰주는 식으로 하면 됩니다.

4.1.2.2 users 스키마 정의


// server/src/schema/users.js
import {gql} from 'apollo-server-express'

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

export default userSchema

// -------------------------------------------- query문
// users() 요청보내면
// {
//     "roy": {"id": "roy", "nickname": "로이"},
//     "jay": {"id": "jay", "nickname": "제이"}
// }
// 이 값들이 응답옴.
// 하지만 graphQL에선 object 형태로 받을 순 없나봄.
// 위와 같이 [User!]! <- 이렇게 배열 형태로 받도록 해야됨.

// user(id: ID!): User 이거는 하나만 오는거니깐 "roy": {"id": "roy", "nickname": "로이"} <- 이런식으로 응답오는건가?

4.1.2.3 server/src/schema/index.js

Note

똑같은 타입 정의가 중첩되지 않게끔 하기 위해서 extend라는 개념을 쓴거고, 그거를 한데 묶기위해서 schema/index.js 같은거를 만들어 놓은겁니다.


// server/src/schema/index.js
// messages.js, users.js 이 둘을 한데 아우르는 index.js 파일을 만듭니다.
import {gql} from "apollo-server-express";
import messageSchema from "./messages";
import userSchema from "./users";

// linkSchema를 만듭니다.
// 아래는 default로 주는 정보입니다.
// 방금 messages.js 파일 같은데 보시면 extend라고 있었거든요?
// type Query 정의를 기본적으로 index.js에서 한꺼번에 몰아서 해주기위해서 의미없는 정보를 하나 준겁니다.
const linkSchema = gql`
    type Query {
        _: Boolean
    }
    type Mutation {
        _: Boolean
    }
`

export default [linkSchema, messageSchema, userSchema]

// 일단 지금은, 스키마를 만들 때 index.js는 여러가지를 취합하고 linkSchema 같은게 하나 추가가되면 동작이 잘 되더라 라고 인식하시고 넘어가시면 되겠습니다.


// server/src/schema/users.js
import {gql} from 'apollo-server-express'

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

export default userSchema

// -------------------------------------------- query문
// users() 요청보내면
// {
//     "roy": {"id": "roy", "nickname": "로이"},
//     "jay": {"id": "jay", "nickname": "제이"}
// }
// 이 값들이 응답옴.
// 하지만 graphQL에선 object 형태로 받을 순 없나봄.
// 위와 같이 [User!]! <- 이렇게 배열 형태로 받도록 해야됨.

// user(id: ID!): User 이거는 하나만 오는거니깐 "roy": {"id": "roy", "nickname": "로이"} <- 이런식으로 응답오는건가?


// server/src/schema/messages.js
import {gql} from "apollo-server-express";

// ------------------------------------------------------------------------------------------------
// -------------------------------- 공통 설명
// gql은 아래와같이 태그드 탬플릿 문법을 사용합니다.
// `` 문자열 탬플릿 내용을 gql(graphql)로 인식합니다.
// `` 문자열 탬플릿 안에는 gql 관련 명령어를 넣어주면됩니다.
// 그럼 `` 문자열 탬플릿 안에 있는 gql 명령어를 gql이 인식해서 자바스크립트 언어로 치환을 해줍니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- type Message { ... } 설명
// 문자열 안에서 type을 정의해줄게요.
// 여기서는 typescript와는 다르게 graphQL 고유의 타입 정의가 있습니다.
// ! 느낌표는 반드시 들어가야된다 라는 뜻입니다.
// ID는 고유값, 유니크한 값이라는 것을 명시해주는 것입니다.
// text는 String, S <- 대문자로 들어갑니다.
// user는 User!
// timestamp는 13자리 숫자였거든요. graphQL에서는 13자리 정수형 숫자를 인식하질 못합니다.
// 그래서 Float이라고 적겠습니다. 정수형 자릿수의 제한이 있기 때문에 Float이라고 지정하는 겁니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- extend type Query { ... } 설명
// extend type Query의 messages에서 cursor값이 없을수도 있기 때문에 ID값 뒤에 ! 느낌표를 붙이지 않았습니다. ---- 여기서 ID는 jay, roy같은거..
// Message가 반드시 와야돼고(!) 그 Message가 배열 형태로 이루어져있고 또 그 배열이 반드시 와야된다(!). 그런 데이터 형태다!
// 그리고 message 단독 요청일 경우! ID값이 반드시 와야됩니다.(!) ---- 여기서 id는 uuid 라이브러리의 v4() 함수로 생성되는 그런 id...
// 위와 같은 Query문을 날리게 될겁니다.
// 위 Query문은 REST API로 치자면 GET 메소드라고 생각하시면됩니다.

// messages Query문을 날리면 기존의 getMessages 개념과 같고,
// message는 getMessage 개념과 같다고 보시면됩니다.

// ------------------------------------------------------------------------------------------------
// -------------------------------- extend type Mutation { ... } 설명
// Mutation이란 의미는 '변화를 일으키다'라는 의미입니다.
// create, update, delete가 모두 여기에 해당합니다.
// create: 새로운 글을 쓸테니까 text: String!, 그리고 user 정보가 필요하니까 ID! 그리고 이때 응답값은 Message!가 될겁니다.
// update: id를 지정해줘야되니까 ID!, 수정할 내용 text: String!, 그리고 userId: ID!, 똑같이 Message!가 응답값으로 넘어올겁니다.
// delete: delete 메소드 같은 경우는 id만 있으면돼고, userId: ID! 가 필요하겠죠? 그리고 이때의 응답값은 Message의 ID! 값이었습니다.
const messageSchema = gql`
    type Message {
        id: ID!
        text: String!
        userId: ID!
        timestamp: Float
    }
    
    extend type Query {
        messages: [Message!]!
        message(id: ID!): Message!
    }
    
    extend type Mutation {
        createMessage(text: String!, userId: ID!): Message!
        updateMessage(id: ID!, text: String!, userId: ID!): Message!
        deleteMessage(id: ID!, userId: ID!): ID!
    }
`

// id는 uuid의 v4() 함수로 생성한 고유한 id
// userId는 jay, roy 이런거

export default messageSchema

// 이러한 형태로 schema를 지정을 했습니다.
// 이 schema에 있는 데이터 정보, 형태 그대로 graphQL 요청에 의한 응답값이 오게됩니다.
// 미리 이런 정의를 해놓은 상태에서, 위와 같은 형태의 응답값이 오게끔 resolvers를 만들어주면됩니다.
// resolvers에서 만들어준 내용이 변경이 필요하면, resolvers도 변경하면서 같이 위 schema도 맞춰주는 식으로 하면 됩니다.

4.2 resolver 정의

그 다음에 route를 대체할 resolver를 만들어보겠습니다.
이 부분이 중요하겠죠?

Note

routes/messages.js 파일에서 작성한 코드가 하던 역할을 resolvers/messages.js 파일 코드에서 그대로 한다고 보시면됩니다.

Note

routes/messages.js 파일 내용을 그대로 복붙하고 수정해보겠습니다.


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        // update에선 params에 들어오는 id가 필요했었다. body.text, body.userId(roy, jay)도 필요했었다.
        // 2번째 인자에서 이 3개를 뭉뜽그려서 꺼내옵니다.
        // 그리고 3번재 인자에선 마찬가지로 db를 꺼내옵니다.
        updateMessage: (parent, {id, text, userId}, {db}) => {
            // REST API의 update 내용을 가져옵니다.
            // msgs는 db.messages로 수정합니다.
            const targetIndex = db.messages.findIndex(msg => msg.id === id)
            if (targetIndex < 0) throw Error('메시지가 없습니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.
            if (db.messages[targetIndex].userId !== userId) throw Error('사용자가 다릅니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.

            const newMsg = {
                ...db.messages[targetIndex],
                text,
            }
            db.messages.splice(targetIndex, 1, newMsg)
            setMsgs(db.messages)
            return newMsg; // res.send(newMsg) 이게아니라 그냥 return을 해주면됩니다.
        },
        // delete도 REST API에선 params의 id와 queryString의 userId로 처리했었다.
        // GraphQL에선 2번째 인자에서 뭉뜽그려서 한번에 처리한다.
        // 마찬가지로 msgs를 db.messages로 수정한다.
        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)
            return id // res.send(id)
        },
    }
}

export default messageResolver


// server/src/index.js
import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
// import cors from 'cors' // cors 라이브러리 불러온거 삭제해줍니다.
import resolvers from './resolvers/index.js'
import schema from './schema/index.js'
import {readDB} from "./dbController.js";

// ApolloServer를 사용하면 아래 2줄 코드는 필요가 없게됩니다.
// app.use(express.urlencoded({extended: true}))
// app.use(express.json()) // express에서 json 형태로 사용하겠다.
// -------- app.use(cors( ... )) 아래 코드는 필요없으니 삭제해줍니다.
// app.use(cors({
//     origin: 'http://localhost:3000', // 클라이언트 서버
//     credentials: true,
// }))

// ApolloServer 서버를 생성합니다.
const server = new ApolloServer({
    // typeDefs 라는 것이 필요하고, schema(스키마)라는게 필요합니다.
    typeDefs: schema,
    // 그리고 resolvers이라는 정보가 필요하게됩니다.
    resolvers,
    // 그리고 resolvers가 참조할 데이터.. 즉, DB 역할을하는 models 부분이 정의가 되어있어야 합니다.
    context: {
        db: {
            // 아래 정의에 대해선 이따 하도록 하겠습니다.
            messages: readDB('messages'),
            users: readDB('users'),
        }
    }
});

const app = express(); // <- 이 코드 위치를 applyMiddleware 위쪽인 이 위치로 옮깁니다.
await server.start(); // <- 그리고 server.start() 코드를 await 키워드를 통해 실행하겠습니다.
// 위 정의가 다 되고나면, server.applyMiddleware() 함수를 통해서 아래와 같이 코드를 작성합니다.
server.applyMiddleware({
    app,
    path: '/graphql',
    // 그리고 cors를 이전과 다르게 applyMiddleware의 3번째 인자(옵션값)로 객체형태로 넘겨줍니다.
    cors: {
        origin: [
            'http://localhost:3000',
            'https://studio.apollographql.com'
        ], // 클라이언트 서버
        credentials: true,
    }
})

// 서버 경로는 8000번
// 그리고 아래도 await 키워드를 붙여주고 첫번째 인자 값을 아래와 같이 수정해줍니다.
await app.listen({port: 8000})
// 위의 await 붙은 코드가 끝나고 아래가 실행될거기 때문에 console.log(...)도 이렇게 밖으로 빼줍니다.
console.log('server listening on 8000...')

// 원래는 아래와 같은 모습이었음.
// app.listen(8000, () => { 
//     console.log('server listening on 8000...')
// })

Note

그래서 readDB를 지웁니다.
writeDB만 가지고 가겠습니다.

Note

schema/messages.js 파일에서

  • messages
  • message
  • createMessage
  • updateMessage
  • deleteMessage

이렇게 정의했었습니다.
이 내용들 각각에 대해서 실제로 어떻게 동작할지에 대한 정의가 resolvers/messages.js에서 본격적으로 이뤄집니다.

Note

server/src/index.jscontext 안에 models라는 것이 들어있습니다.
modelsresolves/messages.js 파일의 Query의 각 함수의 3번째 인자로 구조분해할당으로 받아올 수 있습니다.

Note

위에서 본 models라는 명칭을 db로 바꿔줍시다.

Note

messages Query에서 db.messagesreturn합니다.

Note

위 내용은 REST API에서 GET 요청에 대해 정의한 내용과 같습니다.
똑같이 .find()를 하고, 없는 경우엔 Error를 띄우고, 있는 경우엔 send로 응답보내고..
그 내용과 같다고보시면 되겠습니다.

Note

그런데 REST APIGET 요청에선 cursor 값을 받았었습니다.
GraphQL에선 일단 cursor 값 없이 모든 데이터를 보여주는 것부터 살펴본 다음에
cursor를 통해 무한 스크롤하는 것을 추가적으로 다뤄보도록 하겠습니다.

Note

messages.js 라우터에서 get messages, get message를 지웁니다.
그리고 create를 보겠습니다.

Note

위 코드를 보시면 아시겠지만, REST API에서는 어떤걸로 어떻게 넘어오느냐에따라 처리가 달랐습니다.
요청이 /messages:id 이렇게 넘어오면 REST API 서버에서 {params {id}}로 받고,
localhost:3000/?userId=jay 이렇게 넘어오면 {query: {userId}} 이렇게 받았습니다.
또한 postput 요청인 경우는 새로 등록할 또는 업데이트할 정보를 body로부터 받습니다.
post, put은 새로 등록할 또는 업데이트할 정보를 2번째 인자로 받아오기 때문… (getdelete는 아님)

하지만 GraphQL은 REST API와 달리 모두 뭉뜽그려서 한군데서 처리한다고 보시면됩니다.

GraphQL GET 정의

GET - 모든 메시지들 불러오는 GET 정의


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        updateMessage: () => {},
        deleteMessage: () => {},
    }
}

const getMsgs = () => readDB('messages')
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: ({query: {cursor = ''}}, res) => {
            const msgs = getMsgs();
            const fromIndex = msgs.findIndex(msg => msg.id === cursor) + 1
            res.send(msgs.slice(fromIndex, fromIndex + 15))
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        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 })
            }
        }
    },
    { // 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 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 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)
                res.send(id)
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

export default messagesRoute

GET - 특정 메시지 하나만 불러오는 GET 정의


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        updateMessage: () => {},
        deleteMessage: () => {},
    }
}

const getMsgs = () => readDB('messages')
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: ({query: {cursor = ''}}, res) => {
            const msgs = getMsgs();
            const fromIndex = msgs.findIndex(msg => msg.id === cursor) + 1
            res.send(msgs.slice(fromIndex, fromIndex + 15))
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        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 })
            }
        }
    },
    { // 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 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 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)
                res.send(id)
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

export default messagesRoute

POST - create 정의


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        updateMessage: () => {},
        deleteMessage: () => {},
    }
}

const getMsgs = () => readDB('messages')
const messagesRoute = [
    { // GET MESSAGES : 전체 메시지를 가져오는 명령
        method: 'get',
        route: '/messages',
        handler: ({query: {cursor = ''}}, res) => {
            const msgs = getMsgs();
            const fromIndex = msgs.findIndex(msg => msg.id === cursor) + 1
            res.send(msgs.slice(fromIndex, fromIndex + 15))
        }
    },
    { // GET MESSAGE : id 하나에 대한 메시지를 가져오는 것도 살펴봅시다.
        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 })
            }
        }
    },
    { // 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 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 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)
                res.send(id)
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

export default messagesRoute

PUT - update

Note

/message/:id 이런식으로 들어오는 것이 params
post(create), put(update)에선 text, userIdbody로 들어온다.
REST API에서 post, put 메소드는 두번째 인자가 data를 전달하는 인자이기 때문에.
get, delete는 2번째인자가 config(옵션) 설정하는 인자이다. data를 받는 인자가 없다.


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        // update에선 params에 들어오는 id가 필요했었다. body.text, body.userId(roy, jay)도 필요했었다.
        // 2번째 인자에서 이 3개를 뭉뜽그려서 꺼내옵니다.
        // 그리고 3번재 인자에선 마찬가지로 db를 꺼내옵니다.
        updateMessage: (parent, {id, text, userId}, {db}) => {
            // REST API의 update 내용을 가져옵니다.
            // msgs는 db.messages로 수정합니다.
            const targetIndex = db.messages.findIndex(msg => msg.id === id)
            if (targetIndex < 0) throw Error('메시지가 없습니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.
            if (db.messages[targetIndex].userId !== userId) throw Error('사용자가 다릅니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.

            const newMsg = {
                ...db.messages[targetIndex],
                text,
            }
            db.messages.splice(targetIndex, 1, newMsg)
            setMsgs(db.messages)
            return newMsg; // res.send(newMsg) 이게아니라 그냥 return을 해주면됩니다.
        },
        deleteMessage: () => {},
    }
}

const getMsgs = () => readDB('messages')
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 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)
                res.send(id)
            } catch (err) {
                res.status(500).send({error: err})
            }
        }
    },
]

export default messagesRoute

DELETE

Note

그래서 delete 처리할 때 body에서 못받아오고 위와 같이 {query: {userId}}로 받아왔지만,
GraphQL에서 뭉뜽그려서 한번에 받아올 수 있습니다.


// server/src/resolvers/messages.js
import {v4} from "uuid";
// readDB는 지우고 writeDB만 가지고 가겠습니다.
import {writeDB} from "../dbController.js";

// setMsgs는 graphQL 서버에서도 사용하므로 살립니다.
const setMsgs = data => writeDB('messages', data)

//-------------------------------------------------------------------
// 그리고 나머지는 유지한 상태에서 messageResolver를 만들겠습니다.
// 이 messageResolver는 Query 명령에 대해서 정의를 하고,
// 또 Mutation 명령에 대해서 정의를 할겁니다.
// 이때 schema에서 정의했던 명령어들, 그 내용을 그대로 쓰면됩니다.
// schema에서 정의했던 명령어들에 대한 본격적인 정의가 여기서 이뤄집니다.
// 아래와 같은 구조로 정의를 해줍니다.

// 각각의 resover에는 parent, args, context라는 세개의 인자가 들어옵니다. 이들 각각에 대해서 graphQL 공식 사이트에선 이렇게 설명을 하고 있습니다.
/**
 * parent: 대부분 사용되지 않는 root query type의 이전 객체, parent 객체라고 하고, 거의 사용되지 않습니다.
 * args: graphQL Query에 필요한 필드에 제공되는 인수(parameter)가 들어있는 객체
 * context: 로그인한 사용자 정보, DB Access 권한 등의 중요한 정보들을 담고있는 객체
 * 위 각각에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
 * */
const messageResolver = {
    Query: {
        // 일단은 아래 messages의 인자들은 messages를 가져오는 '정보'니깐, 필요한 부분은 3번째 인자인 context입니다.
        // context 안에 models라는 것이 들어있는데, 이 models는 아까 index.js에서 정의한.. 저희가 직접 정의한 것입니다.
        messages: (parent, args, {db}) => {
            // console.log({obj, args, context}) // 각 인자에 어떤 내용이 들어가있는지는 나중에 따로 살펴보도록 하겠습니다.
            // 아래와 같이 db.messages라고 정의하고 일단은 넘어가도록 하겠습니다.
            return db.messages
        },
        // message를 하나 불러오는 Query입니다.
        // 2번째 인자에서 Query에 필요한 parameter 값이 온다고 했죠? 여기에 id 값이 들어오게 될겁니다.
        // id가 없을 경우를 대비해 default 값을 넣어줬습니다.
        // 그리고 마지막 3번째 인자로 마찬가지로 db가 들어올겁니다.
        message: (parent, {id = ''}, {db}) => {
            // 그럼 db.messages에서 id가 2번째 인자로 넘어온 id와 일치하는 것을 찾아주면 될겁니다.
            return db.messages.find(msg => msg.id === id)
        },
    },
    
    Mutation: {
        // 2번째 데이터, param 데이터들.. 
        // REST API의 create 메소드를 보면, 저희에게 필요했던 데이터가 body에 있는 text와 userId였죠?
        // 그냥 body에서 왔는지, params에서 왔는지, query에서 왔는지 신경쓸거 없이 아래처럼 2번째 인자에 text, userId를 다 받아오면됩니다.
        // graphQL은 모두 뭉뜽그려서 한군데서 처리하면된다 라고 생각하시면 됩니다.
        createMessage: (parent, {text, userId}, {db}) => {
            // 그러면 create에 있는 내용은 사실상 REST API에 있는거랑 똑같은거죠.
            // const msgs = getMsgs(); // getMsgs는 db에있는 messages를 사용하면되니깐 필요가 없고
            const newMsg = {
                id: v4(),
                text, // 여기는 body.text 이렇게 올 필요가 없으므로 왼쪽과 같이 수정하면 됩니다.
                userId, // 여기도 body 필요없으므로 마찬가지로..
                timestamp: Date.now(),
            }
            db.messages.unshift(newMsg) // msgs.unshift(newMsg) <- 이 코드 대신 왼쪽 코드로 작성해주고
            setMsgs(db.messages) // setMsgs(msgs); <- 이걸 왼쪽과 같이 수정
            return newMsg // res.send(newMsg) <- 이걸 return newMsg로 수정
        },
        // update에선 params에 들어오는 id가 필요했었다. body.text, body.userId(roy, jay)도 필요했었다.
        // 2번째 인자에서 이 3개를 뭉뜽그려서 꺼내옵니다.
        // 그리고 3번재 인자에선 마찬가지로 db를 꺼내옵니다.
        updateMessage: (parent, {id, text, userId}, {db}) => {
            // REST API의 update 내용을 가져옵니다.
            // msgs는 db.messages로 수정합니다.
            const targetIndex = db.messages.findIndex(msg => msg.id === id)
            if (targetIndex < 0) throw Error('메시지가 없습니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.
            if (db.messages[targetIndex].userId !== userId) throw Error('사용자가 다릅니다.') // 일단 try catch 구문을 사용안해서, Error 객체를 통한 에러처리는 여기서 보내겠습니다.

            const newMsg = {
                ...db.messages[targetIndex],
                text,
            }
            db.messages.splice(targetIndex, 1, newMsg)
            setMsgs(db.messages)
            return newMsg; // res.send(newMsg) 이게아니라 그냥 return을 해주면됩니다.
        },
        // delete도 REST API에선 params의 id와 queryString의 userId로 처리했었다.
        // GraphQL에선 2번째 인자에서 뭉뜽그려서 한번에 처리한다.
        // 마찬가지로 msgs를 db.messages로 수정한다.
        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)
            return id // res.send(id)
        },
    }
}

export default messageResolver

GraphQL에서 messages.js 관련 작업은 모두 끝났습니다.
이제 users.js 쪽을 해보겠습니다.

users.js resolver 정의 - GET

Note

이번엔 users.js 파일을 통해 userresolvers(REST API의 routes)를 처리해봅시다.

Note

모든 users 정보를 불러오는 Query는 위와 같이 만듭니다.

users: (parent, args, {db}) =&gt; db.users

Note

그런데 schema 만들 때 말씀드렸듯이 users는 배열로 받기로 정의했습니다. (GraphQL엔 객체형태가 없고 배열형태밖에 없기 때문..)

Note

즉 위와 같이 {} 객체로 되어있는 db 정보를 배열로 만들어줘야한다.

Note

배열로 만들기 위해, 굳이 "roy", "jay" 이런 key들을 살릴 필요는 없을 거 같습니다.
뒤의 프로퍼티 값만 살려서 배열로 반환하면 될 거 같습니다.


// server/src/resolvers/users.js
const userResolver = {
    // userResolver는 Query만 있을겁니다.
    // 일단 GET 요청 - '/graphql'로 들어가서 나눠지는건데, REST API에서도 user는 GET요청만 만들어놨었다.
    // graphQL에서의 GET 요청은 Query
    Query: {
        // 모든 user 정보를 다 불러오는 Query입니다.
        // parent와 args는 사용할일이 없고 db만 쓰겠죠?
        // db에있는 users를 전부 반환해주면됩니다.
        // 그런데 schema에서 말씀드렸듯이 users는 배열로 받기로 되어있었습니다.
        // 그래서 아래와 같이 Object.values() 함수를 사용해 value만 배열 형태로 반환하도록하면 될 거 같습니다.
        users: (parent, args, {db}) => Object.values(db.users),
        user: (parent, {id}, {db}) => db.users[id],
    }
}

export default userResolver

resolvers 들을 한데 묶어주는 index.js

Note

resolvers(messages.js, users.js)들을 한데묶는 index.js 파일을 생성합니다.


// server/src/resolvers/index.js
import messageResolver from "./messages";
import userResolver from "./users";

export default [messageResolver, userResolver]


다 만들었습니다.

server/src/index.js

Note

이제 server/src/index.js에서 resolversschema를 각각 불러옵니다.

Note

그리고 위와 같이 server/src/index.js에서 import {readDB} from "./dbConroller";을 통해서 readDB도 불러오겠습니다.

Note

커스텀 파일들은 .js도 꼭 명시해주십시오.
명시 안해도 되는 경우도 있는 것 같지만.. 꼭 명시를 해야 불필요한 오류를 줄일 수 있습니다.

이렇게 resolversschema 정의는 끝났고, db쪽만 정의를 하면 됩니다.

Note

위와 같이 context 부분에 messagesusers 부분에 readDB를 작성해주어 DB를 불러오게합니다.

이렇게하면 GraphQL 서버 완성입니다.
실행을 해보도록 하겠습니다.
서버만 우선 띄워보겠습니다.


yarn run server

yarn run v1.22.10
$ yarn workspace server start
$ nodemon ./src/index.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
internal/process/esm_loader.js:74
    internalBinding('errors').triggerUncaughtException(
                              ^

Error [ERR_MODULE_NOT_FOUND]: Cannot find module 'D:\react-training-graphql\server\src\resolvers\messages' imported from D:\react-training-graphql\server\src\resolvers\index.js
    at finalizeResolution (internal/modules/esm/resolve.js:276:11)
    at moduleResolve (internal/modules/esm/resolve.js:699:10)
    at Loader.defaultResolve [as _resolve] (internal/modules/esm/resolve.js:810:11)
    at Loader.resolve (internal/modules/esm/loader.js:88:40)
    at Loader.getModuleJob (internal/modules/esm/loader.js:241:28)
    at ModuleWrap.<anonymous> (internal/modules/esm/module_job.js:56:40)
    at link (internal/modules/esm/module_job.js:55:36) {
  code: 'ERR_MODULE_NOT_FOUND'
}
[nodemon] app crashed - waiting for file changes before starting...

에러가납니다.

Note

커스텀 파일은 꼭 .js를 붙여주도록합시다.


yarn run server

yarn run v1.22.10
$ yarn workspace server start
$ nodemon ./src/index.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
D:\react-training-graphql\server
D:\react-training-graphql\node_modules\graphql\error\syntaxError.js:15
  return new _GraphQLError.GraphQLError("Syntax Error: ".concat(description), undefined, source, [position]);
         ^

GraphQLError [Object]: Syntax Error: Expected Name, found ")".
    at syntaxError (D:\react-training-graphql\node_modules\graphql\error\syntaxError.js:15:10)
    at Parser.expectToken (D:\react-training-graphql\node_modules\graphql\language\parser.js:1413:40)
    at Parser.parseName (D:\react-training-graphql\node_modules\graphql\language\parser.js:98:22)
    at Parser.parseInputValueDef (D:\react-training-graphql\node_modules\graphql\language\parser.js:911:21)
    at Parser.optionalMany (D:\react-training-graphql\node_modules\graphql\language\parser.js:1503:28)
    at Parser.parseArgumentDefs (D:\react-training-graphql\node_modules\graphql\language\parser.js:900:17)
    at Parser.parseFieldDefinition (D:\react-training-graphql\node_modules\graphql\language\parser.js:880:21)
    at Parser.optionalMany (D:\react-training-graphql\node_modules\graphql\language\parser.js:1503:28)
    at Parser.parseFieldsDefinition (D:\react-training-graphql\node_modules\graphql\language\parser.js:868:17)
    at Parser.parseObjectTypeExtension (D:\react-training-graphql\node_modules\graphql\language\parser.js:1176:23) {
  locations: [ { line: 8, column: 15 } ]
}
[nodemon] app crashed - waiting for file changes before starting...

이번엔 Syntax Error가 납니다.
정확히 어디서 에러가난 것인지 명시를 안해주고 있습니다.
일단 이럴 경우엔 schema에서 난 에러일 확률이 크거든요.

Note

원래는 users(): [Users!]!라고 되어있었는데 여기서 소괄호를 지워줍니다.
괄호 안에 아무것도 오지 않을 경우엔 괄호를 지워줍니다.

Note

messages(cursor: ID): [Message!]!
여기서도 cursor: ID에 올게 없으므로 지워줍니다.messages: [Message!]!

아직 에러가 해결 안된 상태.


yarn run server

yarn run v1.22.10
$ yarn workspace server start
$ nodemon ./src/index.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
D:\react-training-graphql\server
D:\react-training-graphql\node_modules\graphql\validation\validate.js:107
    throw new Error(errors.map(function (error) {
          ^

Error: Unknown type "User". Did you mean "user"?

Unknown type "User". Did you mean "user"?

Unknown type "User". Did you mean "user"?
    at assertValidSDL (D:\react-training-graphql\node_modules\graphql\validation\validate.js:107:11)
    at Object.buildASTSchema (D:\react-training-graphql\node_modules\graphql\utilities\buildASTSchema.js:45:34)
    at Object.makeExecutableSchema (D:\react-training-graphql\node_modules\@graphql-tools\schema\index.js:497:26)
    at ApolloServer.constructSchema (D:\react-training-graphql\node_modules\apollo-server-core\dist\ApolloServer.js:314:25)
    at new ApolloServerBase (D:\react-training-graphql\node_modules\apollo-server-core\dist\ApolloServer.js:118:75)
    at new ApolloServer (D:\react-training-graphql\node_modules\apollo-server-express\dist\ApolloServer.js:12:1)
    at file:///D:/react-training-graphql/server/src/index.js:20:16
    at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
    at async Loader.import (internal/modules/esm/loader.js:177:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)
[nodemon] app crashed - waiting for file changes before starting...

Note

user에서 대문자로 수정합니다. User

그런데 여전히 에러발생…
이건 버전업돼서 생기는 에러인듯 하다.


yarn run server

yarn run v1.22.10
$ yarn workspace server start
$ nodemon ./src/index.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
D:\react-training-graphql\server
D:\react-training-graphql\node_modules\apollo-server-core\dist\ApolloServer.js:288
            throw new Error('You must `await server.start()` before calling `server.' +
                  ^

Error: You must `await server.start()` before calling `server.applyMiddleware()`
    at ApolloServer.assertStarted (D:\react-training-graphql\node_modules\apollo-server-core\dist\ApolloServer.js:288:19)
    at ApolloServer.applyMiddleware (D:\react-training-graphql\node_modules\apollo-server-express\dist\ApolloServer.js:18:14)
    at file:///D:/react-training-graphql/server/src/index.js:36:8
    at ModuleJob.run (internal/modules/esm/module_job.js:152:23)
    at async Loader.import (internal/modules/esm/loader.js:177:24)
    at async Object.loadESM (internal/process/esm_loader.js:68:5)
[nodemon] app crashed - waiting for file changes before starting...

4.3 (추가) Apollo Server 3.0 업데이트 대응 - 환경 세팅 + playground

2021년 7월 17일, Apollo Server 3.0이 릴리즈가 되었습니다.
이 강의 영상을 보시면서 기존 작업된 것들을 수정하시면 되겠습니다.

Note

위와 같이 apollo-server, apollo-server-express 라이브러리의 버전이 업그레이드되었습니다.

이 강의를 올리던 시점에는 apollo-server, apollo-server-express 라이브러리 버전이 2.25.1이었는데, 2021년 9월 13일 기준 3.3.0이 되었습니다.


yarn add apollo-server apollo-server-express express graphql

위 명령어로 최신 버전으로 다시 설치를합니다.

Note

server/src/index.js 파일에서 몇가지 수정을 하겠습니다.


// server/src/index.js
import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
// import cors from 'cors' // cors 라이브러리 불러온거 삭제해줍니다.
import resolvers from './resolvers/index.js'
import schema from './schema/index.js'
import {readDB} from "./dbController.js";

// ApolloServer를 사용하면 아래 2줄 코드는 필요가 없게됩니다.
// app.use(express.urlencoded({extended: true}))
// app.use(express.json()) // express에서 json 형태로 사용하겠다.
// -------- app.use(cors( ... )) 아래 코드는 필요없으니 삭제해줍니다.
// app.use(cors({
//     origin: 'http://localhost:3000', // 클라이언트 서버
//     credentials: true,
// }))

// ApolloServer 서버를 생성합니다.
const server = new ApolloServer({
    // typeDefs 라는 것이 필요하고, schema(스키마)라는게 필요합니다.
    typeDefs: schema,
    // 그리고 resolvers이라는 정보가 필요하게됩니다.
    resolvers,
    // 그리고 resolvers가 참조할 데이터.. 즉, DB 역할을하는 models 부분이 정의가 되어있어야 합니다.
    context: {
        db: {
            // 아래 정의에 대해선 이따 하도록 하겠습니다.
            messages: readDB('messages'),
            users: readDB('users'),
        }
    }
});

const app = express(); // <- 이 코드 위치를 applyMiddleware 위쪽인 이 위치로 옮깁니다.
await server.start(); // <- 그리고 server.start() 코드를 await 키워드를 통해 실행하겠습니다.
// 위 정의가 다 되고나면, server.applyMiddleware() 함수를 통해서 아래와 같이 코드를 작성합니다.
server.applyMiddleware({
    app,
    path: '/graphql',
    // 그리고 cors를 이전과 다르게 applyMiddleware의 3번째 인자(옵션값)로 객체형태로 넘겨줍니다.
    cors: {
        origin: 'http://localhost:3000', // 클라이언트 서버
        credentials: true,
    }
})

// 서버 경로는 8000번
// 그리고 아래도 await 키워드를 붙여주고 첫번째 인자 값을 아래와 같이 수정해줍니다.
await app.listen({port: 8000})
// 위의 await 붙은 코드가 끝나고 아래가 실행될거기 때문에 console.log(...)도 이렇게 밖으로 빼줍니다.
console.log('server listening on 8000...')

// 원래는 아래와 같은 모습이었음.
// app.listen(8000, () => { 
//     console.log('server listening on 8000...')
// })

위와 같이 수정을하면 이제 에러없이 실행이됩니다.


yarn run server

$ yarn workspace server start
$ nodemon ./src/index.js
[nodemon] 2.0.12
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): src\**\*
[nodemon] watching extensions: js,mjs,json
[nodemon] starting `node ./src/index.js`
D:\react-training-graphql\server
server listening on 8000...

4.3.2 apollo-server 3.0 playground

apollo-server3.0이 되면서 playground도 내용이 좀 바뀌었습니다.
localhost:8000/graphql url로 들어가면 아래와 같이 접속화면이 바뀌어있습니다.

Query your server 버튼을 클릭하면 아래와 같이 playground가 열리게됩니다.

playground가 이전 2점대 버전과 차이점이 CORS를 별도로 설정해줘야된다라는 점입니다.
현재로써는 corshttps://studio.apollographql.com/ 이 url에 대해서는 cors를 못잡기 때문에 cross-origin 정책에 의해서 접근을 못하는 상태입니다.

Note

corsorigin 부분에 정의할 때 주요사항이 있습니다.

url 끝 부분에 /를 절대 작성하지 마십시오.

/ 하나 붙어있는 것만으로 에러가납니다.
절대 붙이지 마세요.


// server/src/index.js
import express from 'express'
// graphQL용 서버는 ApolloServer라는걸 이용할겁니다.
import {ApolloServer} from "apollo-server-express";
// import cors from 'cors' // cors 라이브러리 불러온거 삭제해줍니다.
import resolvers from './resolvers/index.js'
import schema from './schema/index.js'
import {readDB} from "./dbController.js";

// ApolloServer를 사용하면 아래 2줄 코드는 필요가 없게됩니다.
// app.use(express.urlencoded({extended: true}))
// app.use(express.json()) // express에서 json 형태로 사용하겠다.
// -------- app.use(cors( ... )) 아래 코드는 필요없으니 삭제해줍니다.
// app.use(cors({
//     origin: 'http://localhost:3000', // 클라이언트 서버
//     credentials: true,
// }))

// ApolloServer 서버를 생성합니다.
const server = new ApolloServer({
    // typeDefs 라는 것이 필요하고, schema(스키마)라는게 필요합니다.
    typeDefs: schema,
    // 그리고 resolvers이라는 정보가 필요하게됩니다.
    resolvers,
    // 그리고 resolvers가 참조할 데이터.. 즉, DB 역할을하는 models 부분이 정의가 되어있어야 합니다.
    context: {
        db: {
            // 아래 정의에 대해선 이따 하도록 하겠습니다.
            messages: readDB('messages'),
            users: readDB('users'),
        }
    }
});

const app = express(); // <- 이 코드 위치를 applyMiddleware 위쪽인 이 위치로 옮깁니다.
await server.start(); // <- 그리고 server.start() 코드를 await 키워드를 통해 실행하겠습니다.
// 위 정의가 다 되고나면, server.applyMiddleware() 함수를 통해서 아래와 같이 코드를 작성합니다.
server.applyMiddleware({
    app,
    path: '/graphql',
    // 그리고 cors를 이전과 다르게 applyMiddleware의 3번째 인자(옵션값)로 객체형태로 넘겨줍니다.
    cors: {
        origin: [
            'http://localhost:3000',
            'https://studio.apollographql.com/'
        ], // 클라이언트 서버
        credentials: true,
    }
})

// 서버 경로는 8000번
// 그리고 아래도 await 키워드를 붙여주고 첫번째 인자 값을 아래와 같이 수정해줍니다.
await app.listen({port: 8000})
// 위의 await 붙은 코드가 끝나고 아래가 실행될거기 때문에 console.log(...)도 이렇게 밖으로 빼줍니다.
console.log('server listening on 8000...')

// 원래는 아래와 같은 모습이었음.
// app.listen(8000, () => { 
//     console.log('server listening on 8000...')
// })

그래서 위와같이 cors origincross-origin의 내용을 추가를 해줘야지만 playground에 접속을 할 수 있게됩니다.

Note

아니다. 강의와 똑같다. 이땐 내가 저 메시지를 클릭을 안해서 그랬었다.
메시지 클릭하면 강의 화면과 동일하게 나온다.

여튼 이 상태에서 가운데있는 query는 할 수 있다고함. 음… 아니네?

Note

아까 위에서 말했듯이 url맨 끝에 / 붙어있는지 확인해!!

4.4 GraphQL Playground

localhost:8000/graphql로 접속해보겠습니다.
처음에 나오는 화면에서 Query your server 버튼을 누르면 playground로 들어갈 수 있습니다.

Note

playground상에서 schema에 해당하는 정보를 그대로 요청할 수 있습니다.
messages 요청을 위한 Query를 날릴 수 있습니다.

Note

corsoriginurl 마지막에 / 붙어있는지 확인!?!?!?!?!?

Unable to reach server에 대한 해결을 하고나서 실습해야될거 같다.

Note

공부하다가 이 시점에서 알게됨..
CORS 에러가 해결이 안되었던게..

아니다. 아직 CORS가 해결이 안된 것이었네..
다시 해봐야겠네.. ㅠ

Note

이때알게됨… 뒤에 / 슬래쉬 지웠더니 됨… ㅠㅠ

Note

위와 같이 shcema에 정의한대로 query Messages를 보냈더니 메시지들에 대한 정보가 원하는대로 온다.
여기서 중요한 점은 GraphQL은 내가 원하는 프로퍼티키 값들만 받아올 수 있다는 것이다.
REST API에선 id, text, timestamp, userId 모두 받아왔었다.
하지만 GraphQL에선 내가 원하는 키들만 선택해서 받아올 수 있다.


여튼 위와 같이 데이터가 잘 나오는 것을 확인하실 수 있을겁니다.
만약에 프론트엔드 개발자 입장에서 ‘이번에는 timestamp까진 필요없을거같아', iduserId만 있으면 될거같아라고 생각한다면,
아래와 같이 Query를 작성해서 날리면됩니다.

Note

위와 같이 원하는 프로퍼티키들만 골라내 받아올 수 있습니다.

위와 같이 서버에서 주는 데이터 자체가, 사용자가 요청한 것들만, 즉, 필요한 것들만 주도록 되어있는겁니다. GraphQL에선.
그래서 REST API 경우에는 오버 엔지니어링이 됐을 때, 프론트엔드가 요청을 할 때,
서버에서 주는 정보를 그대로 들고와서 클라이언트에서 취사선택을 했어야했지만,
GraphQL의 경우는 서버에서부터 이미 취사선택된 데이터가 내려옵니다.

messages에 대한 요청은 성공했고 message에 대한 요청도 해보겠습니다.

Note

이번엔 message 하나에 대한 요청입니다.


// 아래와 같이 schema에 정의한 형식 똑같이 작성해주고
query Messages($id: ID!) {
    message(id: $id) {
        id
        userId
    }
}


// Variables 탭에 원하는 id값을 넣어 인자로 전달해 요청합니다.
{
    "id" : "...w"
}

Note

요청을 보내면 이렇게 위와 같이 요청결과가 반환됩니다.

message 하나만 요청하는 것도 성공했습니다.

Note

위와 같이 이미 저희가 schemaQuery를 다 만들어놨기 때문에 이런것들이 가능한겁니다.

Mutationplayground에서 바로 보낼 수 있습니다.
Mutation으로 createMessage()를 보내볼게요.

Note

위와 같이 Mutation 요청도 보낼 수 있습니다.
Query 요청 보내듯이 똑같은 형식으로 작성하고 Variables에 필요한 값인 textuserId를 작성하여 요청합니다.

Note

전체 메시지를 불러오는 요청을 하면, 방금 작성한 메시지가 DB에 추가되었음을 확인할 수 있습니다.

Note

server/src/db/messages.json 파일에도 기록된 것을 확인할 수 있습니다.

이렇게 GraphQL은 서버에서 만들어놓은걸 즉시 확인해볼 수 있다는 장점이 있습니다.
문법적으로 좀 헷갈리는 부분이 있지만 이거는 익숙해지면 되는 문제라고 생각합니다.

GraphQL로 작성하는 서버쪽 작업이 모두 완료가되었습니다.
나중에 아까 말씀드린대로 무한스크롤을 구현하기위해서 server/src/db/messages.json 파일에서 갯수제한을 둬서 몇개씩 데이터 뽑아오고 이런거는 나중에 다시 작업을 하도록 하겠습니다.

바로 이어서 client의 작업도 이어가겠습니다.
GraphQL에선 client에서 수정할 내용이 의외로 많습니다.