REST API & GraphQL

devAnderson·2022년 3월 17일
0

TIL

목록 보기
74/103

🕹 0. 다시 쓰게되는 이유

이전에 이미 REST API에 대해서 공부하여 정리했었고, 사용을 많이 해봤으므로 완전히 안다고 생각했으나 면접질문에 답변을 할 때에 정확하게 무엇인지 설명을 못하는 내 자신을 바라보며 재정리를 하자는 마음으로 쓰는 글이다.

그리고 나중에 블로그를 볼 때에 여러번 노출되면 더 오래 기억할 수 있으리라는 마음으로도 쓰게 되었다.

🕹 1. REST API란?

REST API는 Representational State Transfer API의 약자로, 의미에서도 볼 수 있듯 네트워크를 통한 서버 데이터 상태의 소통을 최대한 표현적으로 정의하는 아키텍쳐를 의미한다. 이때 서버 리소스의 위치를 URI로 표현하고, 행위요청에 대한 정의를 메서드로 나타냄으로서 정확하게 필요한 요청을 서버에 요청할 수 있다.

개발자마자 REST API를 설정하는 방식이 다 다르지만, 리차드슨의 성숙도 모델에 따르면 다음과 같은 과정을 통해 더욱 정교한 REST API를 작성할 수 있다고 말한다.

  • 0단계 : http 프로토콜을 사용하는 단계
    즉, 그냥 서버와의 요청에서 프로토콜이 http이기만 하면 REST API의 0단계를 충족한다고 말할 수 있다.

  • 1단계 : 적절한 endpoint를 설정하는 단계
    REST API에서 리소스의 위치를 URI를 통해 설정한다고 하였다. 이때 모든 데이터를 하나의 endpoint로 처리해버리는 것이 아니라, 적절한 동적 라우트를 통해 자원의 요청을 분리해서 전달하고 이것에 따라 서버는 적절하게 분류하여 자원을 응답해주는 방식을 해야한다는 것이다.

endpoint에서는 관습적으로 "/moveDate" 와 같은 동사형태나 "/get" 과 같은 메서드를 사용하지 말고 리소스의 특징에 대한 명사 자체를 사용해야 한다.

  • 2단계 : CRUD에 맞는 메서드를 설정하는 단계
    REST API에서는 서버에 대해 리소스의 처리 방식을 메서드를 통하여 나타낸다고 하였다. 이때 메서드는 아무렇게나 정하는 것이 아닌, 요청의 목적에 맞는 메서드가 설정이 되었을 때에 리소스를 반환하도록 정의해야 한다.

예를들어, 어떤 환자의 데이터를 가져와야 한다면 GET 메서드나 POST 메서드 둘 다 사용을 하는 API를 볼 수 있었을 것이다.
하지만 이때 데이터를 가져오는 행위는 CRUD에서 "READ" 에 해당하므로 get 메서드를 사용해야 한다.
이때 get을 위해서 특정 데이터가 함께 보내져야 한다면 get 메서드에는 body가 존재하지 않으므로 적절한 parameter나 헤더, 쿠키 등을 통해 함께 동봉하여 전달을 해야 한다.

또한 메서드의 설정과 더불어 정확한 응답코드와 response message를 설정하는 것도 이 단계에 해당한다고 할 수 있다.

  • 3단계 : 응답으로 리소스의 위치와 응답 메서드 등을 함께 설정하는 단계
    사실 이 단계는 엄밀하게 말하면 선택사항으로 준수할 필요는 없다. 왜냐하면 이미 response의 내부 구조에 어떤 메서드인지, 어떤 오리진에서 응답을 하는지 등을 이미 포함해서 전달하고 있기 때문에 이것을 response의 body에 함께 첨부하는 것은 어찌보면 과한 디테일이라고 말할 수 있다.

🕹 2. GraphQL이란?

일반적으로 REST API의 작성은 서버 개발자가 전담하는 것처럼 여겨졌다. 하지만, REST API로 네트워크 통신을 하다 보면 2가지의 문제점을 직면할 수밖에 없다

  1. overfetching : 예를들어 필요한 데이터가 "title" 하나만 존재한다 할지라도, 서버에서 해당 endpoint에 대해 작성하지 않았을 경우 title 정보 하나를 얻기 위해 전체 데이터를 다 가져와야 한다. 이런 상황을 과도한 fetching이라고한다.
  2. underfetching : 예를들어 필요한 데이터가 title과 body이지만, title의 Endpoint는 존재하나 body는 다른 Endpointf로만 가져와질 경우, title을 위한 호출은 부족한 데이터를 가져오는 상황이라고 할 수 있다. 이것을 부족한 fetching이라고 한다.

graphQL은 REST API의 상단 두가지의 문제를 손쉽게 해결할 수 있다.
왜냐하면 데이터의 종류를 정해서 요청하는 주체가 바로 클라이언트이기 때문이다.

graphQL에서 정의하는 데이터의 관리 방식은 크게 3가지로 나눌 수 있다

  1. Query : GET 과 같은 역할을 한다
  2. Mutation : CUD 의 역할을 한다 (create, update, delete). 즉, 서버의 자원을 추가하거나 변동시키는 역할을 한다.
  3. Subscription : 특정 이벤트가 발생한다면 서버에 요청 없이 알아서 이를 서버가 감지하고 자동으로 클라이언트한테 데이터를 전송한다.

하지만 이렇게 좋게 보이는 graphQL에는 문제점이 하나가 존재하는데 그것은 바로 캐싱이 어렵다는 점이다

HTTP 요청에서는 각 메소드별로 자동 캐싱을 지원받을 수 있다. 하지만, graphQL의 post만으로는 모든 캐싱을 관리하기 어렵기 때문에 Apollo Client와 같은 자동 캐싱 라이브러리를 사용해서 클라이언트에서 요청을 하면 된다.

그렇다면 간단하게 사용하는 방식을 확인해보도록 하면

  1. graphql 서버를 구축한다

    이때 기존에 있던 express에 업그레이드 형식으로 지원이 가능한 express-graphql을 사용하면 손쉽게 연동을 시킬 수 있다.

const fs = require("fs");
const path = require("path");
const express = require("express");
const graphqlHTTP = require("express-graphql"); // 연동 라이브러리를 설치한다.
const { makeExecutableSchema } = require("graphql-tools"); // graphql의 문법을 실행시키는 스키마를 작성해 서버와 연동

// schema.graphql 파일 내부에는 타입스크립트와 같은 요청의 타입정의가 존재한다.
const schemaFile = path.join(__dirname, "schema.graphql"); 
const typeDefs = fs.readFileSync(schemaFile, "utf8");

// resolver에는 실제 요청에 대한 응답과 관련한 내용이 존재한다 (controller와 비슷함)
const resolvers = require("./graphql/resolvers.js")

// schema와 resolver을 합쳐서 연동이 가능한 스키마를 제작 후, 이것을 express app 에서 사용하게 한다.
const schema = makeExecutableSchema({ typeDefs, resolvers });
app.use(
  '/graphql', // endpoint가 하나만 존재하는 것이 graphql의 특징이다.
  graphqlHTTP({
    schema: schema,
    graphiql: true,
  }),
); 
app.listen(4000, ()=> console.log("running"));
  1. schema를 작성한다.
    schema.graphql의 안에는 아래와 같이 타입정의가 존재한다. 이때 특이한점은 모델에 대한 타입과 query, mutation, subscription 타입정의가 함께 존재한다는 점이다.
type Post {
  title: String!
  author: User!
} //model 의 객체정의
        
type User {
  name: String!
  email: String!
  pw: Int!
  age: Int
  posts: [Post!]!  // 만약 모델안에 또다른 모델이 참조되고 있을 경우, 이와같이 정의 가능하다.
}
  // 느낌표(!): 필수 값을 의미 (non-nullable)
  // 대괄호([]): 배열을 의미(array)


// 아래와 같이 query요청, mutation 요청, subscription 요청에 대해 어떤 형태로 응답이 갈지 타입정의가 되어 있다.
type Query {
  getUser(email: String!, pw: Int!): User! //getUser
}
        
type Mutation {
  createUser(email: String!, pw: Int!, name: String!): User!
}
        
type Subscription {
  newUser: User!
}
  1. resolover을 작성한다
    resolver에는 실제 query, mutation, subscription 에 해당하는 요청에 대한 서버 응답을 기술하는 장소이다. 데이터베이스를 조회 후 값을 리턴해준다.
const db = require("./../db")
const resolvers = {
  Query: { // Query : 저장된 데이터 가져오기 (REST 에 GET 과 비슷합니다.)
		getUser: async (_, { email, pw }) => {
			const data = await db.findOne({
				where: { email, pw }
			}) 
                        return data;
		}
  },
  Mutation: { // Mutation : 저장된 데이터 수정하기 ( Create , Update , Delete )
		createUser: async (_, { email, pw, name }) => {
			...

		}
  }
  Subscription: { // Subscription : 실시간 업데이트
    newUser: async () => {
      ...
		}
  }
};

위에까지가 서버의 설정이었다면 아래부터는 클라이언트의 설정이다.
Apollo Client를 사용하면 데이터 요청을 자동으로 캐싱해주고 Apollo Client Developer Tools를 통해 해당 캐싱을 확인하기 편하다. React-query처럼 자동으로 fetching 에 대해 캐싱하는 기능이라고 이해해도 좋을 것 같다.

//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가 쿼리 결과를 해당 인스턴스에 캐싱함.
});

export const GET_USER_INFO = gql`
  query getUser($email: String!, $pw:Int!) {
    getUser(email: $email, pw: $pw) {
			name
			age
		}
  }
`; // redux action처럼 해당 내용을 통해 서버에 전달하면 마치 서버는 액션을 받아 리듀서를 호출하는 것처럼 resolver을 사용해 함수호출 후 결과 도출을 한다.

// React-query처럼 provider을 통해 해당 객체 인스턴스를 앱의 자식들에게 전달하는 기능을 한다.
render(
  <ApolloProvider client={client}> // Apollo 클라이언트와 React를 연결합니다. 
    <App />
  </ApolloProvider>,
  document.getElementById('root'),
);
// 특정 컴포넌트 내
import {useQuery} from "@apollo/client"
import {GET_USER_INFO} from "./index"

function UserInfo({ email, pw }) {
  // React-Query의 useQuery와 하는 행동이 모오오옵시 동일하다.
  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>
  );
}
profile
자라나라 프론트엔드 개발새싹!

0개의 댓글