javascript

REST API(Representational State Transfer)

Bittersweet- 2022. 8. 2. 09:38
728x90

MDN의 공식문서에 따르면 REST는 효율적, 안정적이며 확장 가능한 분산 시스템을 구현하는 소프트웨어 아키텍처 설계 제약의 모음을 나타낸다고 하며, 그 제약들을 준수했을 때 그 시스템은 RESTful하다고 일컬어진다고 한다.

 

REST에 대한 설명 글은 많이 있지만, 사실 딱하니 머리속에 박히지 않았다.

 

알것도 같고 모를 것도 같고... 그러다 찾은 설명으로 REST는 HTTP URI를 통해 자원(resource)를 명시하고 HTTP Method(POST, GET.. etc)를 통해 해당 자원에 대한 CRUD operation을 적용하는 것으로 REST의 중심에 자원(resource)이 있고 HTTP Method를 통해 자원을 처리하도록 설계된 아키텍처를 의미한다고 했다. 웹에 존재하는 모든 자원(이미지, 동영상, DB 자원 등)에 고유한 URI를 부여해 활용하는 것으로 자원을 정의하고 자원에 대한 주소를 지정하는 방법론이라고도 볼 수 있다고 한다.

 

 

💡 HTTP

웹 브라우저(클라이언트)와 웹 서버간의 커뮤니케이션을 위해 디자인된 하이퍼텍스트 전송 프로토콜로 HTML과 같은 하이퍼미디어 문서를 전송하기 위한 프로토콜이다.

 

💡 API(Application Programming Interface)

API는 응용 프로그램에서 사용할 수 있도록 운영체제나 프로그래밍 언어가 제공하는 기능을 제어할 수 있게 만든 인터페이스를 말한다. 주로 파일 제어, 창 제어, 화상 처리, 문자 제어 등의 인터페이스를 제공한다. 웹 개발에서 보통 API는 개발자가 앱을 통해 사용자의 웹 브라우저에서 상호작용할 수 있도록 하는 코드 기능들(e.g. methods, propoerties, event, URLs), 사용자의 컴퓨터 상에 있는 다른 소프트웨어 및 하드웨어 또는 서드파티 웹사이트나 서비스의 집합을 말한다.

 

다시 한번 설명하자면 REST는 한마디로 클라이언트가 서버의 리소스에 접근(전송)하는 방식을 규정한 가이드(아키텍처)라고 볼 수 있다. 처음엔 좀 생소한 개념이긴 한데 REST(가이드)를 기반으로 서비스 API를 구현하는 것이 REST API라고 생각하면 된다.

REST API를 제공하는 웹 서비스를 RESTful하다고 한다. (REST를 REST 답게 쓰기 위한 방법이 누군가에 의해 공식적으로 발표된건 아니지만 REST 원리를 잘 따르는 시스템은 RESTful하다고 할 수 있고 한다)

 

REST API의 구성

1. 자원(Resource) : URI

  - 모든 자원에 고유한 식별자(ID)가 존재하고, 이 자원은 서버에 존재한다.

  - 고유한 식별자(ID)는 '/pages/:page_id'와 같은 HTTP URI다.

  - 클라이언트는 URI를 이용해서 자원을 지정하고 해당 자원의 상태(정보)에 대한 조작을 서버에 요청한다.

 

2. 행위(Verb): HTTP Method

  - HTTP 프로토콜의 메서드를 사용한다.

  - HTTP프로토콜은 GET, POST, PUT, DELETE와 같은 메서드를 제공한다.

 

3. 표현(Representations): 페이로드

  - 클라이언트가 자원의 상태(정보)에 대한 조작을 요청하면 서버는 이에 대한 적절한 응답(Representation)을 보낸다.

  - REST에서 하나의 자원은 JSON, XML, 일반 텍스트, RSS 등 여러 형태의 상태로 나타내질 수 있다.

  - JSON 혹은 XML을 통해 데이터를 주고 받는 것이 일반적이다.

 

💡 페이로드(payload)

사용에 있어서 전송되는 데이터를 뜻한다. 페이로드는 전송의 근본적인 목적이 되는 데이터의 일부분으로 그 데이터와 함께 전송되는 헤더와 메타데이터와 같은 데이터는 제외한다.

 

 

여기서 잠깐!!

여기서 엔드포인트(Endpoint)라는 개념에 대해 짚고 넘어가도록 합시다!!!

엔드포인트는 API가 서버에서 자원(resource)에 접근할 수 있도록 하는 URL이다. 또한, 메서드가 같은 URL들에 대해서도 다른 요청을 하게끔 구별하게 해준다. 

endpoint 기능
GET/todos List all todos
POST/todos Create a new todo
GET/todos/:id Get a todo
PUT/todos/:id Update a todo
DELETE/todos:id Delete a todo and its items
GET/todos/:id/:items Get a todo item
PUT/todos/:id/:items Update a todo item
DELETE/todos/:id/:items Delete a todo item

 

 

 

REST API 설계 원칙

1. URI는 리소스를 표현해야 한다.

URI는 리소스를 표현하는데 중점을 두어 리소스를 식별할 수 있는 이름으로 동사보다는 명사를 사용해야 한다. 

 

2. 리소스에 대한 행위에 HTTP 요청 메서드로 표현한다.

아래의 5가지 메서드를 이용하여 CRUD를 구현한다.

HTTP 요청 메서드 종류 목적 페이로드
GET index/retrieve 모든/특정 리소스 취득  X
POST create 리소스 생성 O
PUT replace 리소스 전체 교체 O
PATCH modify 리소스 일부 수정 O
DELETE delete 모든/특정 리소스 삭제 X

리소스에 대한 행위는 HTTP 요청 메서드를 통해 표현하며 URI에는 표현하지 않는다.

 

💡 HTTP 요청 메서드

클라이언트가 서버에게 요청의 종류와 목적(리소스에 대한 행위)를 알리는 방법이다.

 

 

 


 

 

JSON 서버를 이용한 REST API 실습

HTTP 요청을 전송하고 응답을 받으려면 서버가 필요하다. JSON 서버를 사용해 가상 REST API 서버를 구축하여 HTTP 요청을 전송하고 응답을 받는 실습을 진행해보도록 한다.

 

 

1. JSON 서버 설치

먼저 npm을 이용해 다음과 같이 명령어를 입력하여 JSON 서버를 설치한다.

mkdir json-server-exam && cd json-server-exam
npm init -y
npm install json-server --save-dev

 

2. db.json 파일 생성

생성한 폴더의 루트에 db.json 파일을 생성한다. 이때 생성한 db.json은 리소스를 제공하는 데이터베이스 역할을 한다.

{
  "todos": [
    {
      "id": 1,
      "contnet": "HTML",
      "complete": true
    },
    {
      "id": 2,
      "contnet": "CSS",
      "complete": false
    },
    {
      "id": 3,
      "contnet": "JavaScript",
      "complete": true
    }
  ]
}

 

3. JSON 서버 실행

터미널에 다음과 같이 명령어를 입력하여 JSON 서버를 실행한다. JSON 서버가 데이터베이스 역할을 하는 db.json 파일의 변경을 감지하게 하려면 watch 옵션을 추가한다. 기본 포트를 사용하기 싫다면 두번째 명령어를 사용해 포트를 변경하면 된다.

## 기본포트 3000 사용 / watch 옵션 적용
json-server --watch db.json

## 포트 변경 / watch 옵션 적용
json-server --watch db.json --port 5000

매번 이렇게 실행을 시키는 것은 너무 번거로운 일이므로 package.json파일의 script를 다음과 같이 수정하여 JSON 서버를 실행해 보자. (불필요한 항목 삭제함)

{
  "name": "json-server-exam",
  "version": "1.0.0",
  "scripts": {
    "start": "json-server --watch db.json"
  },
  "devDependencies": {
    "json-server": "^0.17.0"
  }
}

터미널에서 npm start를 입력하면 이제 JSON 서버를 실행한다.

이 화면이 나온다면 잘 따라오고 있다는 뜻이다.

 

4. GET 요청

todos 리소스에서 모든 todo를 취득(index)한다.

JSON 서버 실행을 중단한 뒤 루트 폴더에 public 폴더를 생성하고 재실행 한다. public 폴더에 get_index.html 파일을 생성하고 아래와 같이 작성한 후 http://localhost:3000/get_index.html

 

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Index</title>

</head>

<body>
  <pre></pre>
  <script>
    // XMLHttpRequest 객체 생성
    const xhr = new XMLHttpRequest();
    // HTTP 요청 초기화
    // todos 리소스에서 모든 todo를 취득(index)
    xhr.open('GET', '/todos');

    // HTTP 요청 전송
    xhr.send();

    // load 이벤트는 요청이 성공적으로 완료된 경우 발생한다.
    xhr.onload = () => {
      // status 프로퍼티 값이 200이면 정상적으로 응답된 상태
      if (xhr.status === 200) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>
</body>

</html>

todos 리소스에서 id를 사용하여 특정 todo를 취득(retrieve)한다. pubilc 폴더에 get_retrieve.html 파일을 추가하고 http://localhost:3000/get_retrieve.html로 접속한다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Retrieve</title>
</head>

<body>
  <pre></pre>
  <script>
    //XMLHttpRequest 객체 생성
    const xhr = new XMLHttpRequest();

    // HTTP 요청 초기화
    // todos 리소스에서 id를 사용하여 특정 todo를 취득(retrieve)
    xhr.open('GET', 'todos/1');

    // HTTP 요청 전송
    xhr.send();

    // load 이벤트는 요청이 성공적으로 완료된 경우 발생한
    xhr.onload = () => {
      //status 프로퍼티 값이 200이면 정상적으로 응답된 상태
      if (xhr.status === 200) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>

</body>

</html>

 

5. POST 요청

todos 리소스에 새로운 todo를 생성해보자. POST 요청 시에는 setRequestHeader 메서드를 사용하여 요청 몸체에 담아 서버로 전송할 페이로드의 MIME 타입을 지정해야 한다.

 

💡 MIME이란

웹에서 파일의 확장자는 별 의미가 없다. 각 문서와 함께 올바른 MIME 타입을 전송하도록 서버가 정확하게 설정하는 것이 중요하다. 브라우저(클라이언트)가 리소스를 내려받았을 때 해야할 기본 동작이 무엇인지를 결정하기 위해 대개 MIME타입을 사용한다.

 

public 폴더에 다음 post.html을 추가하고 http://localhost:3000/post.html로 접속한다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>POST</title>
</head>

<body>
  <pre></pre>
  <script>
    const xhr = new XMLHttpRequest();

    xhr.open('POST', '/todos');
    // 요청 몸체에 담아 서버로 전송할 페이로드의 MIME 타입 지정
    xhr.setRequestHeader('content-type', 'application/json');
    xhr.send(JSON.stringify({ id: 4, content: 'Angular', completed: true }));
    xhr.onload = () => {
      // status 프로퍼티 값이 200(OK) 또는 201(Created)이면 정상적으로 응답
      if (xhr.status === 200 || xhr.status === 201) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>
</body>

</html>

 

6. PUT 요청

PUT은 특정 리소스 전체를 교체할 때 사용한다. todos 리소스에서 id로 todo를 특정하여 id를 제외한 리소스 전체를 교체해보자. PUT 요청 시에는 setRequestHeader 메서드를 사용하여 요청 몸체에 담아 서버로 전송할 페이로드의 MIME 타입을 정해야 한다.

public 폴더에 put.html을 추가하고 해당 페이지에 접속한다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PUT</title>
</head>

<body>
  <pre></pre>
  <script>
    const xhr = new XMLHttpRequest();

    xhr.open('PUT', '/todos/4');
    xhr.setRequestHeader('content-type', 'application/json');
    xhr.send(JSON.stringify({ id: 4, content: 'React', completed: true }));
    xhr.onload = () => {
      if (xhr.status === 200) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>

</body>

</html>

 

7. PATCH 요청

PATCH는 특정 리소스의 일부를 수정할 때 사용한다. todos 리소스의 id로 todo를 특정하고 completed만 수정해보자. PATCH 요청 시에는 setRequestHeader메서드를 사용해서 요청 몸체에 담아 서버로 전송할 페이로드의 MIME 타입을 지정해준다.

public 폴더에 patch.html을 추가하고 해당 페이지에 접속한다.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>PATCH</title>
</head>

<body>
  <pre></pre>
  <script>
    const xhr = new XMLHttpRequest();

    xhr.open('PATCH', '/todos/4');
    xhr.setRequestHeader('content-type', 'application/json');
    xhr.send(JSON.stringify({ completed: false }));
    xhr.onload = () => {
      if (xhr.status === 200) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>
</body>

</html>

 

8. DELETE 요청

todos 리소스에서 id로 todo를 특정하고 삭제한다. public폴더에 delete.html을 생성하고 브라우저에서 접속해보자.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>DELETE</title>
</head>

<body>
  <pre></pre>
  <script>
    const xhr = new XMLHttpRequest();

    xhr.open('DELETE', '/todos/4');
    xhr.send();
    xhr.onload = () => {
      if (xhr.status === 200) {
        document.querySelector('pre').textContent = xhr.response;
      } else {
        console.error('Error', xhr.status, xhr.statusText);
      }
    }
  </script>

</body>

</html>