Search
▪️

Working with GraphQL

What is GraphQL?

GraphQL에 대해서 알기 전에 REST API와 비교하여 보자.
REST API라고 함은 Statless하고, Server와 데이터를 교환하기 위한 Client-Independent API이다.
GraphQL 역시 REST API와 크게 다르지는 않다. REST API와 같이 Stateless하고, Server와 데이터를 교환하기 위한 Client-Independent한 API이다. 다만 REST API보다 조금 더 High Query Flexibility를 갖는다.
High Query Flexibility가 무엇일까? REST API의 특징을 잘 살펴보면 REST API가 갖는 Limitation이 보완된 부분이 GraphQL이 갖는 High Query Flexibility이다. 예를 들면 REST API에서는 항상 특정 Routing Path 마다 Return 하는 JSON 데이터가 고정되어 있다. (Server에 그렇게 Return하도록 설정했기 때문이다.) 이 때, A라는 Client에서는 해당 JSON 데이터가 모두 필요할 수도 있지만, B라는 Client에서는 해당 데이터들 중 일부만 필요할 수도 있다. 즉, GraphQL은 Client B와 같은 상황에서, Server가 Return하는 데이터들 중 일부를 선택할 수 있게 지원한다.
만일 GraphQL이 없이 REST API를 이용해서 위와 같은 상황을 처리하려고 하면, Client마다 요구하는 부분에 대해서 Endpoint를 많이 늘려야 하거나 모든 데이터들을 Fetch해오고 필요 없는 데이터들을 그냥 두는 식으로 낭비했을 것이다. 혹은 URL Query를 이용하거나 말이다. (즉, Redundant한 Code들이 많거나 Idle 데이터가 많이 생긴다. 혹은 일일이 URL Query를 명시하거나 말이다. 이렇게 되면 유지보수의 측면에서도 반복되는 노동이 많아지게 된다.) GraphQL은 하나의 REST API Endpoint에서 Return 되는 데이터들 중 Query를 이용하여 일부만 취득하는 등의 행위를 가능하게 하여 위와 같은 문제 상황을 피하게 한다.
GraphQL의 대체적인 흐름은 다음과 같다. 사용할 Query를 Frontend에서 작성하여 Backend로 넘기게 된다. Backend는 Query를 받으면 이를 Parsing하여 요구 받은 데이터에 대해서만 찾아내고 이를 Frontend로 넘기게 된다. (즉, Backend에서 작업하는 일종의 데이터베이스 Query Language와 굉장히 유사함을 볼 수 있다. 해당 작업이 Frontend에서 이뤄지는 것으로 보면 된다.)
GraphQL의 동작 방식은 다음과 같다. Client가 있고, 데이터베이스에 접근할 수 있도록 Server Side 로직을 갖고 있는 Server가 있다고 하자. GraphQL의 세계에서 HTTP Method로 쓸 수 있는 것은 GraphQL에게 날리는 POST Request가 유일하다. (Endpoint는 POST/graphql 단 하나만 있는 것이다.) 이렇게 하나의 Endpoint를 두면서 POST로 Request를 날리는 이유는 무엇인가? → 바로 POST Method에는 Request의 Body가 있고, 이는 Request Body에 Query Expression을 담을 수 있다는 소리가 된다. 이렇게 Query Expression이 Request Body에 담겨서 Server로 전송되면, Server에서는 Request Body에 담긴 Expression을 Parsing하고 Query에 명시된 데이터를 Return하게 되는 것이다.
GraphQL Query는 JSON 형태와 굉장히 유사하다. 첫 {}안에는 Operation Type이 정의된다. Operation Type이 query라 함은 데이터를 얻겠다는 것을 의미한다. (Operation Type으로는 query외에도 Editing, Deleting, Inserting 데이터를 담당하는 mutation, Real Time 데이터의 Subscription을 담당하는 subscription도 있다.) Operation Type이 정의된 다음에 오는 {} 에는 해당 Operation의 Endpoint를 명시한다. Endpoint 다음에 오는 {}에는 Extract하고 싶은 데이터들의 Field Name을 명시한다.
모든 GraphQL의 Operation들은 POST Method로 처리가 되지만, 세 가지 Operation Type들의 특성을 굳이 REST API에 비교를 해보면 다음과 같다. Query에 대해서는 GET과 유사하고, Mutation에 대해서는 POST, PUT, PATCH, DELETE와 유사하다. Subscription의 경우 Real Time Connection에 대한 Set Up을 의미하기 때문에 WebSocket과 유사하다.
즉 전반적인 큰 그림을 다시 상기 해보면, Client가 존재하고 Client는 Request Body에 GraphQL를 담아서 하나의 Endpoint로 유지되는 Path에 POST Method로 Request를 Server로 보내게 된다. 이 때, GraphQL에 담긴 Operation Type이 Query, Mutation, Subscription에 따라서 다르게 처리할 수 있게, Operation을 구분 짓도록 Type Definition을 만들어야 하며 (GraphQL은 Typed Query Langauge를 사용하기 때문에 엄격한 Query, Mutation, Subscription이라는 Type Definition이 존재한다.), 각 Definition에 걸맞는 로직을 Resolver에 구현하도록 한다. 즉, Type Definition은 일종의 Routes이고 Resolver는 일종의 Controllers라고 보면 된다.
요약하면 다음과 같다.
1.
일반적인 Node.js + Express.js Server를 이용한다. (GraphQL은 많은 언어들에서 사용이 가능하다. Node.js에서만 이용 가능한 것이 아니다.)
2.
하나의 Endpoint만을 가진다. (일반적으로 /graphql)
3.
Return 받을 데이터에 대한 정의를 Request Body에 담기 위해서 POST Method만을 사용한다.
4.
들어온 GraphQL에 대해서 Operation Type을 구분하여 이에 따라 Server Side에 존재하는 Resolver에서는 Request Body를 분석하여 요구되는 데이터들을 Fetch하고 Prepare한다.

Understanding the Setup & Writing our First Query

GraphQL을 적용하기 위해선 기존에 작성했던 app.js에 있는 Route들과 socket.js를 삭제하도록 한다. 이와 관련해서 Backend에서 Socket Object를 이용하는 부분들은 모두 삭제하고, GraphQL의 Endpoint를 이용하기 위해서 기존 Route들을 모두 삭제했으므로 기존 Router들도 모두 삭제한다.
기존 Router들과 Socket Object를 모두 삭제했다면, 아래 명령어를 통해서 Third Party Package를 설치하도록 한다. graphql이라고 하는 Third Party Package는 GraphQL Schema를 정의할 수 있도록 돕는다. (Query Definition, Mutation Definition, Subscription Definition들을 정의할 수 있도록 말이다.) express-graphql이라고 하는 Third Party Package는 GraphQL로 들어오는 Incoming Request들을 Parsing할 수 있도록 돕는 Package이다. (자세한 정보가 알고 싶다면, graphql.org에서 찾아본다.)
npm install —save graphql npm install —save express-graphql
graphql이라는 폴더를 생성하여 schema.js와 resolvers.js라는 Script를 두어 GraphQL을 이용한다.
schema.js에는 graphql Package의 buildSchema() Method를 Import하여 사용한다. 사용 방법은 Export 시 buildSchema를 호출하여 인자로 GraphQL을 주고 Schema를 생성한다. (자세한 것들은 Code를 참고하자.) 인자로 쓰이는 GraphQL은 GraphQL의 Type Definition과 Field Name, 그리고 Field Name으로 가져올 데이터의 Type이 되겠다. (Type에 대해서 Strict하게 처리하기 위해서 Type명 뒤에 !를 붙여서 무조건 해당 Type으로 Return 할 것을 알린다. 그렇지 않으면 Error가 난다고 말이다.)
resolvers.js에서는 별도의 Import 없이 Object를 Export하게 된다. 단, Object에 들어가는 내용들은 GraphQL을 받았을 때, 각 GraphQL에 맞게 호출될 Method들이 들어가게 된다. Method의 이름은 Schema를 생성할 때 Definition에 따라 할당 했던 Field Name과 일치해야 한다.
위와 같이 schema.js와 resolvers.js에 대한 정의가 모두 끝났다면, 이 GraphQL들이 Public하게 이용할 수 있도록 Expose해줘야 한다. (즉, Endpoint를 둬야 한다는 것이다.) 이런 작업을 express-graphql Package가 돕는다. 작성된 GraphQL들을 Public하게 돌리는 작업들은 app.js에서 express-grahpql을 Import하여 작업한다.
express-graphql의 Import가 끝났다면, app.use() Method를 통해서 /graphql에 대한 Endpoint를 생성하도록 한다. 이 때, /graphql Endpoint에 해당하는 Callback Function은 Import했던 express-graphql이 되겠다. graphqlHttp() Method는 JavaScript Object를 인자로 받아 Config된다. JavaScript Object에서 받는 Key 값은 schema와 rootValue가 되고 Key의 Value값은 위에서 정의했던 schema.js 그리고 resolvers.js Script를 담은 상수가 되겠다.
** 작성한 GraphQL에 대해서 POSTMAN으로 테스트할 때는, POST Method를 이용하여 Endpoint로 Request를 날리고, Request의 Body는 raw한 JSON으로 담아서 보내면 된다. JSON으로 넘기는 데이터는 GraphQL의 Query이니 Query Langauge는 Official Document를 찾아보도록 한다.

Defining a Mutation Schema

Mutation Schema를 정의하는 것은 Query Schema를 정의하는 것과 크게 다르지 않지만, Type Definition에 대한 설정과 Field Name을 설정을 했을 때, Field Name이 갖는 Syntax와 Arguments가 조금 다르다. Query와 같이 :으로 값을 할당하지 않고 Method 처럼 ()를 이용하여 Argument에 대한 명시를 쉽게 만들어 준다. 자세한 것들은 Code를 직접 살펴보자.
GraphQL에서는 ID Type을 지원한다. Unique한 값이고 ID임을 암시한다. 이렇게 생성된 ID Type은 사실 일종의 String Type이기 때문에, 만약 ID 값을 이용해야 할 때 Mongoose와 같이 ObjectId Type으로 Return하게 된다면, Spread Operator와 ._doc을 이용하여 ID 값을 모두 펼친 다음에 ID 값을 String으로 만들어 Override 해줘야 한다. 또한 GraphQL은 Date에 대한 Type이 존재하지 않기 때문에 String으로 할당해야 하며, Type을 할당할 때는 type Keyword로 직접 생성한 Type으로도 할당이 가능하다.

Adding a Mutation Resolver & GraphiQL

Mutation에 대한 Schema를 설정한 후, 해당 Mutation의 로직을 Resolver에 작성했다면 이를 Test하는 방법은 여럿 있다.
첫 째는 POSTMAN을 그대로 이용하는 것이다. 두 번째 방법은 이보다 더 쉬운 테스트 방법인데, 바로 GraphiQL을 이용하는 것이다. GraphiQL에 대한 설정은 GraphQL의 Endpoint를 설정하는 graphqlHttp의 schema와 rootValue를 할당하던 것에 이어서 graphiql의 Key에 할당한다. (추가적으로 언급하자면, GraphQL은 POST로만 요청을 날리는데도 불구하고, app.post()가 아닌 app.use()를 이용한 이유가 바로 GraphiQL을 이용하기 위해서이다.) graphiql: true로 설정을 하면, app.use()에 할당한 Endpoint로 접속 시 내가 작성한 GraphQL API를 테스트할 수 있는 Tool을 볼 수 있다.

Adding Input Validation

GraphQL을 통해서 Input으로 들어간 값이, Resolver를 통해서 데이터베이스에 추가되기 때문에 추가되는 데이터가 Valid함을 확실히 해야 한다.
REST API의 경우, Route의 Middleware로 Express Validator를 이용하여 검증했지만, GraphQL에서는 하나의 Endpoint만이 이용되기 때문에 이용할 수가 없다. 즉, GraphQL을 이용하게 되면 이런 Validation들은 Resolver에서 처리를 해야 한다. 아래 명령어로 Third Party Package를 설치한다.
npm install —save validator
Resolver에서 validator를 Import하여 사용한다. 사용 방법은 Express Validator와 굉장히 유사하다.

Handling Errors

여러 Validation을 거쳐야 하는 경우 errors라는 Array를 두고 Error 발생마다 Push한 뒤, 최종적으로 errors Array의 길이가 0보다 크면 Error Throw를 하는 기법을 사용하게 된다. 이 때, 더 많은 Information들을 담아서 Error Throw를 해야 하는 경우도 있을 것이고 이에 대해서 제대로 된 Error Handling도 필요하다.
따라서 GraphQL이 담긴 Query를 날렸을 때, Error에 대한 추가적인 정보를 받기 위해선 GraphQL로 부터 받는 데이터의 Format을 정해야 한다. 즉, Error에 대한 구체적인 데이터를 받고자 Error Handling을 하려면 Error 데이터에 대한 Format을 지정해야 하고, 이는 GraphQL Endpoint를 받아서 설정한 graphqlHttp() Method의 인자인 JavaScript Object에 formatError라는 함수를 할당함으로써 설정을 할 수 있다. 자세한 것은 Code를 살펴보자.
** GraphQL의 경우 message는 Throw된 Error로부터 취합할 수 있지만, message를 제외한 추가적인 Information들은 formatError() Method가 사용하는 GraphQLError Class가 자체적으로 originalError로 Wrapping 시킨다. 따라서 message는 .message로 Direct하게 Extract하는 것이고, 추가적으로 할당한 data나 code들은 .originalError.data, .originalError.code와 같이 Extract 하는 것이다.
** GraphQL은 POST, GET Request 이외는 허용하지 않기 때문에 Browser에서 직접 날리는 OPTION S Request는 Deny되면서 오류가 발생하기도 한다. 이에 대해서는 CORS Error을 해결했던 부분에서 특정 로직을 추가함으로써 방지할 수 있다. (Header 값을 설정하여 CORS Error를 해결했던 Middleware에서 OPTIONS Request에 대한 작업을 하는 이유는, CORS Error 자체가 Frontend와 Backend의 도메인이 달라서 생기는 Connection 문제이기 때문에, 가장 먼저 처리해야 하는 Connection이 일어나는 시점에 Request에 대한 작업을 하기 위해서이다.) 추가 되는 로직은 OPTIONS Request에 대해서는 GraphQL Middleware까지 가지 못하도록 response.sendStatus(200)으로 Empty Response를 Return시켜서 next() Method를 호출하지 못하도록 하는 것이다. (sendStatus() Method를 이용하지 않고 단순히 status() Method 만 이용 시, OPTIONS Request에 대한 Response Return이 없기 때문에 Response를 기다리다가 요청 시간이 넘어가면서 GraphQL POST 요청이 Accept되지 않는다.)

Adding a Login Query & Resolver

GraphQL에서의 Authentication은 REST API에서 작동하던 것과 비슷할까? → Yes, GraphQL도 Stateless 한 특성을 갖기 때문에 Token을 두고, 이 Token도 매 Request를 보낼 때 같이 보내게 된다.
Login같은 경우는 사용자 데이터를 Query로 넘기고, Token을 Response로 받아오면 되는 것이다.
** GraphQ의 Type Definition이 mutation이 아닌 일반 query라면 Query를 날릴 때, query : `query`와 같이 query를 중복하여 쓸 필요 없이 바로 `{}`와 같이 작성할 수 있다.

Uploading Images

GraphQL은 오로지 JSON 데이터로만 작동된다. 그렇다면 이미지 FIle에 대해서는 어떻게 처리하면 좋을까? → Third Party Package 중에 GraphQL로 이를 돕는 것이 있다. 하지만 이것보다도 결국에 가장 깔끔한 처리는... 기존의 REST API처럼 Endpoint를 두는 것이 가장 깔끔하다. 즉, 별도로 처리하여 해당 Endpoint에서는 Image를 저장하도록 하고 이미지의 Path를 Return 시키는 것이다.
Endpoint를 추가로 두어 이미지를 저장시키면, 이미지의 Path를 Return 하도록 구현한다고 했다. 이렇게 Return 받은 Path들은 Request에 다시 담겨져 다른 데이터들과 GraphQL에 쓰이게 된다.

Using Variables

단순히 GraphQL을 작성한다고 끝이 아니다. 작성한 GraphQL에 대해서 Optimize를 해줘야 한다.
이번 Chapter에서 구현한 GraphQL도 충분히 괜찮은 Code이고 문제도 없지만, 그리 권장되는 패턴은 아니기 때문이다. 어떤 것들을 Optimize하고 수정해야 하는가? → 사실 GraphQL에 대한 Type Definition 뒤에 따라는 Query 내에는 dynamic한 값이 들어오는 것이 좋지 않다. 여기서 말하는 dynamic한 값이란 Query내에서 쓰인 ${}와 같은 Interpolation Syntax를 의미한다.
해줘야 하는 것들은 다음과 같다. Internal Variable을 통해서 dynamic한 값을 쓸 것이라는 명시와 dynamic한 값의 Type이 무엇인지 명시하는 것이다.
mutation이나 subscription의 경우 `$typeDefinition`에서 typeDefinition 뒤에, 그리고 query의 경우 query라는 Type Definition을 명시하고 그 뒤에, Query 이름을 (임의로 지정이 가능하다.) 설정하고, Query 이름 뒤에 괄호를 쓴 뒤, 그 안에는 dynamic하게 쓸 변수 이름 (임의로 지정이 가능하다.)을 $표시와 함께 쓴다. (Curly Brace는 필요하지 않다.) 또한, $와 함께 명시한 dynamic한 값의 타입을 : $typeName과 같이 GraphQL Syntax로 표기한다. 이렇게 지정한 dynamic값이 들어오는 변수 이름을 $까지 포함하여 실제 Query에서 사용하는 변수 이름과 동일하게 매치 시킨다.
그렇다면 위 과정을 요약하면 Type Definition 뒤에 오는 임의로 정한 Query 이름의 인자로 쓰이는 임의로 정한 변수 이름이 dynamic한 값을 가지게 되는 것이고, 이에 대한 Type을 명시한 것이라고 볼 수 있다. 그리고 이렇게 정한 dynamic한 값을 갖는 변수를 실제 Query에 할당하는 것이다. 이 때, 변수 자체는 임의로 정한 것인데 실제 변수로 사용하는 값에 대한 할당은 어떻게 하는 것인가? → GraphQL의 Query는 JavaScript Object로 이뤄져 있고, 실제 Query는 query라는 Key에 들어가 있다. 즉, 임의로 정한 변수에 대한 실제 변수의 할당은 variables라는 Key에 할당하도록 한다. variables라는 Key는 JavaScript Object를 Value로 갖는다. 이 Object 내에서 dynamic 변수에 대한 실제 변수를 할당하면 된다.
자세한 것은 Code를 참고하자.
** REST API를 삭제하고 GraphQL로 구현하게 되면 데이터에 대해서 더 Flexible하게 처리할 수 있게 될 뿐 아니라, 이를 통해 Frontend의 조금 더 빨라진 성능을 맛 볼 수도 있다. (Backend에서는 Frontend가 원하는 Entire Bandwidth 데이터들 만을 제공하기 때문이다.)