Ver3.0 ๐ GraphQL vs REST API ์๋ฒฝ ๋น๊ต ๊ฐ์ด๋

๐ GraphQL vs REST API ์๋ฒฝ ๋น๊ต ๊ฐ์ด๋
์น ๊ฐ๋ฐ์๋ผ๋ฉด ๊ผญ ์์์ผ ํ API ์ค๊ณ์ ๋ชจ๋ ๊ฒ
๐ฏ ์ ์ง๊ธ GraphQL๊ณผ REST๋ฅผ ๋น๊ตํด์ผ ํ ๊น?
์๋
! ์น ๊ฐ๋ฐ์ ํ๋ค ๋ณด๋ฉด ๋๊ตฌ๋ ํ ๋ฒ์ฏค์ ๊ณ ๋ฏผํ๊ฒ ๋๋ ์๊ฐ์ด ์์ด. ๋ฐ๋ก "์ด๋ค API ๋ฐฉ์์ ์ ํํด์ผ ํ ๊น?"๋ผ๋ ์ง๋ฌธ์ด์ง. ๐ค
2024๋
ํ์ฌ, API ์ค๊ณ๋ ๋จ์ํ ๋ฐ์ดํฐ๋ฅผ ์ฃผ๊ณ ๋ฐ๋ ๊ฒ์ ๋์ด์ ์๋น์ค์ ์ฑ๋ฅ, ์ ์ง๋ณด์์ฑ, ๊ทธ๋ฆฌ๊ณ ๊ฐ๋ฐ ์์ฐ์ฑ๊น์ง ๊ฒฐ์ ํ๋ ํต์ฌ ์์๊ฐ ๋์์ด. ํนํ ๋ชจ๋ฐ์ผ ์ฑ์ ํญ๋ฐ์ ์ธ ์ฑ์ฅ๊ณผ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ
์ฒ์ ๋์คํ๋ก ์ธํด API ์ ํ์ ๋์ฑ ์ค์ํด์ก์ง.
REST API๋ 2000๋
๋ ์ด๋ฐ๋ถํฐ ์น ์๋น์ค์ ํ์ค์ฒ๋ผ ์ฌ์ฉ๋์ด ์์ด. ์ง๊ด์ ์ด๊ณ ์ดํดํ๊ธฐ ์ฌ์ฐ๋ฉฐ, HTTP ํ๋กํ ์ฝ์ ํน์ฑ์ ๊ทธ๋๋ก ํ์ฉํ ์ ์๋ค๋ ์ฅ์ ๋๋ถ์ ๋ง์ ๊ฐ๋ฐ์๋ค์ ์ฌ๋์ ๋ฐ์์ง. ํ์ง๋ง ์๊ฐ์ด ์ง๋๋ฉด์ REST์ ํ๊ณ์ ๋ค๋ ๋ช
ํํด์ก์ด.
๊ทธ๋ฆฌ๊ณ 2015๋
, Facebook(ํ Meta)์ด GraphQL์ ์คํ์์ค๋ก ๊ณต๊ฐํ๋ฉด์ API ์ค๊ณ์ ์๋ก์ด ํจ๋ฌ๋ค์์ด ์์๋์์ด. GraphQL์ ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ํํ ์์ฒญํ ์ ์๋ค๋ ํ์ ์ ์ธ ๊ฐ๋
์ผ๋ก ๋ง์ ๊ฐ๋ฐ์๋ค์ ๊ด์ฌ์ ๋์์ง. ๐
๐ REST API ์์ ์ ๋ณตํ๊ธฐ
REST API์ ๊ธฐ๋ณธ ๊ฐ๋
REST๋ Representational State Transfer์ ์ฝ์๋ก, Roy Fielding์ด 2000๋
๋ฐ์ฌ ๋
ผ๋ฌธ์์ ์ฒ์ ์ ์ํ ์ํคํ
์ฒ ์คํ์ผ์ด์ผ. ์น์ ๊ธฐ์กด ๊ธฐ์ ๊ณผ HTTP ํ๋กํ ์ฝ์ ๊ทธ๋๋ก ํ์ฉํ๊ธฐ ๋๋ฌธ์ ์น์ ์ฅ์ ์ ์ต๋ํ ํ์ฉํ ์ ์๋ ์ํคํ
์ฒ์ง. ๐ก
REST์ ํต์ฌ์ ๋ฆฌ์์ค(Resource) ์ค์ฌ์ ์ค๊ณ์ผ. ๋ชจ๋ ๊ฒ์ ๋ฆฌ์์ค๋ก ํํํ๊ณ , ๊ฐ ๋ฆฌ์์ค๋ ๊ณ ์ ํ URI(Uniform Resource Identifier)๋ฅผ ๊ฐ์ง๊ฒ ๋ผ. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ ๋ณด๋ /users, ํน์ ์ฌ์ฉ์๋ /users/123์ฒ๋ผ ํํํ๋ ๊ฑฐ์ง.
ํด๋ผ์ด์ธํธ์ ์๋ฒ๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์งํํ ์ ์์ด์ผ ํด. ์๋ฒ๋ API๋ฅผ ์ ๊ณตํ๊ณ , ํด๋ผ์ด์ธํธ๋ ์ฌ์ฉ์ ์ธ์ฆ์ด๋ ์ธ์ ๊ด๋ฆฌ๋ฅผ ๋ด๋นํ๋ ์์ผ๋ก ์ญํ ์ด ๋ถ๋ฆฌ๋์ง.
2. Stateless (๋ฌด์ํ์ฑ)
๊ฐ ์์ฒญ์ ๋ ๋ฆฝ์ ์ด์ด์ผ ํ๊ณ , ์๋ฒ๋ ํด๋ผ์ด์ธํธ์ ์ํ๋ฅผ ์ ์ฅํ์ง ์์. ๋ชจ๋ ์์ฒญ์๋ ํ์ํ ๋ชจ๋ ์ ๋ณด๊ฐ ํฌํจ๋์ด์ผ ํด.
3. Cacheable (์บ์ ๊ฐ๋ฅ)
HTTP์ ์บ์ฑ ๊ธฐ๋ฅ์ ๊ทธ๋๋ก ์ฌ์ฉํ ์ ์์ด. ์๋ต์ ์บ์ ๊ฐ๋ฅ ์ฌ๋ถ๋ฅผ ๋ช ์ํด์ผ ํ์ง.
4. Uniform Interface (์ผ๊ด๋ ์ธํฐํ์ด์ค)
URI๋ก ์ง์ ํ ๋ฆฌ์์ค์ ๋ํ ์กฐ์์ ํต์ผ๋๊ณ ํ์ ์ ์ธ ์ธํฐํ์ด์ค๋ก ์ํํด์ผ ํด.
5. Layered System (๊ณ์ธตํ)
ํด๋ผ์ด์ธํธ๋ ์๋ฒ์ ์ง์ ์ฐ๊ฒฐ๋์๋์ง, ์ค๊ฐ ์๋ฒ๋ฅผ ํตํด ์ฐ๊ฒฐ๋์๋์ง ์ ์ ์์ด์ผ ํด.
6. Code-On-Demand (์ ํ์ฌํญ)
์๋ฒ๊ฐ ํด๋ผ์ด์ธํธ์ ์คํ ๊ฐ๋ฅํ ์ฝ๋๋ฅผ ์ ์กํ ์ ์์ด์ผ ํด. ์ด๊ฑด ์ ํ์ ์ธ ์ ์ฝ์ด์ผ.
REST API์ HTTP ๋ฉ์๋ ํ์ฉ
REST API๋ HTTP ๋ฉ์๋๋ฅผ ํตํด ๋ฆฌ์์ค์ ๋ํ ์์ ์ ์ ์ํด. ์ด๋ฅผ CRUD(Create, Read, Update, Delete) ์์ ๊ณผ ๋งคํํ๋ฉด ์ดํดํ๊ธฐ ์ฌ์. ๐ง
| HTTP ๋ฉ์๋ | CRUD ์์ | ์ค๋ช | ์์ |
|---|---|---|---|
| GET | Read | ๋ฆฌ์์ค ์กฐํ | GET /users/123 |
| POST | Create | ์ ๋ฆฌ์์ค ์์ฑ | POST /users |
| PUT | Update | ๋ฆฌ์์ค ์ ์ฒด ์์ | PUT /users/123 |
| PATCH | Update | ๋ฆฌ์์ค ๋ถ๋ถ ์์ | PATCH /users/123 |
| DELETE | Delete | ๋ฆฌ์์ค ์ญ์ | DELETE /users/123 |
REST API ์ค๊ณ ์ค์ ์์
์ค์ ๋ธ๋ก๊ทธ ์๋น์ค๋ฅผ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด๋ณผ๊ฒ. REST API๋ก ์ด๋ป๊ฒ ์ค๊ณํ๋์ง ์ดํด๋ณด์! ๐
// ๊ฒ์๊ธ ๋ชฉ๋ก ์กฐํ
GET /api/posts
Response: [
{
"id": 1,
"title": "GraphQL ์์ํ๊ธฐ",
"author": "๊น๊ฐ๋ฐ",
"createdAt": "2024-01-15"
},
...
]
// ํน์ ๊ฒ์๊ธ ์กฐํ
GET /api/posts/1
Response: {
"id": 1,
"title": "GraphQL ์์ํ๊ธฐ",
"content": "GraphQL์...",
"author": "๊น๊ฐ๋ฐ",
"createdAt": "2024-01-15"
}
// ๊ฒ์๊ธ์ ๋๊ธ ์กฐํ
GET /api/posts/1/comments
Response: [
{
"id": 101,
"content": "์ข์ ๊ธ์ด๋ค์!",
"author": "์ด๋๊ธ"
},
...
]
// ์ ๊ฒ์๊ธ ์์ฑ
POST /api/posts
Body: {
"title": "REST API ์๋ฒฝ ๊ฐ์ด๋",
"content": "REST๋..."
}
// ๊ฒ์๊ธ ์์
PUT /api/posts/1
Body: {
"title": "์์ ๋ ์ ๋ชฉ",
"content": "์์ ๋ ๋ด์ฉ"
}
// ๊ฒ์๊ธ ์ญ์
DELETE /api/posts/1
HTTP ํ๋กํ ์ฝ์ ๊ทธ๋๋ก ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ณ๋์ ํ์ต ์์ด๋ ์ดํดํ๊ธฐ ์ฌ์. URL๋ง ๋ด๋ ์ด๋ค ๋ฆฌ์์ค๋ฅผ ๋ค๋ฃจ๋์ง ์ ์ ์์ง.
2. ๋ฐ์ด๋ ์บ์ฑ ์ฑ๋ฅ
HTTP์ ์บ์ฑ ๋ฉ์ปค๋์ฆ์ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ด. GET ์์ฒญ์ ๋ธ๋ผ์ฐ์ ๋ CDN์์ ์๋์ผ๋ก ์บ์ฑ๋๊ธฐ ๋๋ฌธ์ ์ฑ๋ฅ ์ต์ ํ๊ฐ ์ฉ์ดํด.
3. ๊ด๋ฒ์ํ ๋๊ตฌ ์ง์
Postman, Swagger, curl ๋ฑ ๋ค์ํ ํ ์คํธ ๋ฐ ๋ฌธ์ํ ๋๊ตฌ๋ค์ด REST๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ง์ํด. ๊ฐ๋ฐ ์ํ๊ณ๊ฐ ๋งค์ฐ ์ฑ์ํ์ง.
4. ์๋ฒ ๋ถํ ๋ถ์ฐ
Stateless ํน์ฑ ๋๋ถ์ ๋ก๋ ๋ฐธ๋ฐ์ฑ์ด ์ฝ๊ณ , ์๋ฒ๋ฅผ ์ํ ํ์ฅํ๊ธฐ ์ฉ์ดํด.
5. ํ์คํ๋ ์ํ ์ฝ๋
HTTP ์ํ ์ฝ๋(200, 404, 500 ๋ฑ)๋ฅผ ํ์ฉํด ์๋ต ์ํ๋ฅผ ๋ช ํํ๊ฒ ์ ๋ฌํ ์ ์์ด.
ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๊ฒ๋ณด๋ค ๋ ๋ง์ ๋ฐ์ดํฐ๋ฅผ ๋ฐ๊ฒ ๋ผ. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ์ด๋ฆ๋ง ํ์ํ๋ฐ ์ ์ฒด ์ฌ์ฉ์ ์ ๋ณด๋ฅผ ๋ฐ์์ผ ํ๋ ๊ฒฝ์ฐ๊ฐ ๋ง์.
2. Under-fetching ๋ฌธ์
ํ๋์ ํ๋ฉด์ ๊ตฌ์ฑํ๊ธฐ ์ํด ์ฌ๋ฌ ๋ฒ์ API ํธ์ถ์ด ํ์ํด. ๊ฒ์๊ธ๊ณผ ๋๊ธ, ์์ฑ์ ์ ๋ณด๋ฅผ ๊ฐ๊ฐ ๋ฐ๋ก ์์ฒญํด์ผ ํ๋ ์์ด์ง.
3. ์๋ํฌ์ธํธ ์ฆ๊ฐ
๊ธฐ๋ฅ์ด ์ถ๊ฐ๋ ๋๋ง๋ค ์๋ก์ด ์๋ํฌ์ธํธ๋ฅผ ๋ง๋ค์ด์ผ ํด. ๊ด๋ฆฌํด์ผ ํ URL์ด ๊ธฐํ๊ธ์์ ์ผ๋ก ๋์ด๋์ง.
4. ๋ฒ์ ๊ด๋ฆฌ์ ์ด๋ ค์
API๊ฐ ๋ณ๊ฒฝ๋๋ฉด /v1/, /v2/ ๊ฐ์ ๋ฒ์ ๊ด๋ฆฌ๊ฐ ํ์ํ๊ณ , ์ด์ ๋ฒ์ ์ ๊ณ์ ์ ์งํด์ผ ํ๋ ๋ถ๋ด์ด ์์ด.
5. ์ค์๊ฐ ๋ฐ์ดํฐ ์ฒ๋ฆฌ ํ๊ณ
๊ธฐ๋ณธ์ ์ผ๋ก ์์ฒญ-์๋ต ๋ชจ๋ธ์ด๋ผ ์ค์๊ฐ ์ ๋ฐ์ดํธ๋ฅผ ๊ตฌํํ๋ ค๋ฉด WebSocket ๊ฐ์ ๋ณ๋ ๊ธฐ์ ์ด ํ์ํด.
๐ GraphQL ์์ ์ ๋ณตํ๊ธฐ
GraphQL์ ํ์ ๋ฐฐ๊ฒฝ๊ณผ ์ฒ ํ
GraphQL์ 2012๋
Facebook์ ๋ชจ๋ฐ์ผ ์ฑ ๊ฐ๋ฐํ์ด ์ง๋ฉดํ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ํด ํ์ํ์ด. ๋น์ Facebook์ ๋ชจ๋ฐ์ผ ๋คํธ์ํฌ ํ๊ฒฝ์์ ํจ์จ์ ์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๋ ๋ฐฉ๋ฒ์ ๊ณ ๋ฏผํ๊ณ ์์์ง. ๐ค
REST API์ ๊ฐ์ฅ ํฐ ๋ฌธ์ ๋ ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์ ๋์ฑ ๋๋๋ฌ์ก์ด. ๋๋ฆฐ ๋คํธ์ํฌ์์ ์ฌ๋ฌ ๋ฒ์ API ํธ์ถ์ ์ฌ์ฉ์ ๊ฒฝํ์ ํฌ๊ฒ ์ ํ์์ผฐ๊ฑฐ๋ . ๋ํ ๋ค์ํ ํ๋ฉด ํฌ๊ธฐ์ ๊ธฐ๊ธฐ์์ ํ์ํ ๋ฐ์ดํฐ๊ฐ ๋ฌ๋๊ธฐ ๋๋ฌธ์, ํ๋์ REST ์๋ํฌ์ธํธ๋ก๋ ๋ชจ๋ ์๊ตฌ์ฌํญ์ ์ถฉ์กฑ์ํค๊ธฐ ์ด๋ ค์ ์ด.
GraphQL์ ํต์ฌ ์ฒ ํ์ ๊ฐ๋จํด. "ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์ ํํ ์์ฒญํ๊ณ , ๊ทธ๊ฒ๋ง ๋ฐ๋๋ค"๋ ๊ฑฐ์ผ. ์ด๋ฅผ ํตํด ๋คํธ์ํฌ ํจ์จ์ฑ์ ๊ทน๋ํํ๊ณ , ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์์๊ฒ ๋ ๋ง์ ์ ์ฐ์ฑ์ ์ ๊ณตํ์ง. ๐ฏ
GraphQL์ ํต์ฌ ๊ตฌ์ฑ ์์
GraphQL์ ์ดํดํ๋ ค๋ฉด ์ธ ๊ฐ์ง ํต์ฌ ๊ฐ๋ ์ ์์์ผ ํด. ์คํค๋ง(Schema), ์ฟผ๋ฆฌ(Query), ๊ทธ๋ฆฌ๊ณ ๋ฆฌ์กธ๋ฒ(Resolver)์ผ. ์ด ์ธ ๊ฐ์ง๊ฐ ์ ๊ธฐ์ ์ผ๋ก ์๋ํ๋ฉด์ GraphQL์ ๊ฐ๋ ฅํจ์ด ๋ฐํ๋์ง. ๐ช
์คํค๋ง๋ API์ ์ฒญ์ฌ์ง์ด์ผ. ์ด๋ค ๋ฐ์ดํฐ ํ์ ์ด ์๊ณ , ์ด๋ค ์ฟผ๋ฆฌ๋ฅผ ํ ์ ์๋์ง ์ ์ํ์ง. GraphQL์ ๊ฐํ์ ์์คํ ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ ๋ชจ๋ ๊ฒ์ด ๋ช ํํ๊ฒ ์ ์๋์ด์ผ ํด.
type User {
id: ID! # ! ๋ ํ์ ํ๋๋ฅผ ์๋ฏธ
name: String!
email: String
age: Int
posts: [Post!]! # Post ๋ฐฐ์ด
createdAt: DateTime!
}
type Post {
id: ID!
title: String!
content: String!
author: User! # User ํ์
์ฐธ์กฐ
comments: [Comment!]!
publishedAt: DateTime
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
}
type Query {
user(id: ID!): User
users(limit: Int, offset: Int): [User!]!
post(id: ID!): Post
posts(authorId: ID): [Post!]!
}
type Mutation {
createUser(name: String!, email: String!): User!
updateUser(id: ID!, name: String, email: String): User!
deleteUser(id: ID!): Boolean!
createPost(title: String!, content: String!, authorId: ID!): Post!
}
์ฟผ๋ฆฌ๋ ํด๋ผ์ด์ธํธ๊ฐ ์๋ฒ์ ๋ฐ์ดํฐ๋ฅผ ์์ฒญํ๋ ๋ฐฉ๋ฒ์ด์ผ. SQL๊ณผ ๋น์ทํ๊ฒ ์๊ฒผ์ง๋ง, JSON ๊ตฌ์กฐ๋ก ๊ฒฐ๊ณผ๋ฅผ ๋ฐ์ ์ ์์ด.
# ๊ธฐ๋ณธ ์ฟผ๋ฆฌ - ์ฌ์ฉ์ ์ด๋ฆ๋ง ๊ฐ์ ธ์ค๊ธฐ
query {
user(id: "1") {
name
}
}
# ์ค์ฒฉ ์ฟผ๋ฆฌ - ์ฌ์ฉ์์ ๊ทธ์ ๊ฒ์๊ธ๊น์ง
query {
user(id: "1") {
name
email
posts {
title
publishedAt
}
}
}
# ๋ณ์นญ ์ฌ์ฉ - ๊ฐ์ ํ๋๋ฅผ ๋ค๋ฅธ ์ธ์๋ก ์ฌ๋ฌ ๋ฒ ์กฐํ
query {
user1: user(id: "1") {
name
}
user2: user(id: "2") {
name
}
}
# ๋ณ์ ์ฌ์ฉ - ๋์ ์ฟผ๋ฆฌ
query GetUser($userId: ID!) {
user(id: $userId) {
name
email
posts(limit: 5) {
title
}
}
}
# ํ๋๊ทธ๋จผํธ - ์ฌ์ฌ์ฉ ๊ฐ๋ฅํ ์ฟผ๋ฆฌ ์กฐ๊ฐ
fragment UserInfo on User {
name
email
createdAt
}
query {
user(id: "1") {
...UserInfo
posts {
title
}
}
}
๋ฆฌ์กธ๋ฒ๋ ์ค์ ๋ก ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ค๋ ํจ์์ผ. ๊ฐ ํ๋๋ง๋ค ๋ฆฌ์กธ๋ฒ๊ฐ ์๊ณ , GraphQL ์์ง์ด ์ฟผ๋ฆฌ๋ฅผ ๋ถ์ํด์ ํ์ํ ๋ฆฌ์กธ๋ฒ๋ค์ ์คํํ์ง.
// Node.js + Apollo Server ์์
const resolvers = {
Query: {
user: async (parent, { id }, context) => {
// ๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์ฌ์ฉ์ ์กฐํ
return await context.db.users.findById(id);
},
users: async (parent, { limit, offset }, context) => {
return await context.db.users.find()
.limit(limit)
.skip(offset);
}
},
User: {
// User์ posts ํ๋๋ฅผ ์ํ ๋ฆฌ์กธ๋ฒ
posts: async (parent, args, context) => {
// parent๋ User ๊ฐ์ฒด
return await context.db.posts.find({
authorId: parent.id
});
}
},
Mutation: {
createUser: async (parent, { name, email }, context) => {
const newUser = await context.db.users.create({
name,
email,
createdAt: new Date()
});
return newUser;
},
updateUser: async (parent, { id, name, email }, context) => {
const updatedUser = await context.db.users.update(id, {
name,
email
});
return updatedUser;
}
}
};
GraphQL์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค
GraphQL์ ์ง์ง ๊ฐ๋ ฅํจ์ ๊ณ ๊ธ ๊ธฐ๋ฅ๋ค์์ ๋ํ๋. ์ค์๊ฐ ๊ตฌ๋ , ๋ฐฐ์น ์ฒ๋ฆฌ, ๊ทธ๋ฆฌ๊ณ ์ง์์ด(Directives) ๊ฐ์ ๊ธฐ๋ฅ๋ค์ด ์์ง. ๐
GraphQL์ WebSocket์ ํตํ ์ค์๊ฐ ๋ฐ์ดํฐ ๊ตฌ๋ ์ ์ง์ํด. ์ฑํ ์ฑ์ด๋ ์ค์๊ฐ ์๋ฆผ ๊ฐ์ ๊ธฐ๋ฅ์ ๊ตฌํํ ๋ ์ ๋ง ์ ์ฉํ์ง.
# ์คํค๋ง ์ ์
type Subscription {
messageAdded(roomId: ID!): Message!
userStatusChanged(userId: ID!): UserStatus!
}
# ํด๋ผ์ด์ธํธ ๊ตฌ๋
subscription {
messageAdded(roomId: "room123") {
id
content
author {
name
}
createdAt
}
}
// ์๋ฒ ๊ตฌํ (Node.js)
const resolvers = {
Subscription: {
messageAdded: {
subscribe: (parent, { roomId }, { pubsub }) => {
return pubsub.asyncIterator(`MESSAGE_ADDED_${roomId}`);
}
}
},
Mutation: {
sendMessage: async (parent, { roomId, content }, { pubsub, db }) => {
const message = await db.messages.create({
roomId,
content,
createdAt: new Date()
});
// ๊ตฌ๋
์๋ค์๊ฒ ์๋ฆผ
pubsub.publish(`MESSAGE_ADDED_${roomId}`, {
messageAdded: message
});
return message;
}
}
};
์ง์์ด๋ ์ฟผ๋ฆฌ ์คํ ๋ฐฉ์์ ๋์ ์ผ๋ก ๋ณ๊ฒฝํ ์ ์๊ฒ ํด์ค. ์กฐ๊ฑด๋ถ ํ๋ ํฌํจ์ด๋ ๊ฑด๋๋ฐ๊ธฐ ๊ฐ์ ๊ธฐ๋ฅ์ ์ ๊ณตํ์ง.
# @include - ์กฐ๊ฑด์ด true์ผ ๋๋ง ํ๋ ํฌํจ
query GetUser($withEmail: Boolean!) {
user(id: "1") {
name
email @include(if: $withEmail)
}
}
# @skip - ์กฐ๊ฑด์ด true์ผ ๋ ํ๋ ๊ฑด๋๋ฐ๊ธฐ
query GetUser($skipPosts: Boolean!) {
user(id: "1") {
name
posts @skip(if: $skipPosts) {
title
}
}
}
# ์ปค์คํ
์ง์์ด ์ ์
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
adminData: [Data!]! @auth(requires: ADMIN)
userData: [Data!]! @auth(requires: USER)
}
ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ ์์ฒญํ ์ ์์ด. Over-fetching๊ณผ Under-fetching ๋ฌธ์ ๋ฅผ ์๋ฒฝํ๊ฒ ํด๊ฒฐํ์ง. ๋ชจ๋ฐ์ผ ํ๊ฒฝ์์ ํนํ ๋น์ ๋ฐํด.
2. ๋จ์ผ ์๋ํฌ์ธํธ
๋ชจ๋ ์์ฒญ์ด ํ๋์ ์๋ํฌ์ธํธ(/graphql)๋ก ๊ฐ. ์๋ํฌ์ธํธ ๊ด๋ฆฌ๊ฐ ํจ์ฌ ๊ฐ๋จํด์ง๊ณ , API ๋ฒ์ ๊ด๋ฆฌ ๋ถ๋ด๋ ์ค์ด๋ค์ด.
3. ๊ฐ๋ ฅํ ํ์ ์์คํ
์คํค๋ง๋ฅผ ํตํด API์ ๋ชจ๋ ๊ฒ์ด ๋ช ํํ๊ฒ ์ ์๋ผ. TypeScript์ ๊ถํฉ์ด ์๋ฒฝํ๊ณ , ์๋ ์์ฑ๊ณผ ํ์ ์ฒดํฌ๊ฐ ๊ฐ๋ฅํด์ ธ์ ๊ฐ๋ฐ ์์ฐ์ฑ์ด ํฌ๊ฒ ํฅ์๋ผ.
4. ๋ฐ์ด๋ ๊ฐ๋ฐ์ ๊ฒฝํ
GraphQL Playground๋ Apollo Studio ๊ฐ์ ๋๊ตฌ๋ก API๋ฅผ ์ฝ๊ฒ ํ์ํ๊ณ ํ ์คํธํ ์ ์์ด. ๋ฌธ์ํ๋ ์๋์ผ๋ก ๋์ง.
5. ํจ์จ์ ์ธ ๋ฐ์ดํฐ ๋ก๋ฉ
DataLoader ๊ฐ์ ๋๊ตฌ๋ฅผ ์ฌ์ฉํ๋ฉด N+1 ์ฟผ๋ฆฌ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ณ , ๋ฐฐ์น ์ฒ๋ฆฌ์ ์บ์ฑ์ ์๋์ผ๋ก ์ฒ๋ฆฌํ ์ ์์ด.
6. ๋น ๋ฅธ ํ๋กํ ํ์ดํ
ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๊ฐ ๋ฐฑ์๋ ๋ณ๊ฒฝ ์์ด ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ ๋กญ๊ฒ ์กฐํฉํ ์ ์์ด. ๊ฐ๋ฐ ์๋๊ฐ ์ ๋ง ๋นจ๋ผ์ ธ.
7. ์ค์๊ฐ ๊ธฐ๋ฅ ๋ด์ฅ
Subscription์ ํตํด ์ค์๊ฐ ๋ฐ์ดํฐ ์ ๋ฐ์ดํธ๋ฅผ ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด. WebSocket ์ค์ ์ด ํจ์ฌ ๊ฐ๋จํด์ง์ง.
REST์ ๋นํด ๋ฐฐ์์ผ ํ ๊ฐ๋ ์ด ๋ง์. ์คํค๋ง ์ค๊ณ, ๋ฆฌ์กธ๋ฒ ์์ฑ, ์บ์ฑ ์ ๋ต ๋ฑ ์ด๊ธฐ ํ์ต ๋น์ฉ์ด ์๋นํด.
2. ๋ณต์กํ ์บ์ฑ
HTTP ์บ์ฑ์ ์ง์ ํ์ฉํ๊ธฐ ์ด๋ ค์. Apollo Client ๊ฐ์ ์ ์ฉ ์บ์ฑ ์๋ฃจ์ ์ด ํ์ํ๊ณ , ์บ์ฑ ์ ๋ต์ ์ง์ ์ค๊ณํด์ผ ํด.
3. ์ฟผ๋ฆฌ ๋ณต์ก๋ ๊ด๋ฆฌ
ํด๋ผ์ด์ธํธ๊ฐ ๋๋ฌด ๋ณต์กํ ์ฟผ๋ฆฌ๋ฅผ ๋ณด๋ผ ์ ์์ด. ์ฟผ๋ฆฌ ๊น์ด ์ ํ, ๋ณต์ก๋ ๋ถ์ ๋ฑ์ ๋ณด์ ์กฐ์น๊ฐ ํ์ํ์ง.
4. ํ์ผ ์ ๋ก๋ ์ฒ๋ฆฌ
๊ธฐ๋ณธ์ ์ผ๋ก ํ์ผ ์ ๋ก๋๋ฅผ ์ง์ํ์ง ์์. ๋ณ๋์ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ ๋ฉํฐํํธ ์์ฒญ ์ฒ๋ฆฌ๊ฐ ํ์ํด.
5. ์๋ฌ ์ฒ๋ฆฌ์ ๋ณต์ก์ฑ
HTTP ์ํ ์ฝ๋๋ฅผ ํ์ฉํ๊ธฐ ์ด๋ ต๊ณ , ์๋ฌ๋ฅผ ์๋ต ๋ณธ๋ฌธ์ ํฌํจ์์ผ์ผ ํด. ์๋ฌ ์ฒ๋ฆฌ ๋ก์ง์ด ๋ณต์กํด์ง ์ ์์ด.
6. ์๋ฒ ๋ถํ
์๋ชป ์ค๊ณ๋ ์ฟผ๋ฆฌ๋ ์๋ฒ์ ํฐ ๋ถํ๋ฅผ ์ค ์ ์์ด. ์ฟผ๋ฆฌ ๋น์ฉ ๋ถ์๊ณผ ๋ ์ดํธ ๋ฆฌ๋ฏธํ ์ด ํ์์ ์ด์ผ.
7. ๋ชจ๋ํฐ๋ง์ ์ด๋ ค์
๋ชจ๋ ์์ฒญ์ด POST /graphql๋ก ๊ฐ๊ธฐ ๋๋ฌธ์ ์ ํต์ ์ธ HTTP ๋ชจ๋ํฐ๋ง ๋๊ตฌ๋ก๋ ํ์ ํ๊ธฐ ์ด๋ ค์. ์ ์ฉ ๋ชจ๋ํฐ๋ง ์๋ฃจ์ ์ด ํ์ํ์ง.
โ๏ธ GraphQL vs REST: ์ฌ์ธต ๋น๊ต ๋ถ์
์ฑ๋ฅ ์ธก๋ฉด์์์ ๋น๊ต
์ฑ๋ฅ์ API ์ ํ์์ ๊ฐ์ฅ ์ค์ํ ์์ ์ค ํ๋์ผ. ํ์ง๋ง "์ด๋ ๊ฒ์ด ๋ ๋น ๋ฅด๋ค"๋ผ๊ณ ๋จ์ํ๊ฒ ๋งํ ์ ์์ด. ์ํฉ์ ๋ฐ๋ผ ๋ฌ๋ผ์ง๊ฑฐ๋ . ๐โโ๏ธ
REST์ ๊ฒฝ์ฐ:
๋ณต์กํ ํ๋ฉด์ ๊ตฌ์ฑํ๋ ค๋ฉด ์ฌ๋ฌ ๋ฒ์ ์์ฒญ์ด ํ์ํด. ์๋ฅผ ๋ค์ด, ์ฌ์ฉ์ ํ๋กํ ํ์ด์ง๋ฅผ ๋ง๋ ๋ค๋ฉด:
- GET /users/123 (์ฌ์ฉ์ ์ ๋ณด)
- GET /users/123/posts (๊ฒ์๊ธ ๋ชฉ๋ก)
- GET /users/123/followers (ํ๋ก์ ๋ชฉ๋ก)
- GET /users/123/stats (ํต๊ณ ์ ๋ณด)
์ด 4๋ฒ์ ์๋ณต์ด ํ์ํ๊ณ , ๊ฐ ์์ฒญ๋ง๋ค HTTP ์ค๋ฒํค๋๊ฐ ๋ฐ์ํด. ๋ชจ๋ฐ์ผ ๋คํธ์ํฌ์์๋ ์ด๊ฒ ํฐ ๋ฌธ์ ๊ฐ ๋ ์ ์์ง.
GraphQL์ ๊ฒฝ์ฐ:
๋จ ํ ๋ฒ์ ์์ฒญ์ผ๋ก ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๊ฐ์ ธ์ฌ ์ ์์ด:
query {
user(id: "123") {
name
email
posts(limit: 10) {
title
createdAt
}
followers(limit: 5) {
name
}
stats {
postCount
followerCount
}
}
}
๋คํธ์ํฌ ์๋ณต์ด 1๋ฒ์ผ๋ก ์ค์ด๋ค๊ณ , ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ ๋ฐ์์์ ์ ์ก๋๋ ์ต์ํ๋ผ.
REST์ ๊ฐ์ :
HTTP ์บ์ฑ์ ๊ทธ๋๋ก ํ์ฉํ ์ ์์ด. GET ์์ฒญ์ ๋ธ๋ผ์ฐ์ , CDN, ํ๋ก์ ์๋ฒ์์ ์๋์ผ๋ก ์บ์ฑ๋์ง.
// HTTP ํค๋๋ฅผ ํตํ ์บ์ฑ
Cache-Control: max-age=3600, public
ETag: "abc123"
Last-Modified: Wed, 21 Oct 2024 07:28:00 GMT
์ด๋ฐ ํ์ค ์บ์ฑ ๋ฉ์ปค๋์ฆ์ ๊ฒ์ฆ๋์๊ณ ์์ ์ ์ด์ผ.GraphQL์ ๋์ :
POST ์์ฒญ์ ์ฌ์ฉํ๊ธฐ ๋๋ฌธ์ HTTP ์บ์ฑ์ด ์๋์ผ๋ก ์๋ํ์ง ์์. ํ์ง๋ง Apollo Client ๊ฐ์ ๋๊ตฌ๋ ์ ๊ตํ ํด๋ผ์ด์ธํธ ์ฌ์ด๋ ์บ์ฑ์ ์ ๊ณตํด:
// Apollo Client ์บ์ฑ ์ค์
const client = new ApolloClient({
cache: new InMemoryCache({
typePolicies: {
User: {
keyFields: ["id"],
fields: {
posts: {
merge(existing = [], incoming) {
return [...existing, ...incoming];
}
}
}
}
}
})
});
์ ๊ทํ๋ ์บ์๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ ์ค๋ณต์ ์ ๊ฑฐํ๊ณ , ์๋์ผ๋ก UI๋ฅผ ์
๋ฐ์ดํธํ ์ ์์ด.
๊ฐ๋ฐ ์์ฐ์ฑ ๋น๊ต
๊ฐ๋ฐ ์๋์ ์ ์ง๋ณด์์ฑ๋ ์ค์ํ ์ ํ ๊ธฐ์ค์ด์ผ. ํ์ ๊ท๋ชจ์ ํ๋ก์ ํธ ํน์ฑ์ ๋ฐ๋ผ ์ ํฉํ ์ ํ์ด ๋ฌ๋ผ์ง์ง. ๐จโ๐ป
| ์ธก๋ฉด | REST API | GraphQL |
|---|---|---|
| ์ด๊ธฐ ์ค์ | โ
๋งค์ฐ ๊ฐ๋จ Express.js๋ง์ผ๋ก ์์ ๊ฐ๋ฅ |
โ ๏ธ ๋ณต์ก ์คํค๋ง ์ ์, ๋ฆฌ์กธ๋ฒ ์ค์ ํ์ |
| API ๋ฌธ์ํ | โ ๏ธ ์๋ ์์
Swagger ๋ฑ ๋ณ๋ ๋๊ตฌ ํ์ |
โ
์๋ ์์ฑ ์คํค๋ง๊ฐ ๊ณง ๋ฌธ์ |
| ํ๋ก ํธ์๋ ๊ฐ๋ฐ | โ ๏ธ ๋ฐฑ์๋ ์์กด์ ์ ๋ฐ์ดํฐ ํ์์ API ์ถ๊ฐ ์์ฒญ |
โ
๋
๋ฆฝ์ ๊ธฐ์กด ์คํค๋ง ๋ด์์ ์์ ๋กญ๊ฒ ์กฐํฉ |
| ํ์ ์์ ์ฑ | โ ๏ธ ๋ณ๋ ์์
ํ์ TypeScript ํ์ ์๋ ์ ์ |
โ
์๋ ์์ฑ ์คํค๋ง์์ ํ์ ์๋ ์ถ์ถ |
| ํ ์คํธ | โ
๊ฐ๋จ curl, Postman์ผ๋ก ์ฝ๊ฒ ํ ์คํธ |
โ
๊ฐ๋ ฅ GraphQL Playground ์ ๊ณต |
| ๋ฒ์ ๊ด๋ฆฌ | โ ๏ธ ๋ณต์ก /v1/, /v2/ ๋ฑ ๋ฒ์ ๋ถ๋ฆฌ |
โ
๊ฐ๋จ ์คํค๋ง ์งํ๋ก ํด๊ฒฐ |
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ฌ์ฉ์ ํ๋กํ ํ์ด์ง๋ฅผ ๋ง๋ ๋ค๊ณ ๊ฐ์ ํด๋ณด์. ํ์ํ ๋ฐ์ดํฐ๋:
- ์ฌ์ฉ์ ๊ธฐ๋ณธ ์ ๋ณด
- ์ ๊ณตํ๋ ์ฌ๋ฅ ๋ชฉ๋ก
- ๋ฐ์ ๋ฆฌ๋ทฐ
- ํฌํธํด๋ฆฌ์ค
- ํ๋ ํต๊ณ
REST ๋ฐฉ์:
// 5๋ฒ์ API ํธ์ถ ํ์
const user = await fetch('/api/users/123');
const talents = await fetch('/api/users/123/talents');
const reviews = await fetch('/api/users/123/reviews?limit=5');
const portfolio = await fetch('/api/users/123/portfolio');
const stats = await fetch('/api/users/123/stats');
// ๋ฐ์ดํฐ ์กฐํฉ
const profileData = {
...user,
talents,
reviews,
portfolio,
stats
};
GraphQL ๋ฐฉ์:// ๋จ 1๋ฒ์ ์์ฒญ
const { data } = await client.query({
query: gql`
query UserProfile($id: ID!) {
user(id: $id) {
name
profileImage
bio
talents {
title
category
price
}
reviews(limit: 5) {
rating
comment
reviewer { name }
}
portfolio {
title
imageUrl
}
stats {
totalSales
averageRating
responseTime
}
}
}
`,
variables: { id: '123' }
});
GraphQL์ด ํจ์ฌ ๊ฐ๊ฒฐํ๊ณ ํจ์จ์ ์ด์ง? ํ์ง๋ง ์ด๊ธฐ ์ค์ ์ REST๊ฐ ๋ ์ฌ์.
๋ณด์ ์ธก๋ฉด ๋น๊ต
๋ณด์์ ์ ๋ ํํํ ์ ์๋ ์์ญ์ด์ผ. ๋ ๋ฐฉ์ ๋ชจ๋ ์ฅ๋จ์ ์ด ์์ด์ ์ ์คํ๊ฒ ๊ณ ๋ คํด์ผ ํด. ๐
์ฅ์ :
- HTTP ๋ฉ์๋๋ณ๋ก ๊ถํ์ ์ฝ๊ฒ ์ ์ดํ ์ ์์ด
- Rate limiting์ URL ๊ธฐ๋ฐ์ผ๋ก ๊ฐ๋จํ๊ฒ ์ ์ฉ ๊ฐ๋ฅ
- ํ์ค ๋ณด์ ๋๊ตฌ๋ค์ด ์ ์๋ํด
์ฃผ์์ฌํญ:
- ๊ฐ ์๋ํฌ์ธํธ๋ง๋ค ์ธ์ฆ/์ธ๊ฐ ๋ก์ง ํ์
- CORS ์ค์ ์ ์๋ํฌ์ธํธ๋ณ๋ก ๊ด๋ฆฌํด์ผ ํด
// Express.js ๋ฏธ๋ค์จ์ด ์์
app.get('/api/users/:id',
authenticate, // ์ธ์ฆ ํ์ธ
authorize(['user']), // ๊ถํ ํ์ธ
async (req, res) => {
// ๋ก์ง
}
);
app.delete('/api/users/:id',
authenticate,
authorize(['admin']), // ๊ด๋ฆฌ์๋ง ์ญ์ ๊ฐ๋ฅ
async (req, res) => {
// ๋ก์ง
}
);
์ฅ์ :
- ์คํค๋ง ๋ ๋ฒจ์์ ๊ถํ ์ ์ด ๊ฐ๋ฅ
- ํ๋๋ณ๋ก ์ธ๋ฐํ ๊ถํ ์ค์ ๊ฐ๋ฅ
์ฃผ์์ฌํญ:
- ์ฟผ๋ฆฌ ๋ณต์ก๋ ๊ณต๊ฒฉ์ ์ทจ์ฝํ ์ ์์ด
- Depth limiting ํ์
- Query cost analysis ๊ตฌํ ํ์
// ์ฟผ๋ฆฌ ๋ณต์ก๋ ์ ํ
const depthLimit = require('graphql-depth-limit');
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
schema,
validationRules: [
depthLimit(5), // ์ต๋ ๊น์ด 5๋ก ์ ํ
createComplexityLimitRule(1000) // ๋ณต์ก๋ 1000์ผ๋ก ์ ํ
]
});
// ํ๋ ๋ ๋ฒจ ๊ถํ ์ ์ด
const resolvers = {
User: {
email: (parent, args, context) => {
// ๋ณธ์ธ์ด๊ฑฐ๋ ๊ด๋ฆฌ์๋ง ์ด๋ฉ์ผ ์กฐํ ๊ฐ๋ฅ
if (context.user.id !== parent.id && !context.user.isAdmin) {
throw new Error('Unauthorized');
}
return parent.email;
}
}
};
// ์ง์์ด๋ฅผ ํตํ ๊ถํ ์ ์ด
directive @auth(requires: Role = USER) on FIELD_DEFINITION
type Query {
users: [User!]! @auth(requires: ADMIN)
me: User! @auth(requires: USER)
}
1. ๊น์ ์ค์ฒฉ ์ฟผ๋ฆฌ ๊ณต๊ฒฉ:
query MaliciousQuery {
user(id: "1") {
posts {
author {
posts {
author {
posts {
# ๋ฌดํ ๋ฐ๋ณต...
}
}
}
}
}
}
}
์ด๋ฐ ์ฟผ๋ฆฌ๋ ์๋ฒ๋ฅผ ๋ง๋น์ํฌ ์ ์์ด. ๋ฐ๋์ ๊น์ด ์ ํ์ ์ค์ ํด์ผ ํด.2. ๋ฐฐ์น ์ฟผ๋ฆฌ ๊ณต๊ฒฉ:
query {
user1: user(id: "1") { name }
user2: user(id: "2") { name }
user3: user(id: "3") { name }
# ... ์์ฒ ๊ฐ์ ์ฟผ๋ฆฌ
}
๋ณ์นญ์ ์ด์ฉํ ๋๋ ์์ฒญ๋ ์ ํํด์ผ ํด.ํด๊ฒฐ์ฑ :
- Query complexity analysis
- Persistent queries (ํ์ฉ๋ ์ฟผ๋ฆฌ๋ง ์คํ)
- Rate limiting
- Timeout ์ค์
๐ฏ ์ค์ ์ ํ ๊ฐ์ด๋๋ผ์ธ
REST API๋ฅผ ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ
REST๊ฐ ์ฌ์ ํ ๊ฐ๋ ฅํ ์ ํ์ง์ธ ์ํฉ๋ค์ด ์์ด. ๋ฌด์กฐ๊ฑด GraphQL์ด ์ข๋ค๋ ๊ฑด ์คํด์ผ! ๐ช
๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ๊ฐ ์๊ณ , ๊ธฐ๋ณธ์ ์ธ ์์ฑ/์กฐํ/์์ /์ญ์ ๋ง ํ์ํ ๊ฒฝ์ฐ. ๋ธ๋ก๊ทธ, ๊ฐ๋จํ ๊ฒ์ํ, ๊ธฐ๋ณธ์ ์ธ ๊ด๋ฆฌ ์์คํ ๋ฑ.
2. ์บ์ฑ์ด ์ค์ํ ์๋น์ค
๊ณต๊ฐ API๋ ์ ์ ์ฝํ ์ธ ๊ฐ ๋ง์ ์๋น์ค. ๋ด์ค ์ฌ์ดํธ, ๋ฌธ์ ์ฌ์ดํธ, ๊ณต๊ฐ ๋ฐ์ดํฐ API ๋ฑ. HTTP ์บ์ฑ์ ์ด์ ์ ์ต๋ํ ํ์ฉํ ์ ์์ด.
3. ํ์ผ ์ ๋ก๋/๋ค์ด๋ก๋๊ฐ ์ฃผ์ ๊ธฐ๋ฅ
์ด๋ฏธ์ง ํธ์คํ , ํ์ผ ๊ณต์ ์๋น์ค, ๋ฏธ๋์ด ์คํธ๋ฆฌ๋ฐ ๋ฑ. REST์ ๋ฉํฐํํธ ์์ฒญ ์ฒ๋ฆฌ๊ฐ ๋ ์์ฐ์ค๋ฌ์.
4. ๋ง์ดํฌ๋ก์๋น์ค ๊ฐ ํต์
์๋น์ค ๊ฐ ๋จ์ํ๊ณ ๋ช ํํ ์ธํฐํ์ด์ค๊ฐ ํ์ํ ๊ฒฝ์ฐ. ๊ฐ ์๋น์ค๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ์งํํด์ผ ํ ๋.
5. ๋ ๊ฑฐ์ ์์คํ ํตํฉ
๊ธฐ์กด ์์คํ ์ด REST ๊ธฐ๋ฐ์ด๊ณ , ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์ ์ด ํ์ํ ๊ฒฝ์ฐ.
6. ํ์ GraphQL ๊ฒฝํ ๋ถ์กฑ
ํ์ต ์๊ฐ์ด ๋ถ์กฑํ๊ฑฐ๋, ๋น ๋ฅธ ์ถ์๊ฐ ์ค์ํ ๊ฒฝ์ฐ. REST์ ๋ฎ์ ์ง์ ์ฅ๋ฒฝ์ด ์ ๋ฆฌํด.
7. ๊ณต๊ฐ API ์ ๊ณต
์ธ๋ถ ๊ฐ๋ฐ์๋ค์๊ฒ API๋ฅผ ์ ๊ณตํ๋ ๊ฒฝ์ฐ. REST๊ฐ ๋ ์น์ํ๊ณ ๋๊ตฌ ์ง์์ด ๊ด๋ฒ์ํด.
โ๏ธ ๋ฐ์ดํฐ ๊ตฌ์กฐ๊ฐ ๋จ์ํ๊ณ ํ๋ฉด์ ์ธ๊ฐ?
โ๏ธ ๊ฐ ๋ฆฌ์์ค๊ฐ ๋ ๋ฆฝ์ ์ผ๋ก ๊ด๋ฆฌ๋๋๊ฐ?
โ๏ธ ์บ์ฑ์ด ์ฑ๋ฅ์ ํต์ฌ ์์์ธ๊ฐ?
โ๏ธ ํ์ค HTTP ๋๊ตฌ๋ง์ผ๋ก ์ถฉ๋ถํ๊ฐ?
โ๏ธ API ์ฌ์ฉ์๊ฐ REST์ ์ต์ํ๊ฐ?
โ๏ธ ๋น ๋ฅธ ๊ฐ๋ฐ๊ณผ ์ถ์๊ฐ ์ฐ์ ์ธ๊ฐ?
โ๏ธ ํ์ผ ์ฒ๋ฆฌ๊ฐ ์ฃผ์ ๊ธฐ๋ฅ์ธ๊ฐ?
GraphQL์ ์ ํํด์ผ ํ๋ ๊ฒฝ์ฐ
GraphQL์ด ์ง๊ฐ๋ฅผ ๋ฐํํ๋ ์ํฉ๋ค์ ์์๋ณด์. ์ด๋ฐ ๊ฒฝ์ฐ๋ผ๋ฉด GraphQL์ด ๊ฒ์ ์ฒด์ธ์ ๊ฐ ๋ ์ ์์ด! ๐
์์ ๋คํธ์ํฌ, ์ ์์๊ฑฐ๋, ์ฝํ ์ธ ๊ด๋ฆฌ ์์คํ ์ฒ๋ผ ๋ฐ์ดํฐ ๊ฐ ๊ด๊ณ๊ฐ ๋ณต์กํ ๊ฒฝ์ฐ. ์ฌ์ฉ์-๊ฒ์๊ธ-๋๊ธ-์ข์์ ๊ฐ์ ๋ค์ธต ๊ตฌ์กฐ.
2. ๋ค์ํ ํด๋ผ์ด์ธํธ ์ง์
์น, iOS, Android, ์ค๋งํธ์์น ๋ฑ ์ฌ๋ฌ ํ๋ซํผ์ ์ง์ํด์ผ ํ๋ ๊ฒฝ์ฐ. ๊ฐ ํด๋ผ์ด์ธํธ๊ฐ ํ์ํ ๋ฐ์ดํฐ๋ง ์์ฒญํ ์ ์์ด.
3. ๋ชจ๋ฐ์ผ ์ฐ์ ์๋น์ค
๋คํธ์ํฌ ํจ์จ์ฑ์ด ์ค์ํ ๋ชจ๋ฐ์ผ ์ฑ. ๋ฐ์ดํฐ ์ฌ์ฉ๋๊ณผ ๋ฐฐํฐ๋ฆฌ ์๋ชจ๋ฅผ ์ต์ํํ ์ ์์ด.
4. ๋น ๋ฅธ ํ๋กํ ํ์ดํ ํ์
์คํํธ์ ์ด๋ MVP ๊ฐ๋ฐ์ฒ๋ผ ๋น ๋ฅธ ์คํ๊ณผ ๋ฐ๋ณต์ด ํ์ํ ๊ฒฝ์ฐ. ํ๋ก ํธ์๋ ๊ฐ๋ฐ์๊ฐ ๋ฐฑ์๋ ๋ณ๊ฒฝ ์์ด ์์ ๊ฐ๋ฅํด.
5. ์ค์๊ฐ ๊ธฐ๋ฅ์ด ํต์ฌ
์ฑํ , ํ์ ๋๊ตฌ, ์ค์๊ฐ ๋์๋ณด๋ ๋ฑ. Subscription์ ํตํ ์ค์๊ฐ ์ ๋ฐ์ดํธ๊ฐ ๊ฐํธํด.
6. ๋ง์ดํฌ๋กํ๋ก ํธ์๋ ์ํคํ ์ฒ
์ฌ๋ฌ ํ์ด ๋ ๋ฆฝ์ ์ผ๋ก UI ์ปดํฌ๋ํธ๋ฅผ ๊ฐ๋ฐํ๋ ๊ฒฝ์ฐ. ๊ฐ ํ์ด ํ์ํ ๋ฐ์ดํฐ๋ฅผ ์์ ๋กญ๊ฒ ์กฐํฉํ ์ ์์ด.
7. API ๋ฒ์ ๊ด๋ฆฌ ๋ถ๋ด
API๊ฐ ์์ฃผ ๋ณ๊ฒฝ๋๊ณ , ์ฌ๋ฌ ๋ฒ์ ์ ๋์์ ์ ์งํ๊ธฐ ์ด๋ ค์ด ๊ฒฝ์ฐ. ์คํค๋ง ์งํ๋ก ํด๊ฒฐ ๊ฐ๋ฅํด.
โ๏ธ ๋ฐ์ดํฐ ๊ฐ ๊ด๊ณ๊ฐ ๋ณต์กํ๊ณ ๊น์๊ฐ?
โ๏ธ ์ฌ๋ฌ ํ๋ซํผ์ ๋์์ ์ง์ํ๋๊ฐ?
โ๏ธ ๋คํธ์ํฌ ํจ์จ์ฑ์ด ์ค์ํ๊ฐ?
โ๏ธ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ์๋๊ฐ ์ค์ํ๊ฐ?
โ๏ธ ์ค์๊ฐ ๊ธฐ๋ฅ์ด ํ์ํ๊ฐ?
โ๏ธ ํ์ ์์ ์ฑ์ด ์ค์ํ๊ฐ?
โ๏ธ ํ์ด GraphQL ํ์ต์ ํฌ์ํ ์ ์๋๊ฐ?
ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ: ํจ๊ป ์ฌ์ฉํ๊ธฐ
๊ผญ ํ๋๋ง ์ ํํ ํ์๋ ์์ด! ๋ง์ ๊ธฐ์ ๋ค์ด REST์ GraphQL์ ํจ๊ป ์ฌ์ฉํ๊ณ ์์ด. ๊ฐ๊ฐ์ ์ฅ์ ์ ์ด๋ฆฌ๋ ๊ฑฐ์ง. ๐ค
ํจํด 1: ๊ธฐ๋ฅ๋ณ ๋ถ๋ฆฌ
- REST: ํ์ผ ์ ๋ก๋/๋ค์ด๋ก๋, ์ธ์ฆ, ๊ฒฐ์
- GraphQL: ๋ณต์กํ ๋ฐ์ดํฐ ์กฐํ, ์ค์๊ฐ ์ ๋ฐ์ดํธ
// ํ์ผ ์
๋ก๋๋ REST
POST /api/upload
Content-Type: multipart/form-data
// ๋ฐ์ดํฐ ์กฐํ๋ GraphQL
POST /graphql
{
query {
user(id: "123") {
name
uploadedFiles {
url
size
}
}
}
}
ํจํด 2: ํด๋ผ์ด์ธํธ๋ณ ๋ถ๋ฆฌ- ์น ๊ด๋ฆฌ์: REST (๊ฐ๋จํ CRUD)
- ๋ชจ๋ฐ์ผ ์ฑ: GraphQL (๋ณต์กํ ๋ฐ์ดํฐ, ๋คํธ์ํฌ ํจ์จ์ฑ)
ํจํด 3: ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์
- ๊ธฐ์กด ๊ธฐ๋ฅ: REST ์ ์ง
- ์ ๊ท ๊ธฐ๋ฅ: GraphQL๋ก ๊ฐ๋ฐ
- ์ค์ ๊ธฐ๋ฅ๋ถํฐ ์์ฐจ์ ์ผ๋ก GraphQL๋ก ์ ํ
ํจํด 4: GraphQL์ REST ์์
GraphQL์ REST API์ ๋ํผ๋ก ์ฌ์ฉ. ๊ธฐ์กด REST API๋ฅผ ์ ์งํ๋ฉด์ GraphQL์ ์ด์ ์ ํ์ฉ:
// GraphQL ๋ฆฌ์กธ๋ฒ๊ฐ ๋ด๋ถ์ ์ผ๋ก REST API ํธ์ถ
const resolvers = {
Query: {
user: async (parent, { id }) => {
const response = await fetch(`/api/users/${id}`);
return response.json();
},
posts: async (parent, { userId }) => {
const response = await fetch(`/api/users/${userId}/posts`);
return response.json();
}
}
};
๐ ๏ธ ์ค์ ๊ตฌํ ๊ฐ์ด๋
REST API ๊ตฌํ ๋ฒ ์คํธ ํ๋ํฐ์ค
REST API๋ฅผ ์ ๋๋ก ๊ตฌํํ๋ ค๋ฉด ๋ช ๊ฐ์ง ์ค์ํ ์์น๋ค์ ๋ฐ๋ผ์ผ ํด. ๋จ์ํ HTTP๋ฅผ ์ฌ์ฉํ๋ค๊ณ REST๊ฐ ์๋๊ฑฐ๋ ! ๐
1. ๋ช ์ฌ ์ฌ์ฉ, ๋์ฌ ๊ธ์ง
// โ ๋์ ์
GET /getUsers
POST /createUser
DELETE /deleteUser/123
// โ
์ข์ ์
GET /users
POST /users
DELETE /users/123
2. ๊ณ์ธต ๊ตฌ์กฐ ํํ// ์ฌ์ฉ์์ ๊ฒ์๊ธ
GET /users/123/posts
// ๊ฒ์๊ธ์ ๋๊ธ
GET /posts/456/comments
// ํน์ ๋๊ธ
GET /posts/456/comments/789
3. ๋ณต์ํ ์ฌ์ฉ// โ
์ผ๊ด์ฑ ์๋ ๋ณต์ํ
GET /users
GET /users/123
GET /posts
GET /posts/456
4. ์ผ๋ฐฅ ์ผ์ด์ค ์ฌ์ฉ// โ
์ฝ๊ธฐ ์ฌ์ด URL
GET /user-profiles
GET /blog-posts
GET /payment-methods
5. ํํฐ๋ง, ์ ๋ ฌ, ํ์ด์ง// ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํ์ฉ
GET /users?role=admin&status=active
GET /posts?sort=createdAt&order=desc
GET /products?page=2&limit=20
GET /articles?category=tech&author=123
์ ์ ํ ์ํ ์ฝ๋ ์ฌ์ฉ์ API์ ํ์ง์ ํฌ๊ฒ ๋์ฌ์ค:
์ฑ๊ณต ์๋ต (2xx)
- 200 OK: ์ฑ๊ณต์ ์ธ GET, PUT, PATCH
- 201 Created: ์ฑ๊ณต์ ์ธ POST (์ ๋ฆฌ์์ค ์์ฑ)
- 204 No Content: ์ฑ๊ณต์ ์ธ DELETE
ํด๋ผ์ด์ธํธ ์๋ฌ (4xx)
- 400 Bad Request: ์๋ชป๋ ์์ฒญ ํ์
- 401 Unauthorized: ์ธ์ฆ ํ์
- 403 Forbidden: ๊ถํ ์์
- 404 Not Found: ๋ฆฌ์์ค ์์
- 409 Conflict: ๋ฆฌ์์ค ์ถฉ๋ (์ค๋ณต ๋ฑ)
- 422 Unprocessable Entity: ์ ํจ์ฑ ๊ฒ์ฆ ์คํจ
์๋ฒ ์๋ฌ (5xx)
- 500 Internal Server Error: ์๋ฒ ์ค๋ฅ
- 503 Service Unavailable: ์๋น์ค ์ผ์ ์ค๋จ
// Express.js ์์
app.post('/users', async (req, res) => {
try {
// ์ ํจ์ฑ ๊ฒ์ฆ
const errors = validateUser(req.body);
if (errors.length > 0) {
return res.status(422).json({ errors });
}
// ์ค๋ณต ํ์ธ
const existing = await User.findByEmail(req.body.email);
if (existing) {
return res.status(409).json({
error: 'Email already exists'
});
}
// ์์ฑ
const user = await User.create(req.body);
res.status(201).json(user);
} catch (error) {
console.error(error);
res.status(500).json({
error: 'Internal server error'
});
}
});
// JWT ๊ธฐ๋ฐ ์ธ์ฆ ๋ฏธ๋ค์จ์ด
const jwt = require('jsonwebtoken');
const authenticate = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({
error: 'Authentication required'
});
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.user = decoded;
next();
} catch (error) {
res.status(401).json({
error: 'Invalid token'
});
}
};
// ๊ถํ ํ์ธ ๋ฏธ๋ค์จ์ด
const authorize = (roles) => {
return (req, res, next) => {
if (!roles.includes(req.user.role)) {
return res.status(403).json({
error: 'Insufficient permissions'
});
}
next();
};
};
// ์ฌ์ฉ ์
app.get('/admin/users',
authenticate,
authorize(['admin']),
async (req, res) => {
const users = await User.findAll();
res.json(users);
}
);
// Rate Limiting
const rateLimit = require('express-rate-limit');
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100 ์์ฒญ
message: 'Too many requests'
});
app.use('/api/', limiter);
GraphQL API ๊ตฌํ ๋ฒ ์คํธ ํ๋ํฐ์ค
GraphQL์ ์ ๋๋ก ํ์ฉํ๋ ค๋ฉด ์คํค๋ง ์ค๊ณ๋ถํฐ ์ฑ๋ฅ ์ต์ ํ๊น์ง ์ ๊ฒฝ ์จ์ผ ํ ๊ฒ ๋ง์. ํ๋์ฉ ์ดํด๋ณด์! ๐จ
1. ๋น์ฆ๋์ค ๋๋ฉ์ธ ์ค์ฌ ์ค๊ณ
๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ตฌ์กฐ๋ฅผ ๊ทธ๋๋ก ๋ ธ์ถํ์ง ๋ง๊ณ , ํด๋ผ์ด์ธํธ ๊ด์ ์์ ์ค๊ณํด:
// โ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค์ฌ
type User {
user_id: Int!
user_name: String!
created_at: String!
updated_at: String!
}
// โ
๋น์ฆ๋์ค ๋๋ฉ์ธ ์ค์ฌ
type User {
id: ID!
name: String!
displayName: String!
profile: UserProfile!
createdAt: DateTime!
}
type UserProfile {
bio: String
avatar: String
socialLinks: [SocialLink!]!
}
2. Nullable vs Non-Nullable ์ ์คํ๊ฒ ๊ฒฐ์ type User {
id: ID! # ํญ์ ์กด์ฌ (!)
name: String! # ํญ์ ์กด์ฌ
email: String # ์ ํ์ (nullable)
phone: String # ์ ํ์
posts: [Post!]! # ๋ฐฐ์ด์ ํญ์ ์กด์ฌ, ์์๋ ํญ์ ์กด์ฌ
friends: [User!] # ๋ฐฐ์ด์ null ๊ฐ๋ฅ, ์์๋ ํญ์ ์กด์ฌ
}
3. ํ์ด์ง๋ค์ด์
ํจํด// Relay ์คํ์ผ ์ปค์ ๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
type Query {
posts(
first: Int
after: String
last: Int
before: String
): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
// ์คํ์
๊ธฐ๋ฐ ํ์ด์ง๋ค์ด์
(๊ฐ๋จํ ๊ฒฝ์ฐ)
type Query {
posts(limit: Int, offset: Int): PostList!
}
type PostList {
items: [Post!]!
total: Int!
hasMore: Boolean!
}
GraphQL์ ๊ฐ์ฅ ํฐ ์ฑ๋ฅ ๋ฌธ์ ๋ N+1 ์ฟผ๋ฆฌ์ผ. DataLoader๋ก ํด๊ฒฐํ ์ ์์ด:
// DataLoader ์ค์
const DataLoader = require('dataloader');
// ๋ฐฐ์น ๋ก๋ฉ ํจ์
const batchUsers = async (userIds) => {
const users = await User.findAll({
where: { id: userIds }
});
// ID ์์๋๋ก ์ ๋ ฌ
const userMap = {};
users.forEach(user => {
userMap[user.id] = user;
});
return userIds.map(id => userMap[id]);
};
// Context์ DataLoader ์ถ๊ฐ
const createContext = () => ({
loaders: {
user: new DataLoader(batchUsers)
}
});
// ๋ฆฌ์กธ๋ฒ์์ ์ฌ์ฉ
const resolvers = {
Post: {
author: async (post, args, context) => {
// N+1 ๋ฌธ์ ์์ด ๋ฐฐ์น๋ก ๋ก๋ฉ
return context.loaders.user.load(post.authorId);
}
}
};
// ์ฌ์ฉ ์
const server = new ApolloServer({
typeDefs,
resolvers,
context: createContext
});
์ด๋ ๊ฒ ํ๋ฉด 100๊ฐ์ ๊ฒ์๊ธ์ด ์์ด๋ ์ฌ์ฉ์ ์กฐํ๋ ๋จ 1๋ฒ๋ง ์คํ๋ผ!
// 1. ์ฟผ๋ฆฌ ๋ณต์ก๋ ์ ํ
const { createComplexityLimitRule } = require('graphql-validation-complexity');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
createComplexityLimitRule(1000, {
onCost: (cost) => {
console.log('Query cost:', cost);
}
})
]
});
// 2. ๊น์ด ์ ํ
const depthLimit = require('graphql-depth-limit');
const server = new ApolloServer({
typeDefs,
resolvers,
validationRules: [
depthLimit(5) // ์ต๋ 5๋จ๊ณ๊น์ง๋ง ํ์ฉ
]
});
// 3. ํ๋ ๋ ๋ฒจ ์ธ์ฆ
const resolvers = {
User: {
email: (user, args, context) => {
// ๋ณธ์ธ ๋๋ ๊ด๋ฆฌ์๋ง ์ด๋ฉ์ผ ์กฐํ ๊ฐ๋ฅ
if (context.user?.id !== user.id && !context.user?.isAdmin) {
return null; // ๋๋ ์๋ฌ throw
}
return user.email;
},
privateData: (user, args, context) => {
if (!context.user) {
throw new Error('Authentication required');
}
if (context.user.id !== user.id) {
throw new Error('Unauthorized');
}
return user.privateData;
}
}
};
// 4. Persisted Queries (ํ์ฉ๋ ์ฟผ๋ฆฌ๋ง ์คํ)
const persistedQueries = {
'hash123': 'query GetUser($id: ID!) { user(id: $id) { name } }',
'hash456': 'query GetPosts { posts { title } }'
};
const server = new ApolloServer({
typeDefs,
resolvers,
plugins: [{
requestDidStart() {
return {
didResolveOperation({ request, operation }) {
if (!persistedQueries[request.extensions?.persistedQuery?.sha256Hash]) {
throw new Error('Query not allowed');
}
}
};
}
}]
});
๐ ์ค์ ์ฌ๋ก ์ฐ๊ตฌ
๋๊ท๋ชจ ์๋น์ค์ ์ ํ
์ค์ ๋ก ์ ๋ช ํ ์๋น์ค๋ค์ด ์ด๋ค ์ ํ์ ํ๋์ง ์ดํด๋ณด๋ฉด ๋ง์ ์ธ์ฌ์ดํธ๋ฅผ ์ป์ ์ ์์ด. ๊ฐ์์ ์ํฉ์ ๋ง๋ ๊ฒฐ์ ์ ๋ด๋ ธ๊ฑฐ๋ ! ๐ข
- GraphQL ์ฐฝ์์๋ต๊ฒ ์ ๋ฉด ๋์
- ๋ชจ๋ฐ์ผ ์ฑ์ ๋ณต์กํ ๋ฐ์ดํฐ ์๊ตฌ์ฌํญ ํด๊ฒฐ
- ๋ด์คํผ๋, ํ๋กํ, ๋ฉ์ ์ ๋ชจ๋ GraphQL ์ฌ์ฉ
GitHub
- REST API v3์ GraphQL API v4 ๋์ ์ ๊ณต
- ๋ณต์กํ ์ ์ฅ์ ๋ฐ์ดํฐ ๊ตฌ์กฐ์ GraphQL์ด ์ ํฉ
- ๊ฐ๋ฐ์๋ค์ด ํ์ํ ๋ฐ์ดํฐ๋ง ์ ํํ ์์ฒญ ๊ฐ๋ฅ
Shopify
- ์ ์์๊ฑฐ๋์ ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ
- ์ํ-์ฌ๊ณ -์ฃผ๋ฌธ-๊ณ ๊ฐ ๊ฐ ์ฐ๊ฒฐ์ด ๋ง์
- ํํธ๋ ๊ฐ๋ฐ์๋ค์๊ฒ ์ ์ฐ์ฑ ์ ๊ณต
Airbnb
- ์์-ํธ์คํธ-์์ฝ-๋ฆฌ๋ทฐ์ ๋ณต์กํ ๊ด๊ณ
- ๋ค์ํ ํ๋ซํผ(์น, iOS, Android) ์ง์
- ๊ฒ์ ๊ฒฐ๊ณผ ์ต์ ํ์ GraphQL ํ์ฉ
Netflix
- ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ์ GraphQL ๋ ์ด์ด ์ถ๊ฐ
- ๋ค์ํ ๋๋ฐ์ด์ค๋ณ ๋ง์ถค ๋ฐ์ดํฐ ์ ๊ณต
- ์ถ์ฒ ์์คํ ๊ณผ์ ํตํฉ
- ๊ฒฐ์ API์ ๋จ์์ฑ๊ณผ ๋ช ํ์ฑ ์ค์
- ๊ด๋ฒ์ํ ์ธ์ด ์ง์ ํ์
- ๋ฐ์ด๋ ๋ฌธ์ํ์ ๊ฐ๋ฐ์ ๊ฒฝํ
- REST์ ์บ์ฑ ์ด์ ํ์ฉ
Twilio
- ํต์ API์ ์ง๊ด์ ์ธ ์ธํฐํ์ด์ค
- ์ ํ, SMS ๋ฑ ๋จ์ํ ์์ ์ค์ฌ
- ๊ด๋ฒ์ํ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ
AWS
- ์๋ง์ ์๋น์ค์ ๋ ๋ฆฝ์ ์ด์
- ๊ฐ ์๋น์ค๋ณ ๋ช ํํ ์ธํฐํ์ด์ค
- ๋ ๊ฑฐ์ ํธํ์ฑ ์ค์
Slack
- REST API๋ฅผ ์ฃผ๋ ฅ์ผ๋ก ์ฌ์ฉ
- ์ค์๊ฐ ๊ธฐ๋ฅ์ WebSocket ๋ณ๋ ์ฌ์ฉ
- ๋จ์ํ๊ณ ์์ธก ๊ฐ๋ฅํ API ์ ํธ
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์์ ์ ํ
์ฌ๋ฅ ๊ณต์ ํ๋ซํผ์ฒ๋ผ ๋ณต์กํ ๋น์ฆ๋์ค ๋ก์ง์ ๊ฐ์ง ์๋น์ค์์๋ ์ด๋ค ์ ํ์ด ์ข์๊น? ์ค์ ์๋๋ฆฌ์ค๋ฅผ ํตํด ์์๋ณด์! ๐ผ
์ฌ๋ฅ๋ท์ ํต์ฌ ๊ธฐ๋ฅ๋ค์ ์๊ฐํด๋ณด๋ฉด:
- ์ฌ๋ฅ ๋ฑ๋ก ๋ฐ ๊ฒ์
- ์ฌ์ฉ์ ํ๋กํ ๋ฐ ํฌํธํด๋ฆฌ์ค
- ๊ฑฐ๋ ๋ฐ ๊ฒฐ์
- ๋ฆฌ๋ทฐ ๋ฐ ํ์
- ์ค์๊ฐ ์ฑํ
- ์๋ฆผ ์์คํ
ํ์ด๋ธ๋ฆฌ๋ ์ ๊ทผ ์ถ์ฒ:
GraphQL ์ฌ์ฉ ์์ญ:
# ์ฌ๋ฅ ๊ฒ์ ๋ฐ ์์ธ ์ ๋ณด
query SearchTalents($keyword: String!, $category: String) {
talents(keyword: $keyword, category: $category) {
id
title
description
price
seller {
name
rating
responseTime
profileImage
}
reviews(limit: 3) {
rating
comment
buyer { name }
}
portfolio {
images
description
}
}
}
# ์ฌ์ฉ์ ๋์๋ณด๋
query MyDashboard {
me {
profile {
name
bio
stats {
totalSales
averageRating
completedOrders
}
}
activeTalents {
title
views
orders
}
recentOrders {
id
status
buyer { name }
talent { title }
}
notifications(unreadOnly: true) {
type
message
createdAt
}
}
}
# ์ค์๊ฐ ์ฑํ
subscription OnNewMessage($roomId: ID!) {
messageAdded(roomId: $roomId) {
id
content
sender { name }
createdAt
}
}
REST ์ฌ์ฉ ์์ญ:// ํ์ผ ์
๋ก๋ (ํฌํธํด๋ฆฌ์ค ์ด๋ฏธ์ง)
POST /api/upload/portfolio
Content-Type: multipart/form-data
// ๊ฒฐ์ ์ฒ๋ฆฌ
POST /api/payments
{
"orderId": "123",
"amount": 50000,
"method": "card"
}
// ์ธ์ฆ
POST /api/auth/login
POST /api/auth/refresh
POST /api/auth/logout
// ํ์ผ ๋ค์ด๋ก๋
GET /api/files/download/:fileId
์ด๋ฐ ์์ผ๋ก ๊ฐ ๊ธฐ์ ์ ์ฅ์ ์ ์ด๋ฆฌ๋ฉด ์ต์ ์ ์ฌ์ฉ์ ๊ฒฝํ์ ์ ๊ณตํ ์ ์์ด!
๐ ๋ง์ด๊ทธ๋ ์ด์ ์ ๋ต
REST์์ GraphQL๋ก ์ ํํ๊ธฐ
๊ธฐ์กด REST API๋ฅผ GraphQL๋ก ์ ํํ๊ณ ์ถ๋ค๋ฉด? ํ ๋ฒ์ ๋ฐ๊พธ๋ ๊ฑด ์ํํด. ์ ์ง์ ์ธ ์ ๊ทผ์ด ํ์ํ์ง! ๐
1๋จ๊ณ: ์ค๋น ๋ฐ ํ์ต (1-2๊ฐ์)
- ํ ๊ต์ก ๋ฐ GraphQL ํ์ต
- ํ๋กํ ํ์ ํ๋ก์ ํธ๋ก ๊ฒฝํ ์๊ธฐ
- ๊ธฐ์กด API ๋ถ์ ๋ฐ ์คํค๋ง ์ค๊ณ
- ๋๊ตฌ ๋ฐ ์ธํ๋ผ ์ค๋น
2๋จ๊ณ: ๋ํผ ๋ ์ด์ด ๊ตฌ์ถ (1๊ฐ์)
๊ธฐ์กด REST API๋ฅผ ํธ์ถํ๋ GraphQL ๋ํผ ์์ฑ:
// REST API๋ฅผ ๋ํํ๋ GraphQL ๋ฆฌ์กธ๋ฒ
const resolvers = {
Query: {
user: async (parent, { id }) => {
const response = await fetch(`${REST_API_URL}/users/${id}`);
return response.json();
},
posts: async (parent, { userId }) => {
const response = await fetch(
`${REST_API_URL}/users/${userId}/posts`
);
return response.json();
}
}
};
3๋จ๊ณ: ์ ๊ท ๊ธฐ๋ฅ GraphQL๋ก ๊ฐ๋ฐ (์งํ ์ค)- ์๋ก์ด ๊ธฐ๋ฅ์ GraphQL๋ก๋ง ๊ฐ๋ฐ
- ๊ธฐ์กด ๊ธฐ๋ฅ์ REST ์ ์ง
- ์ ์ง์ ์ผ๋ก ์ฌ์ฉ์ ์ด๋
4๋จ๊ณ: ํต์ฌ ๊ธฐ๋ฅ ์ ํ (2-3๊ฐ์)
- ์ฌ์ฉ๋์ด ๋ง์ ์๋ํฌ์ธํธ๋ถํฐ ์ ํ
- A/B ํ ์คํธ๋ก ์ฑ๋ฅ ๋น๊ต
- ๋ฌธ์ ๋ฐ์ ์ ์ฆ์ ๋กค๋ฐฑ ๊ฐ๋ฅํ๋๋ก ์ค๋น
5๋จ๊ณ: REST API ๋จ๊ณ์ ํ๊ธฐ (6๊ฐ์~1๋ )
- ์ฌ์ฉ๋ ๋ชจ๋ํฐ๋ง
- ์ฌ์ฉ์์๊ฒ ๋ง์ด๊ทธ๋ ์ด์ ์๋ด
- ์ถฉ๋ถํ ์ ์ ๊ธฐ๊ฐ ์ ๊ณต
- ์ต์ข ์ ์ผ๋ก REST API ์ข ๋ฃ
1. ํ์ ํธํ์ฑ ์ ์ง
๊ธฐ์กด ํด๋ผ์ด์ธํธ๊ฐ ๊ณ์ ์๋ํ๋๋ก REST API๋ฅผ ๋น๋ถ๊ฐ ์ ์งํด์ผ ํด.
2. ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง
GraphQL ๋์ ํ ์ฑ๋ฅ์ด ์คํ๋ ค ์ ํ๋ ์ ์์ด. ์ง์์ ์ธ ๋ชจ๋ํฐ๋ง์ด ํ์์ผ.
3. ํ ์ญ๋ ๊ณ ๋ ค
๋ชจ๋ ํ์์ด GraphQL์ ์ต์ํด์ง ๋๊น์ง ์ถฉ๋ถํ ์๊ฐ์ ์ค์ผ ํด.
4. ๋ฌธ์ํ
๋ง์ด๊ทธ๋ ์ด์ ๊ณผ์ ๊ณผ ์๋ก์ด API ์ฌ์ฉ๋ฒ์ ์ฒ ์ ํ ๋ฌธ์ํํด์ผ ํด.
5. ๋กค๋ฐฑ ๊ณํ
๋ฌธ์ ๋ฐ์ ์ ์ฆ์ ์ด์ ์ํ๋ก ๋์๊ฐ ์ ์๋ ๊ณํ์ด ์์ด์ผ ํด.
๐ฎ ๋ฏธ๋ ์ ๋ง๊ณผ ํธ๋ ๋
API ๊ธฐ์ ์ ์งํ ๋ฐฉํฅ
REST์ GraphQL ์ธ์๋ ์๋ก์ด API ๊ธฐ์ ๋ค์ด ๊ณ์ ๋ฑ์ฅํ๊ณ ์์ด. ๋ฏธ๋๋ฅผ ์ค๋นํ๋ ค๋ฉด ์ด๋ฐ ํธ๋ ๋๋ ์์๋ฌ์ผ ํด! ๐
1. gRPC
Google์ด ๊ฐ๋ฐํ ๊ณ ์ฑ๋ฅ RPC ํ๋ ์์ํฌ. Protocol Buffers๋ฅผ ์ฌ์ฉํด์ REST๋ณด๋ค ํจ์ฌ ๋น ๋ฅด๊ณ ํจ์จ์ ์ด์ผ.
์ฅ์ :
- ๋ฐ์ด๋๋ฆฌ ํ๋กํ ์ฝ๋ก ๋น ๋ฅธ ์๋
- ์๋ฐฉํฅ ์คํธ๋ฆฌ๋ฐ ์ง์
- ๊ฐ๋ ฅํ ํ์ ์์คํ
๋จ์ :
- ๋ธ๋ผ์ฐ์ ์์ ์ง์ ์ฌ์ฉ ์ด๋ ค์
- ๋๋ฒ๊น ์ด REST๋ณด๋ค ๋ณต์ก
- ํ์ต ๊ณก์ ์ด ๊ฐํ๋ฆ
2. tRPC
TypeScript ๊ธฐ๋ฐ์ end-to-end ํ์ ์์ RPC. GraphQL์ ๋ณต์ก์ฑ ์์ด ํ์ ์์ ์ฑ์ ์ ๊ณตํด.
// tRPC ์์
// ์๋ฒ
const appRouter = router({
getUser: publicProcedure
.input(z.object({ id: z.string() }))
.query(({ input }) => {
return db.user.findById(input.id);
}),
});
// ํด๋ผ์ด์ธํธ (์๋ ํ์
์ถ๋ก !)
const user = await trpc.getUser.query({ id: '123' });
// user์ ํ์
์ด ์๋์ผ๋ก ์ถ๋ก ๋จ
3. Hasura / Supabase๋ฐ์ดํฐ๋ฒ ์ด์ค์์ ์๋์ผ๋ก GraphQL API๋ฅผ ์์ฑํด์ฃผ๋ ์๋น์ค. ๋ฐฑ์๋ ๊ฐ๋ฐ ์๊ฐ์ ํฌ๊ฒ ๋จ์ถ์์ผ.
4. Federation (GraphQL)
์ฌ๋ฌ GraphQL ์๋น์ค๋ฅผ ํ๋๋ก ํตํฉํ๋ ๊ธฐ์ . ๋ง์ดํฌ๋ก์๋น์ค ํ๊ฒฝ์์ ์ ์ฉํด.
// ์ฌ์ฉ์ ์๋น์ค
type User @key(fields: "id") {
id: ID!
name: String!
}
// ์ฃผ๋ฌธ ์๋น์ค
extend type User @key(fields: "id") {
id: ID! @external
orders: [Order!]!
}
1. AI ํตํฉ API
ChatGPT API์ฒ๋ผ AI ๊ธฐ๋ฅ์ ์ ๊ณตํ๋ API๊ฐ ๊ธ์ฆํ ๊ฑฐ์ผ. ์์ฐ์ด ์ฒ๋ฆฌ, ์ด๋ฏธ์ง ์์ฑ, ๋ฐ์ดํฐ ๋ถ์ ๋ฑ.
2. ์ฃ์ง ์ปดํจํ API
Cloudflare Workers, Vercel Edge Functions ๊ฐ์ ์ฃ์ง์์ ์คํ๋๋ API๊ฐ ๋ ๋ณดํธํ๋ ๊ฑฐ์ผ.
3. ์ค์๊ฐ ์ฐ์ ์ค๊ณ
WebSocket, Server-Sent Events, GraphQL Subscriptions ๋ฑ ์ค์๊ฐ ๊ธฐ๋ฅ์ด ๊ธฐ๋ณธ์ด ๋ ๊ฑฐ์ผ.
4. ํ์ ์์ ์ฑ ๊ฐํ
TypeScript์ ๋์คํ๋ก API๋ ํ์ ์์ ์ฑ์ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํ๊ฒ ๋ ๊ฑฐ์ผ.
5. ์๋ํ์ ์ฝ๋ ์์ฑ
์คํค๋ง์์ ํด๋ผ์ด์ธํธ ์ฝ๋, ๋ฌธ์, ํ ์คํธ๋ฅผ ์๋ ์์ฑํ๋ ๋๊ตฌ๊ฐ ๋ ๋ฐ์ ํ ๊ฑฐ์ผ.
๐ ํ์ต ๋ฆฌ์์ค ๋ฐ ๋๊ตฌ
REST API ํ์ต ์๋ฃ
- MDN Web Docs - HTTP
- RESTful API Design Best Practices
- OpenAPI Specification (Swagger)
์ ์ฉํ ๋๊ตฌ:
- Postman: API ํ ์คํธ ๋ฐ ๋ฌธ์ํ
- Insomnia: REST ํด๋ผ์ด์ธํธ
- Swagger UI: API ๋ฌธ์ ์๋ ์์ฑ
- curl: ์ปค๋งจ๋๋ผ์ธ HTTP ํด๋ผ์ด์ธํธ
ํ๋ ์์ํฌ:
- Express.js (Node.js)
- FastAPI (Python)
- Spring Boot (Java)
- Django REST Framework (Python)
- Laravel (PHP)
GraphQL ํ์ต ์๋ฃ
- GraphQL.org - ๊ณต์ ์ฌ์ดํธ
- Apollo Docs - ๊ฐ์ฅ ์ธ๊ธฐ ์๋ GraphQL ํ๋ซํผ
- GraphQL Code Generator
์ ์ฉํ ๋๊ตฌ:
- Apollo Studio: GraphQL ๊ฐ๋ฐ ๋ฐ ๋ชจ๋ํฐ๋ง
- GraphQL Playground: ์ธํฐ๋ํฐ๋ธ IDE
- Altair GraphQL Client: ๊ฐ๋ ฅํ GraphQL ํด๋ผ์ด์ธํธ
- GraphiQL: ๋ธ๋ผ์ฐ์ ๊ธฐ๋ฐ IDE
ํ๋ ์์ํฌ ๋ฐ ๋ผ์ด๋ธ๋ฌ๋ฆฌ:
- Apollo Server (Node.js)
- Apollo Client (React, Vue, Angular)
- Relay (React)
- GraphQL Yoga
- Hasura (์๋ GraphQL ์์ฑ)
- Prisma (ORM + GraphQL)
๐ฌ ๊ฒฐ๋ก : ํ๋ช ํ ์ ํ์ ์ํ ์ต์ข ๊ฐ์ด๋
์, ์ฌ๊ธฐ๊น์ง GraphQL๊ณผ REST API์ ๋ํด ๊น์ด ์๊ฒ ์์๋ดค์ด. ์ด์ ์ ๋ฆฌ๋ฅผ ํด๋ณผ๊น? ๐ฏ
๋ค์ ์ง๋ฌธ๋ค์ ๋ตํด๋ณด์:
Q1: ํ๋ก์ ํธ๊ฐ ๊ธํ๊ฐ?
โ YES: REST ์ถ์ฒ (๋น ๋ฅธ ๊ฐ๋ฐ)
โ NO: ๋ค์ ์ง๋ฌธ์ผ๋ก
Q2: ๋ฐ์ดํฐ ๊ด๊ณ๊ฐ ๋ณต์กํ๊ฐ?
โ YES: GraphQL ๊ณ ๋ ค
โ NO: REST๋ก ์ถฉ๋ถ
Q3: ์ฌ๋ฌ ํ๋ซํผ์ ์ง์ํ๋๊ฐ?
โ YES: GraphQL ์ถ์ฒ
โ NO: ๋ค์ ์ง๋ฌธ์ผ๋ก
Q4: ํ์ด GraphQL ๊ฒฝํ์ด ์๋๊ฐ?
โ YES: GraphQL ๊ฐ๋ฅ
โ NO: REST ๋๋ ํ์ต ์๊ฐ ํ๋ณด
Q5: ์ค์๊ฐ ๊ธฐ๋ฅ์ด ํต์ฌ์ธ๊ฐ?
โ YES: GraphQL Subscriptions ํ์ฉ
โ NO: REST๋ก ์ถฉ๋ถ
Q6: ์บ์ฑ์ด ๋งค์ฐ ์ค์ํ๊ฐ?
โ YES: REST ์ถ์ฒ
โ NO: GraphQL ๊ฐ๋ฅ
Q7: ํ์ผ ์ฒ๋ฆฌ๊ฐ ์ฃผ์ ๊ธฐ๋ฅ์ธ๊ฐ?
โ YES: REST ์ถ์ฒ
โ NO: GraphQL ๊ฐ๋ฅ
โ ๊ฐ๋จํ CRUD ์ ํ๋ฆฌ์ผ์ด์
โ ๊ณต๊ฐ API ์ ๊ณต
โ ์บ์ฑ์ด ์ค์ํ ์๋น์ค
โ ํ์ผ ์ฒ๋ฆฌ ์ค์ฌ ์๋น์ค
โ ๋น ๋ฅธ ํ๋กํ ํ์ดํ ํ์
โ ํ์ GraphQL ๊ฒฝํ ๋ถ์กฑ
GraphQL์ ์ ํํ์ธ์:
โ ๋ณต์กํ ๋ฐ์ดํฐ ๊ด๊ณ
โ ๋ค์ค ํ๋ซํผ ์ง์
โ ๋ชจ๋ฐ์ผ ์ฐ์ ์๋น์ค
โ ์ค์๊ฐ ๊ธฐ๋ฅ ํ์
โ ๋น ๋ฅธ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ์ค์
โ ๋ง์ดํฌ๋ก์๋น์ค ์ํคํ ์ฒ
ํ์ด๋ธ๋ฆฌ๋๋ฅผ ์ ํํ์ธ์:
โ ๋๊ท๋ชจ ํ๋ซํผ
โ ๋ค์ํ ์ฌ์ฉ ์ฌ๋ก
โ ์ ์ง์ ๋ง์ด๊ทธ๋ ์ด์
โ ๊ฐ ๊ธฐ์ ์ ์ฅ์ ํ์ฉ
REST๋ GraphQL์ด๋ ๊ฐ๊ฐ ์ฅ๋จ์ ์ด ์์ด. ํ๋ก์ ํธ์ ํน์ฑ๊ณผ ํ์ ์ํฉ์ ๋ง๋ ์ ํ์ด ์ต์ ์ด์ผ.
2. ์์์ ์๊ฒ, ๋ฐ๋ณต์ ๋น ๋ฅด๊ฒ
์ฒ์๋ถํฐ ์๋ฒฝํ๊ฒ ์ค๊ณํ๋ ค๊ณ ํ์ง ๋ง. ์๊ฒ ์์ํด์ ํผ๋๋ฐฑ์ ๋ฐ๊ณ ๊ฐ์ ํด ๋๊ฐ๋ ๊ฒ ์ค์ํด.
3. ๋๊ตฌ์ ์ฝ๋งค์ด์ง ๋ง
REST๋ GraphQL์ด๋ ๊ฒฐ๊ตญ ๋๊ตฌ์ผ ๋ฟ์ด์ผ. ๋น์ฆ๋์ค ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ๊ฒ ๋ชฉ์ ์ด๋ผ๋ ๊ฑธ ์์ง ๋ง.
4. ์ง์์ ์ธ ํ์ต
API ๊ธฐ์ ์ ๊ณ์ ์งํํ๊ณ ์์ด. ์๋ก์ด ํธ๋ ๋์ ๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๊พธ์คํ ํ์ตํ์.
5. ์ปค๋ฎค๋ํฐ ํ์ฉ
ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ์ปค๋ฎค๋ํฐ์ ๋์์ ๋ฐ์. ์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์ ์ ๋ฌธ๊ฐ์ ์กฐ์ธ์ ๊ตฌํ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ!
์ด ๊ธ์ด ์ฌ๋ฌ๋ถ์ API ์ ํ์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ์ด! REST๋ GraphQL์ด๋ , ์ค์ํ ๊ฑด ํ๋ก์ ํธ์ ์ฑ๊ณต์ด์ผ. ๊ฐ์์ ์ํฉ์ ๋ง๋ ์ต์ ์ ์ ํ์ ํ๊ธธ ๋ฐ๋ผ! ๐
ํน์ ๋ ๊ถ๊ธํ ์ ์ด ์๊ฑฐ๋, ์ค์ ํ๋ก์ ํธ์ ์ ์ฉํ๋ฉด์ ์ด๋ ค์์ ๊ฒช๋๋ค๋ฉด ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํจ๊ป ์ฑ์ฅํ๋ ๊ฐ๋ฐ์ ์ปค๋ฎค๋ํฐ์์ ๋ง์ ์ธ์ฌ์ดํธ๋ฅผ ์ป์ ์ ์์ ๊ฑฐ์ผ! ๐ช
ํ๋ณตํ ์ฝ๋ฉ ๋๊ธธ ๋ฐ๋ผ! Happy coding! ๐
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.