프론트가 알아야 할 API 규칙 : REST API와 GraphQL API

API 디자인은 보통 백엔드 개발자의 영역이지만 프론트 개발자 또한 Mock API를 디자인하거나 백엔드 개발자와의 원활한 협업을 해야하기 때문에 API디자인 규칙 및 방법론을 알아야 한다! 그렇기 때문에 서버 API를 만드는 방법론 중인 REST APIGraphQL API에 대해서 알아보려고 한다!

REST API

REST API란?

웹에서 사용되는 데이터나 자원(Resource)을 HTTP URI로 표현하고, HTTP 프로토콜을 통해 요청과 응답을 정의하는 방식을 말한다. 이 방법론은 많은 Server API들을 구성하기 위해 사용되었고, 현재에도 많이 사용되고 있다!

좀 더 자세히 알아보자면 REST API에서는 HTTP메소드를 이용해 서버와 통신한다. GET을 통해 웹 페이지나 데이터를 요청하고, POST로 새로운 글이나 데이터를 전송하고 DELETE로 저장된 글이나 데이터를 삭제할 수 있다. 이처럼 클라이언트와 서버가 HTTP 통신을 할 때는 어떤 요청을 보내고 받느냐에 따라 메소드의 사용이 달라진다.

그렇다면 어떻게 해야 좋은 REST API를 디자인 할 수 있을까?

좋은 REST API를 디자인 하는 방법

REST API를 작성할 때는 몇 가지 지켜야 할 규칙들이 있다.
크게 4단계 모델로 REST 성숙도 모델을 구조화하면 다음과 같다.
이 단계들을 모두 충족해야 REST API라고 생각할 수 있지만, 엄밀히 3단계 까지는 지키기 어렵기 때문에 2단계까지만 적용해도 좋은 API 디자인이라고 할 수 있다. 이러한 경우에는 HTTP API라고 불려지게 된다.

REST 성숙도 모델 - 0단계 (HTTP 프로토콜 사용)

0단계에서는 단순히 HTTP 프로토콜을 사용하기만 해도 된다. 그렇지만 이 경우 해당 API를 REST API라고 할 수는 없다!
0단계는 좋은 REST API를 작성하기 위한 기본 단계이다.

예를 들어 허준이라는 이름의 주치의의 예약 가능한 시간을 확인하고 어떤 특정 시간에 예약하는 상황을 예로 들어 보자!

위 예시에서 HTTP 프로토콜을 사용하고 있는 것을 확인할 수 있다. 이렇듯 단순히 HTTP 프로토콜을 사용하는 것이 REST API의 출발점이다.

REST 성숙도 모델 - 1단계 (개별 리소스와 통신 준수)

이 단계는 개별 리소스와의 통신을 준수해야하는 단계이다. 즉, 웹에서 사용되는 모든 데이터나 자원(Resource)을 HTTP URI로 표현한다는 의미이다. 그래서 모든 자원은 개별 리소스에 맞게 엔드 포인트를 사용해야하고 요청받은 자원에 대한 정보를 응답으로 전달해야한다.

앞에서 0단계에서는 모든 요청에서 엔드포인트로 /appointment를 사용했다. 하지만 1단계에서는 요청하는 리소스가 무엇인지에 따라 각기 다른 엔드포인트로 구분하여 사용해야한다.

위의 예시에서 예약 가능한 시간 확인이라는 요청의 응답으로 받게 되는 자원(리소스)은 허준이라는 의사의 예약 가능 시간대이다. 그렇기에 요청시 /doctors/허준이라는 엔드포인트를 사용한 것을 볼 수 있다. 그 뿐만이 아니라, 특정 시간에 예약하게 되면, 실제 slot이라는 리소스의 123이라는 id를 가진 리소스가 변경되기 때문에, 하단의 특정 시간에 예약이라는 요청에서는 /slots/123으로 실제 변경되는 리소스를 사용해야한다.

예시와 같이, 어떤 리소스를 변화시키는지 혹은 어떤 응답이 제공되는지에 따라 각기 다른 엔드포인트를 사용하기 때문에, 적절한 엔드포인트를 작성하는 것이 중요하다.
엔드 포인트 작성 시에는 동사, HTTP 메소드 혹은 어떤 행위에 대한 단어 사용은 지양하고 리소스에 집중해 명사 형태의 단어로 작성하는 것이 바람직한 방법이다.

또한 요청에 따른 응답으로 리소스를 전달할 때에도 사용한 리소스에 대한 정보와 함께 리소스 사용에 대한 성공/실패 여부를 반환해야한다. 예를 들어 만약 김코딩 환자가 허준 의사에게 9시에 예약을 진행하였으나, 해당 시간이 마감되어 예약이 불가능하다고 가정할 때, 아래와 같이 리소스 사용에 대한 실패 여부를 포함한 응답을 받아야 한다.

REST 성숙도 모델 - 2단계 (HTTP 메소드 원칙 준수)

REST 성숙도 모델 2단계에서는 CRUD에 맞게 적절한 HTTP를 사용해야한다. 앞서 0단계와 1단계 예시는 POST로 하고 있었지만 2단계에 따르면 CRUD에 따른 적합한 메소드를 사용한 것은 아니다.

먼저 예약 가능한 시간을 확인한다는 것은 예약 가능한 시간을 조회(READ)하는 행위를 의미하고, 특정 시간에 예약을 한다는 것을 특정 시간에 예약알 생성(CRATE)한다는 것과 같다. 그렇기에 조회(READ)하기 위해서는 GET메소드를 사용하여 요청을 보내고, 이 때 GET메소드는 body를 가지지 않기 때문에 query parameter를 사용하여 필요한 리소스를 전달해야한다.

또한 예약을 생성(CREATE)하기 위해서는 POST메소드를 사용하여 요청을 보내는 것이 바람직하다. 그리고 2단계에서는 POST요청에 대한 응답이 어떻게 반환되는지도 중요하다.

이 경우 응답은 새롭게 생성된 리소스를 보내주기 때문에, 응답 코드도 201 Created로 명확하게 작성해야하며, 관련 리소스를 클라이언트가 Location 헤더에 작성된 URI를 통해 확인할 수 있도록 해야 완벽하게 REST 성숙도 모델 2단계를 충족한 것이라고 볼 수 있다!

메소드 사용 규칙

  • GET: 서버의 데이터를 변화시키지 않는 요청에 사용
  • POST: 요청마다 새로운 리소스를 생성하고 PUT은 요청마다 같은 리소스를 반환
    매 요청마다 같은 리소스를 반환하는 특징 => 멱등
    그렇기에 멱등성을 가지는 매소드 PUTPOST를 구분하여 사용해야함
  • PUT: 교체
  • PATCH: 수정의 용도
    PUTPATCH구분하여 사용해야함!
    MDN HTTP request methods

REST 성숙도 모델 - 3단계(HATEOAS 원칙 준수)

마지막 단계는 HATEOAS(Hypertext As The Engine Of Application State)라는 약어로 표현되는 하이퍼미디어 컨트롤을 적용한다. 3단계 요청은 2단계와 동일하지만 응답에는 리소스의 URI를 포함한 링크 요소를 삽입하여 작성한다는 것이 다르다.

이 때 응답에 들어가게 되는 링크 요소는 응답을 받은 다음에 할 수 있는 다양한 액션들을 위해 많은 하이퍼미디어 컨트롤을 포함하고 있다.

예를 들어 위와 같이 허준이라는 의사의 예약 가능 시간을 확인한 후에는 그 시간대에 예약을 할 수 있는 링크를 삽입하거나, 특정 시간에 예약을 완료하고 나서는 그 예약을 다시 확인할 수 있도록 링크를 작성해 넣을 수도 있다. 이렇게 응답 내에 새로운 링크를 넣어 새로운 기능에 접근할 수 있도록 하는 것이 3단계의 중요 포인트이다.

만약 클라이언트 개발자들이 응답에 담겨 있는 링크를 눈여겨본다면, 이러한링크들은 좀 더 쉽고 효율적으로 리소스와 기능에 접근할 수 있게 하는 트리거가 될 수 있다.

GraphQL

GraphQL이란?

GraphQL은 Graph + Query Language의 줄임말로 Query Language 중에서도 Server API 를 통해 정보를 주고받기 위해 사용하는 Query Language를 뜻한다. 쉽게 이야기하면 API를 위한 쿼리언어라고 말한다.

  • Query Language 는 정보를 얻기 위해 보내는 질의문(Query)을 만들기 위해 사용되는 Computer 언어의 일종.

왜 Graph인가?


그래프는 여러 개의 점들이 서로 복잡하게 연결되어 있는 관계를 표현한 자료구조를 뜻한다. 하나의 점을 그래프에서는 Node 또는 정점(vertex)라고 표현하고, 하나의 선은 간선이라고 한다. 직접적인 관계가 있는 경우 두 점 사이를 이어주는 선이 있으며 간접적인 관계라면 몇 개의 점과 선에 걸쳐 이어진다. 또한 각 노드간의 간선을 통해 특정한 순서에 따라 그래프를 재귀적으로 탐색할 수 있다. 이는 마인드 맵과 유사하다!



GraphQL에서는 모든 데이터가 그래프 형태로 연결되어 있다고 전제한다. 일대일로 연결된 관계도, 여러 계층으로 이루어진 관계도 모두 그래프이다. 단지 그 그래프를 누구의 입장에서 정렬하느냐(클라이언트가 어떤 데이터를 필요로 하느냐)에 따라 트리구조를 이룰 수 있다.

이를 통해 GraphQL은 클라이언트 요청에 따라 유연하게 트리 구조의 JSON 데이터를 응답으로 전송할 수 있다. 다시 말해 GraphQL은 REST API 방식의 고정된 자원이 아닌 클라이언트 요청에 따라 유연하게 자원을 가져올 수 있다는 점에서 엄청난 이점을 갖는다.

GraphQL Keywords

서버로부터 데이터를 조회(Read)하는 경우, REST API에선 GET 요청이 있었다면 GraphQL에서는 Query를 이용해 원하는 데이터를 요청할 수 있다. 또한 Create, Delete와 같이 저장된 데이터를 수정하는 경우에는 Mutation을 이용해 이를 수행할 수 있다.

더 나아가 GraphQL에서는 구독(Subscription)이라는 개념을 제공하며 이를 이용해 실시간 업데이트를 구현할 수 있다.

Subscription는 전통적인 Client(요청)-Server(응답) 모델을 따르는 Query 또는 Mutation과 달리, 발행/구독(pub/sub) 모델을 따른다. 클라이언트가 어떤 이벤트를 구독하면, 클라이언트는 서버와 WebSocket을 기반으로 지속적인 연결을 형성하고 유지하게 된다. 그 후 특정 이벤트가 발생하면, 서버는 대응하는 데이터를 클라이언트에 푸시해준다.

  • Query: 저장된 데이터 가져오기(REST의 GET과 유사)
  • Mutation: 저장된 데이터 수정하기
    • Create: 새로운 데이터 생성
    • Update: 기존의 데이터 수정
    • Delete: 기존의 데이터 삭제
  • Subscription: 특정 이벤트가 발생 시 서버가 대응하는 데이터를 실시간으로 클라이언트에게 전송

REST API vs GraphQL

REST API의 한계

REST API라는 방법론이 있음에도 왜 GraphQL이 탄생했을까? 예제를 통해 REST API 한계에 대해 알아보고 GraphQL 방식과 비교해보자!


가상의 블로그 앱을 구현하기 위해서는 다음과 같은 데이터가 필요하다.

  • 사용자의 이름
  • 사용자의 포스팅 목록
  • 사용자의 팔로워 목록

REST API로 Blog 앱을 구현할 때

  • Overfetch: 필요 없는 데이터까지 제공함
    -> 블로그 앱 예제처럼 유저의 이름만 필요한 상황에서 REST API를 사용한다면, 응답 데이터에는 유저의 주소, 생일 등과 같이 실제로는 클라이언트에게 필요없는 정보가 포함되어 있을 수도 있다.

  • Underfetch: endpoint 가 필요한 정보를 충분히 제공하지 못함
    -> Underfetch의 경우 클라이언트는 필요한 정보를 모두 확보하기 위하여 추가적인 요청을 보내야만한다. 로그 앱 예제 화면을 구현하기 위해선 유저 정보 뿐만 아니라 유저의 포스팅 목록 및 유저가 보유한 팔로워까지 필요하다. 이 때 필요한 정보를 모두 가져오려면 REST API에서는 각각의 자원에 따라 엔드포인트를 구분하기 때문에 3가지 엔드포인트에 요청을 보내야한다.
  • 클라이언트 구조 변경 시 엔드포인트 변경 또는 데이터 수정이 필요함
    -> REST API에서는 자원의 크기와 형태를 서버에서 결정하기 때문에 클라이언트가 직접 데이터의 형태를 결정할 수 없다. 이로 인해 만약 클라이언트에서 필요한 데이터의 내용이 변할 경우 다른 endpoint를 통해 변경된 데이터를 가져오거나 수정을 해야한다.

GraphQL로 Blog 앱을 구현할 때


  • 하나의 endpoint 요청
    -> /graphql이라는 하나의 endpoint로 요청을 받고 그 요청에 따라 query,mutation을 resolver 함수로 전달해서 요청에 응답한다.
    모든 클라이언트 요청은 POST 메소드를 사용한다.
  • No! under & overfetching
    -> 여러 개의 endpoint 요청을 할 필요없이 하나의 endpoint에서 쿼리를 이용해 원하는 데이터를 정확하게 API에 요청하고 응답으로 받을 수 있다.
  • 강력한 playground
    -> graphql 서버를 실행하면 Playground라는 GUI를 이용해 resolverschema 를 한 눈에 보고 테스트 해 볼 수 있다.(POSTMAN과 유사)
  • 클라이언트 구조 변경에도 지장이 없음
    -> 클라이언트 구조가 바뀌어도 받을 필요한 데이터를 결정하는 주체가 클라이언트이기 때문에 서버에 지장이 없다. 클라이언트에서는 무슨 데이터가 필요한 지에 대해서만 요구사항을 쿼리로 작성하면된다.

GraphQL의 장점

  • 사용하는 측에서 원하는 정보를 가져올 수 있음
  • 편하게 정보를 수정할 수 있도록 표준화된 Query language 를 만들게 됨

GraphQL의 단점

  • 캐싱이 REST보다 훨씬 복잡
    -> HTTP에선 각 메소드에 따라 캐싱이 구현되어 있다. 하지만 GraphQL에선 POST 메소드만을 이용해 요청을 보내기 떄문에 각 메소드에 따른 캐싱을 지원받을 수 없다. 그래서 이를 보안하기 위해 Apollo 엔진의 캐싱과 영속 쿼리 등이 등장하게 되었다.
  • 고정된 요청과 응답만 필요할 경우에는 Query 로 인해 요청의 크기가 RESTful API 의 경우보다 더 커진다.

GraphQL Hands-on

GraphQL Server

GraphQL을 이용해 서버에 요청을 보내기 위해선 GraphQL 서버를 먼저 구축해야한다.

express-graphql

GraphQL 서버를 구축할 수 있는 라이브러리는 다양하지만 다음 예제는 express-graphql 라이브러리를 사용하였다.
해당 미들웨어는 이미 express에서 REST API를 사용하여 서버 구축이 되었어도 쉽게 GraphQL을 도입할 수 있다는 장점이 있다.
GraphQL 서버는 Schema(typeDefs)와 Resolver를 이용해 생성할 수 있다.
index.js

const fs = require("fs");
const path = require("path");
const express = require("express");
const graphqlHTTP = require("express-graphql");
const { makeExecutableSchema } = require("graphql-tools");

const schemaFile = path.join(__dirname, "schema.graphql");
const typeDefs = fs.readFileSync(schemaFile, "utf8");
const resolvers = require("./graphql/resolvers.js")
const schema = makeExecutableSchema({ typeDefs, resolvers });
//typeDefs와 resolvers를 설정해야 합니다.

app.use(
  '/graphql',
  graphqlHTTP({
    schema: schema,
    graphiql: true,
  }),
);
//서버를 만들 때 환경설정이 필요합니다.
 
app.listen(4000);
console.log("Running a GraphQL API server at localhost:4000/graphql");

graphql/schema.graphql

  • Schema : GraphQL 데이터 타입을 지정하고 query 와 mutation 등 요청에 따라 구분
    -> Type Schema
type Post {
  title: String!
  author: User!
}
        
type User {
  name: String!
  email: String!
  pw: Int!
  age: Int
  posts: [Post!]! 
  // 위와 같이 타입 간에 관계를 표현하는 것도 가능합니다.
  // User에서 posts 필드는 실제로 게시글들로 구성된 배열입니다. 
}
        
  // 오브젝트 타입: User
  // 필드: name, age, posts 등
  // 스칼라 타입: string, Int 등
  // 느낌표(!): 필수 값을 의미 (non-nullable)
  // 대괄호([]): 배열을 의미(array)

  // Type Query, Mutation, Subscription**
        
type Query {
  getUser(email: String!, pw: Int!): User!
}
        
type Mutation {
  createUser(email: String!, pw: Int!, name: String!): User!
}
        
type Subscription {
  newUser: User!
}

graphql/resolver.js

  • Resolver : 요청에 대한 응답을 결정해주는 함수로써 GraphQL의 여러 가지 타입 중 Query, Mutation, Subscription과 같은 타입의 실제 일하는 방식
    즉 로직을 작성한다. 다시 말하면 위와 같이 스키마를 정의하면 그 스키마 필드에 사용되는 함수의 실제 행동을 Resolver에서 정의한다. 또한 이러한 함수들이 모여 있기 때문에 보통 Resolvers라 부른다.
    -> GraphQL에서는 데이터를 가져오는 구제적인 과정을 직접 구현해야 하는데 이와 같은 작업(e.g. 데이터베이스 쿼리, 원격 API 요청)을 Resolver가 담당하게된다.
const db = require("./../db")
const resolvers = {
  Query: { // Query : 저장된 데이터 가져오기 (REST 에 GET 과 비슷합니다.)
		getUser: async (_, { email, pw }) => {
			db.findOne({
				where: { email, pw }
			}) ... // 실제 디비에서 데이터를 가져오는 로직을 작성합니다. 
			...
		}
  },
  Mutation: { // Mutation : 저장된 데이터 수정하기 ( Create , Update , Delete )
		createUser: async (_, { email, pw, name }) => {
			...
		}
  }
  Subscription: { // Subscription : 실시간 업데이트
    newUser: async () => {
      ...
		}
  }
};

GraphQL Client

앞서 GraphQL 서버를 구성하는 방법에 대해 알아보았다. 이제는 클라이언트에서 Apollo Client 라이브러리를 이용해 GraphQL 서버로 요청을 보내는 방법에 대해 알아보자!
사실, GraphQL API를 호출할 때, 반드시 Apollo Client와 같은 전용 클라이언트가 필요한 것은 아니다. raphQL API를 별다른 라이브러리 없이 fetch함수를 이용하거나 터미널로 호출할 수도 있다

하지만 Apollo Client는 다음과 같은 장점이 있다.

Apollo Client

  • GraphQL 을 기반으로 한 상태관리 플랫폼이며 클라이언트에서 GraphQL을 사용하여 데이터를 가져오는 UI를 만들 때 사용하기 좋음
  • API 서버에서 데이터를 가져오기 위해 번거로운 네트워크단의 HTTP 요청을 신경쓰지 않아도 됨((fetch 또는 axios를 사용할 필요가 없음)
  • 자동으로 데이터를 캐싱하므로 redux의 reducer처럼 복잡하게 데이터를 관리할 필요가 없어짐
  • 크롬 브라우저에서 Apollo Client Developer Tools 익스텐션을 설치하면, 개발 환경에서 캐시 상태와 정보를 즉시 확인이 가능할 수 있으므로 보다 편리한 개발환경을 제공해줌

시작해보기!

1. 터미널에서 다음 명령어를 입력하여 클라이언트에 필요한 라이브러리를 설치

  • @apollo/client: 이 패키지에는 메모리 내 캐시, 로컬 상태 관리, 에러 핸들링 및 React 기반 View 레이어와 같은 Apollo Client를 설정하는 데 필요한 대부분이 포함되어있음
  • graphql : 이 패키지는 GraphQL 쿼리를 분석하기 위한 로직을 제공

    cli

npm install @apollo/client graphql

2. Apollo Client의 모듈을 이용하여 Client 객체를 생성

index.js

import React from 'react';
import { render } from 'react-dom';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  gql
} from "@apollo/client"; 

const client = new ApolloClient({
  uri: 'https://my-app.com/graphql', // GraphQL 서버의 URL을 지정합니다.
  cache: new InMemoryCache() // Apollo Client가 쿼리 결과를 캐시하기 위해 인스턴스를 생성합니다. 
});

3. 생성한 클라이언트를 ApolloProvider를 이용하여 React와 연결

이는 react-redux의 Provider와 비슷한 역할을 한다고 볼 수 있다!
index.js

import React from 'react';
import { render } from 'react-dom';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  gql
} from "@apollo/client"; 

const client = new ApolloClient({
  uri: 'https://my-app.com/graphql', // GraphQL 서버의 URL을 지정합니다.
  cache: new InMemoryCache() // Apollo Client가 쿼리 결과를 캐시하기 위해 인스턴스를 생성합니다. 
});

render(
  <ApolloProvider client={client}> // APollo 클라이언트와 React를 연결합니다. 
    <App />
  </ApolloProvider>,
  document.getElementById('root'),
);

4. 동일한 index.js내에서 템플릿 리터럴을 사용하여 실행할 쿼리를 정의

index.js

const GET_USER_INFO = gql`
  query getUser($email: String!, $pw:Int!) {
    getUser(email: $email, pw: $pw) {
			name
			age
		}
  }
`;

5. Apollo Client에서 제공하는 useQuery Hook을 이용하여 UserInfo 컴포넌트 내에서 GraphQL 데이터를 fetch함

UserInfo 컴포넌트 또한 index.js내에서 useQuery를 사용하여 정의해보면 다음과 같다.

useQuery는 기본적으로 gql 인자를 전달받아 실행하며 쿼리에 필요한 변수와 같은 여러 옵션들을 추가 인자로 전달할 수 있다. 그리고 UI를 렌더링하는 데 사용할 수 있는 loading, error, data를 반환한다. 이를 이용하면 useEffect와 isLoading과 같은 훅과 플래그를 사용할 필요없이 훨씬 간결하게 Fetching 로직을 작성할 수 있다.
index.js

function UserInfo({ email, pw }) {
  const { loading, error, data } = useQuery(GET_USER_INFO, {
    variables: { email, pw }, // 쿼리에 필요한 변수를 두번째 인자로 넘겨줍니다. 
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;
  // 로딩 시, 에러 발생 시, 성공 시에 따라 UI를 분기할 수 있습니다. 
 
  return (
    <div>
      <p>
        name: {data.getUser.name}
      </p>
			<p>
        age: {data.getUser.age}
      </p>
    </div>
  );
}

전체 코드

index.js

import React from 'react';
import { render } from 'react-dom';
import {
  ApolloClient,
  InMemoryCache,
  ApolloProvider,
  useQuery,
  gql
} from "@apollo/client"; 

const client = new ApolloClient({
  uri: 'https://my-app.com/graphql', // GraphQL 서버의 URL을 지정합니다.
  cache: new InMemoryCache() // Apollo Client가 쿼리 결과를 캐시하기 위해 인스턴스를 생성합니다. 
});

const GET_USER_INFO = gql`
  query getUser($email: String!, $pw:Int!) {
    getUser(email: $email, pw: $pw) {
			name
			age
		}
  }
`;

function UserInfo({ email, pw }) {
  const { loading, error, data } = useQuery(GET_USER_INFO, {
    variables: { email, pw }, // 쿼리에 필요한 변수를 두번째 인자로 넘겨줍니다. 
  });

  if (loading) return null;
  if (error) return `Error! ${error}`;
  // 로딩 시, 에러 발생 시, 성공 시에 따라 UI를 분기할 수 있습니다. 
 
  return (
    <div>
      <p>
        name: {data.getUser.name}
      </p>
			<p>
        age: {data.getUser.age}
      </p>
    </div>
  );
}

function App() {
// 유저 인증과 관련된 로직이 생략되어 있습니다. 
  return (
    <div>
      <h2>My first Apollo app 🚀</h2>
      <UserInfo />
    </div>
  );
}

render(
  <ApolloProvider client={client}> // APollo 클라이언트와 React를 연결합니다. 
    <App />
  </ApolloProvider>,
  document.getElementById('root'),
);

좋은 웹페이지 즐겨찾기