1. GraphQL이란
GraphQL은 SQL과 마찬가지로 쿼리 언어 중 하나이다.
- SQL: 데이터 베이스 시스템에 저장된 데이터를 효율적으로 가져오는 것이 목적
- GraphQL:웹 클라이언트가 데이터를 서버로부터 효율적으로 가져오는 것이 목적
즉, 클라이언트와 서버간 통신을 위해 주로 사용된다.
1) 장점
- 필요한 정보만 선택하여 받아올 수 있다.
- RESTAPI의 Overfetching 문제 해결
- 데이터 전송량 감소
- 여러 계층의 정보들을 한 번에 받아올 수 있다.
- RESTAPI의 Underfetching 문제 해결
- 요청 횟수 감소
- 하나의 Endpoint에서 모든 요청을 처리한다.
- 하나의 URI에서 POST로 모든 요청이 가능하다.
2) Apollo
GraphQL은 RESPAPI와 마찬가지로 소프트웨어 통신을 원할 하게 위한 명세일 뿐이다.
그래서 GraphQL을 구현할 솔루션이 필요하다. 그 중 하나의 솔루션이 Apollo이다.
백엔드에서 정보를 제공 및 처리
프론트엔드에서 요청을 전송
2. Apollo 서버 구축하기
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
const database = require('./database')
const { ApolloServer, gql } = require('apollo-server')
// typeDef: GraphQL 명세에서 사용될 데이터의 타입 (schema), 요청의 타입을 지정한다.
// gql(template literal tag)로 생성된다.
const typeDefs = gql`
type Query {
teams: [Team]
}
type Team {
id: Int
manager: String
office: String
extension_number: String
mascot: String
cleaning_duty: String
project: String
}
`
// resolver: 서비스의 액션들을 함수로 지정한다.
// 요청에 따라 데이터를 반환, 입력, 수정, 삭제한다.
const resolvers = {
// teams를 요청하면, database의 team을 반환하는 함수를 실행한다.
Query: {
teams: () => database.teams
}
}
// ApolloServer 클래스는 typeDefs와 resiolvers를 인자로 받아서 서버를 생성한다.
const server = new ApolloServer({ typeDefs, resolvers })
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`)
})
typeDef에 대해 좀 더 자세히 살펴보면 Query의 루트 타입이 지정되어 있음을 확인할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Query로 teams을 요청하면 Team의 배열을 확인할 수 있다 (요청에 대한 타입)
type Query {
teams: [Team]
}
// Team의 타입은 다음과 같다. (데이터에 대한 타입)
type Team {
id: Int
manager: String
office: String
extension_number: String
mascot: String
cleaning_duty: String
project: String
}
1) Schemas and Types
GraphQL에서 데이터의 타입을 지정할 때, 사용되는 타입들은 정해져있다.
우선, GraphQL 스키마의 가장 기본적인 구성 요소는 객체 유형이다. 그 외에도 기본적인 타입들이 다음과 같이 존재한다.
타입 | 설명 |
---|---|
ID | 기본적으로는 String이나, 고유 식별자 역할임을 나타냄 |
String | UTF-8 문자열 |
Int | 부호가 있는 32비트 정수 |
Float | 부호가 있는 부동소수점 값 |
Boolean | 참/거짓 |
! | null을 반환할 수 없다. |
[Type] | 배열 |
이외에도 enum, union, interface, input 타입이 존재한다. (자세한 내용은 공식문서 참고)
2) Query (Read)
일반적으로 조건에 맞는 데이터를 찾으려는 경우가 많다.
그런 경우 인자를 사용하여 조건을 제한할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// typeDefs: id를 인자로 받아 해당 값을 가지고 있는 team을 반환한다.
const typeDefs = gql`
type Query {
team(id: Int): Team
}
`
// resolver
const resolvers = {
Query: {
team: (parent, args, context, info) => database.teams
.filter((team) => {
return team.id === args.id
})[0]
}
}
여러 계층의 정보를 요청할 때는 아래와 같이 해결 할 수 있다. (UnderFetching 문제 해결 )
예를 들면, Team과 Supply가 1:N 관계일 때 Team의 타입에 supplies라는 필드를 만들어 Supply의 배열 타입을 추가한다.
그 후, Team의 데이터를 요청 할때 해당 Team에 맞는 Supply를 supplies 필드에 추가하는 로직을 넣어 Supply 데이터를 함께 반환한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 해당 team이 소유하고 있는 supplies를 같이 요청한다.
const typeDefs = gql`
type Query {
teams: [Team]
}
type Team {
id: Int
manager: String
office: String
extension_number: String
mascot: String
cleaning_duty: String
project: String
supplies: [Supply]
}
type Supply {
id: string
team: Int
}
`
const resolvers = {
Query: {
teams: () => database.teams
.map((team)=> {
team.supplies = database.supplies
.filter((supply) => {
return supply.team === team.id
})
return team
})
}
}
3) Mutation (Create, Update, Delete)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 해당 팀을 삭제하고 삭제한 팀을 반환한다.
const typeDefs = gql`
type Mutation {
deleteTeam(id:Int): Team
}
`
const resolvers = {
Mutatation: {
deleteTeam: (parent, args, context, info) => {
const deleted = database.teams
.filter((team) => {
return team.id === args.id
})[0]
database.team = database.teams
.filter((team) => {
return team.id !== args.id
})
return deleted
}
}
}
5. Request
클라이언트에서는 데이터를 요청하는 방식을 알아보면, 이는 기본적으로 특정한 필드를 요청하는 방식으로 되어 있다.
1
2
3
4
5
6
7
8
9
// teams라는 Action을 통해 Team 데이터를 요청한다.
// Team 데이터의 id, manager, office 필드만 요청하고 있다.
query requestTeam {
teams {
id
manager
office
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 반환 되는 배열을 별칭 myTeam으로 지정할 수도 있다.
query requestTeam {
myTeam: team {
id
manager
office
}
}
// 조건을 지정할 때는 다음과 같이 소괄호를 이용하여 인자를 전달한다.
query requestTeam {
team(id:1) {
id
manager
office
}
}
// 변수를 사용하여 인자에 전달할 수도 있다. (동적 쿼리)
// 선언된 변수는 scalars, enums, or input object 타입 중 하나여야 한다.
query requestTeam($id: Id) {
team(id: $id) {
id
manager
office
}
}
// 변수에 기본 값을 지정해 줄 수도 있다.
// 변수를 사용하여 인자에 전달할 수도 있다.
query requestTeam($id: Id = ABC) {
team(id: $id) {
id
manager
office
}
}
// ...을 이용하여 타입에 제한을 줄 수도 있다.
// 반환되는 데이터의 manager가 Developer 타입인지 Marketing 타입인지에 따라 반환되는 데이터가 다르다.
query requestTeam {
teams {
manager
... on Developer {
office
}
... on Marketing {
id
}
}
}
타입이 복잡한 경우 Fragment를 사용하여 재사용 할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
const TeamInfo = gql`
fragment teamInfo on Team {
manager
year
}
`
const WorkInfo = gql`
fragment workInfo on Team {
office
role
}
`
const Get_TEAM = gql`
query GetTeam {
team {
id
...workInfo
...teamInfo
}
${WorkInfo}
${TeamInfo}
}
`
또한, 지시어를 사용하여 좀 더 편리하게 데이터를 받을 수 있다.
1
2
3
4
5
6
7
8
9
const Get_TEAM = gql`
query GetTeam($id: Int!, $checkManager:Boolean = true) {
team(id: $id) {
id
manager @include(if: $checkManager)
office
}
}
`
@include(if: Boolean)
: 인자가true
인 경우에만 이 필드를 결과에 포함한다.@skip(if: Boolean)
인자가true
이면 이 필드를 건너뛴다.