[모자딥] 43~45

Seungrok Yoon (Lethe)·2024년 1월 16일
0

스터디 

목록 보기
6/6

43장,44장 - AJAX, REST

AJAX

Ajax(Asynchronous Javascript and XML)은 자바스크립트를 사용하여 브라우저가 서버에 비동기 방식으로 데이터를 요청하고, 서버가 응답한 데이터를 수신하여 웹페이지를 동적으로 갱신하는 프로그래밍 방식이다.

이러한 프로그래밍 방식은 브라우저에서 제공하는 Web API인 XMLHttpRequest객체를 기반으로 동작한다.

JSON

JSON(Javacript Object Notation)은 클라이언트와 서버 간의 HTTP 통신을 위한 텍스트 데이터 포맷이다.

사용 시 주의점은 반드시 키 값을 큰따옴표("")로 묶어야 한다. 값은 문자열이라면 반드시 큰따옴표로 묶어야 한다.

직렬화

클라이언트가 서버로 객체를 전송할 때는 일반 객체 형식이 아니라, 객체를 문자열화해야 한다.

이를 직렬화-serializing이라 한다.

쉬운 이해를 위해 샘플 프로젝트와 함께해보자.

npx json-server db.json
http://localhost:3000/posts/1
{
  "id": "1",
  "title": "a title"
}

XMLHttpRequest

const xhr = new XMLHttpRequest()

프로토타입 프로퍼티

XMLHttpRequest 는 몇 가지 프로토타입 프로퍼티를 가지고 있다.

readyState
HTTP 요청의 현재 상태를 나타내는 정수

  • UNSENT:0
  • OPENED:0
  • HEADERS_RECEIVED:2
  • LOADING:3
  • DONE: 4

status
HTTP 요청에 대한 응답 상태(HTTP 상태 코드)를 나타내는 정수
ex) 200

statusText
HTTP 요청에 대한 응답 메시지를 나타내는 문자열
ex) "OK"

responseType
HTTP 응답 타입
ex) document, json, text, blob, arraybuffer

response
HTTP요청에 대한 응답 몸체(response body). responseType에 따라 타입이 다르다.

responseText
서버가 전송한 HTTP요청에 대한 응답 문자열

이벤트 핸들러 프로퍼티

p.823 참고

HTTP 요청을 전송하는 순서

  1. XMLHttpRequest.prototype.open 메서드로 HTTP요청을 초기화한다.
  2. 필요에 따라 HttpRequest.prototype.setRequestHeader메서드로 특정 HTTP 요청의 헤더값을 설정한다.
  3. XMLHttpRequest.prototype.send 메서드로 HTTP요청을 전송한다.

그렇다면 이대로 한 번 XMLHttpRequest객체를 생성하여 요청을 전송해보도록 하겠다.

실습 코드는 다음과 같다.

liver-server extension을 통해서 실행해주자.

<!DOCTYPE html>
<html lang="en">
  <link rel="stylesheet" href="index.css" />
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="root">
      <div id="title">DOM, AJAX Practice</div>
      <button id="fetch_btn" type="button">Post!</button>
      <div id="res_txt"></div>
    </div>
    <script type="module" src="ajax_test.js"></script>
  </body>
</html>
//index.css
#root {
  width: 100vw;
  height: 100vh;
  display: flex;
  gap: 20px;
  flex-direction: column;
  justify-content: center;
  align-items: center;
}
#title {
  text-align: center;
  width: 500px;
  height: 20px;
}

#fetch_btn {
  text-align: center;
  padding: 20px;
  background-color: blueviolet;
  border-radius: 5px;
  cursor: pointer;
}

#res_txt {
  text-align: center;
  font-size: 20px;
  background-color: yellow;
}
const fetchBtnEl = document.getElementById('fetch_btn')
const resTextEl = document.getElementById('res_txt')

const BASE_URL = 'http://localhost:3000'

const get = async (path) => {
  const res = await fetch(BASE_URL + path)
  const body = await res.json()
  const createdNode = document.createElement('span')
  resTextEl.appendChild(createdNode)
}

const postXML = () => {
  const xhr = new XMLHttpRequest()
  /**
   * method - HTTP 요청 메서드(get, post, put, delete)
   * url - HTTP요청을 전송할 URL
   * async - 비동기 요청 여부, 옵션으로 기본값 true
   */
  xhr.open('POST', BASE_URL + '/posts')
  //클라이언트가 서버로 전송할 데이터의 MIME 타입 지정: json
  xhr.setRequestHeader('content-type', 'application/json')
  xhr.send(JSON.stringify({ id: '3', title: 'created new title' }))
  //   xhr.onreadystatechange = () => {
  //     //XMLHttpRequest.DONE = 4
  //     if (xhr.readyState !== XMLHttpRequest.DONE) return
  //     if (xhr.status >= 200 || xhr.status < 300) {
  //       const createdNode = document.createElement('span')
  //       resTextEl.appendChild(createdNode)
  //     } else {
  //       console.log('Error', xhr.status, xhr.statusText)
  //     }
  //   }
  //onload이벤트는 HTTP요청이 성공한 경우에만 발생한다.
  xhr.onload = () => {
    if (xhr.status >= 200 && xhr.status < 300) {
      const res = JSON.parse(xhr.response)
      const span = document.createElement('span')
      console.log(res.title)
      const textNode = document.createTextNode(res.title)
      span.appendChild(textNode)
      resTextEl.appendChild(span)
    } else {
      console.log('Error', xhr.status, xhr.statusText)
    }
  }
}

fetchBtnEl.addEventListener('click', (e) => {
  postXML()
  console.log('click')
})

GET 요청 메서드의 경우 데이터를 URL의 일부분인 쿼리 문자열 QUERY STRING로 서버에 전송한다.

POST 요청 메서드의 경우 데이터(페이로드 payload)를 요청 몸체 request body 에 담아 전송한다.

JSON 서버도 설정해주자.
별도의 디렉토리에
json-server를 설치하고, dp.json파일에 다음과 같은 내용을 추가해준다.


{
  "posts": [
    {
      "id": "1",
      "title": "a title"
    },
    {
      "id": "2",
      "title": "another title"
    }
  ],
  "comments": [
    {
      "id": "1",
      "text": "a comment about post 1",
      "postId": "1"
    },
    {
      "id": "2",
      "text": "another comment about post 1",
      "postId": "1"
    }
  ],
  "profile": {
    "name": "typicode"
  }
}

npx json-server db.json

REST(Representational State Transfer)

위 실습에서 사용한 json-server는 REST API를 빠르게 구축해주는 라이브러리이다. 그런데, REST API란 무엇인가?

REST는 HTTP를 기반으로 클라이언트가 서버의 리소스에 접근하는 방식을 규정한 아키텍처이다. 그리고 REST API는 REST를 기반으로 서비스 API를 구현한 것을 의미한다. 그리고 RESTful하다는 표현은, REST의 기본 원칙을 성실히 지킨 서비스 디자인을 일컫는다.

REST API의 3요소

REST API는

  • 자원(resource) - 자원 - URI(엔드포인트)
  • 행위(verb) - 자원에 대한 행위 - HTTP요청 메서드
  • 표현(representation) - 자원에 대한 행위의 구체적 내용 - 페이로드
    으로 이루어져있다.

REST에서 가장 중요한 원칙은 두 가지이다. URI는 리소스를 표현하는데 집중하고, 행위에 대한 정보는 HTTP요청 메서드를 통하는 것이다.

URI는 리소스를 표현하는데 중점을 두어야 한다. 그래서 동사보다는 명사를 사용하는 것이 권장된다.

그래서 아래의 표현은 리소스를 표현하는데 집중하지 못한 좋지 않은 URI이다.

GET /getTodos/1 
GET /todos/show/1
=> GET /todos/1

리소스에 대한 행위는 HTTP 요청 메서드로 표현한다. 그래서 이는 URI에 포함하지 않는다.

GET /todos/delete/1 => DELETE /todos/1

45장 - 프로미스

ES6부터 추가된 비동기처리를 위한 패턴인 프로미스는 기존의 비동기 처리를 위한 콜백 패턴의 단점을 보완하고, 비동기 처리 시점을 명확하게 표현할 수 있다는 장점이 있다.

프로미스는 콜백 헬을 보완한다

아래 코드의 postXML함수는 POST요청을 한 후에, 응답 값을 HTML조작에 사용하는 로직을 가지고 있다.

const postXML = () => {
  const xhr = new XMLHttpRequest()
  xhr.open('POST', BASE_URL + '/posts')
  //클라이언트가 서버로 전송할 데이터의 MIME 타입 지정: json
  xhr.setRequestHeader('content-type', 'application/json')
  xhr.send(JSON.stringify({ id: '3', title: 'created new title' }))
  xhr.onload = () => {
    if (xhr.status >= 200 && xhr.status < 300) {
      const res = JSON.parse(xhr.response)
      const span = document.createElement('span')
      console.log(res.title)
      const textNode = document.createTextNode(res.title)
      span.appendChild(textNode)
      resTextEl.appendChild(span)
    } else {
      console.log('Error', xhr.status, xhr.statusText)
    }
  }
}

하지만 이 함수는 조금 불편하다.

  • 함수가 비동기 처리 결과인 서버의 응답 결과를 반환하지 않는다
  • 그래서 비동기 로직 안에, 화면(HTML DOM 요소)를 조작하는 로직이 섞여 있어 커플링이 높다.

그렇다고 post함수에서 서버 응답 결과인 res를 리턴하도록 해도 제대로 값이 나오지 않는다. 그 이유는 post함수가 비동기 함수이기 때문이다.

onload이벤트 핸들러가 비동기로 동작하기 때문이다. 그래서 서버의 응답을 반환하는 return문을 작성해도, 함수가 실행된 직후에는 undefined를 반환하게 된다.

이 과정을 조금 더 자세히 살펴보자.

post함수가 호출되면, 함수 코드를 평가하는 과정에서 post함수의 실행 컨텍스트가 생성이 되고, 실행 컨텍스트 스택에 푸시된다.

그리고 이벤트 핸들러가 xhr.onload프로퍼티에 바인딩된다.

서버로부터 응답이 도착하면 xhr객체에서 load이벤트가 발생한다. 하지만, 이 이벤트 핸들러는 load이벤트가 발생하면 일단 태스크 큐에 저장되어 대기하다가, 콜 스택이 비면 이벤트 루프에 의해 콜 스택으로 푸시되어 실행이 된다.

우리가 여기서 얻을 수 있는 교훈은 다음과 같다.

비동기 함수는 비동기 처리 결과를 외부에 반환할 수 없고, 상위 스코프의 변수에 할당할 수도 없다. 따라서, 비동기 함수의 처리 결과는, 그리고 후속 처리는 비동기 함수 내부에서 수행해야 한다.

그래서 보통은 비동기 함수 외부에서, 내부로 후속처리 함수를 콜백함수로 전달해주게된다.

그러나, 후속처리 함수가 또 비동기 처리 결과를 가지고 비동기 함수를 호출해야 한다면?

=> 콜백 헬 발생

프로미스는 에러처리를 용이하게 한다

콜백 패턴은 에러 처리를 곤란하게 만든다.

아래 코드의 결과를 예측해보자.

try{
  	setTimeout(()=>{throw new Error('Error  from setTimeout')},1000)

}catch(e){
console.error('캐치한 에러',e)}

에러는 호출자 방향으로 전파된다. 그러면 setTimeout의 에러는 실행컨텍스트의 어디에 전파가 될까?

Promise

프로미스는 표준 빌트인 객체이다.

두 가지 콜백함수를 인수로 전달받는다. resolve, reject함수이다.
전자는 비동기 처리 성공시, 후자는 비동기 처리 실패시 호출이 된다.


const promisePostXML = (url) =>
  new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest()
    xhr.open('POST', BASE_URL + url)
    xhr.setRequestHeader('content-type', 'application/json')
    xhr.send(JSON.stringify({ id: '3', title: 'created new title' }))
    xhr.onload = () => {
      if (xhr.status >= 200 && xhr.status < 300) {
        const res = JSON.parse(xhr.response)
        // const textNode = document.createTextNode(res.title)
        // span.appendChild(textNode)
        // resTextEl.appendChild(span)
      } else {
        reject(new Error(xhr.status))
      }
    }
  })

Promise는 현재 비동기 처리가 어떻게 진행되고 있는지를 나타내는 상태정보를 갖는다.

  • pending: 비동기 처리가 아직 수행되지 않은 상태-프로미스 생성직후
  • fulfilled: 성공적으로 비동기 처리가 수행된 상태-resolve함수 호출이 되면, fulfilled가 됨
  • rejected: 비동기 처리가 실패된 상태 -reject함수 호출이 되면, fulfilled가 됨

fulfilled, rejected = settled 상태라 부른다.

.then, .catch, .finally

then은 두 개의 콜백함수를 받을 수 있다.

첫 번쨰 콜백함수는 앞선 프로미스에서 resolve가 호출되었을 때 호출된다.
두 번째 콜백함수는 앞선 프로미스에서 rejected가 호출되었을 때 호출된다.

두 번째 콜백함수는 프로미스가 rejected인 경우에만 호출된다는 점에서 .catch와 비슷하다.

하지만, then의 두 번쨰 콜백함수는 해당 첫 번째 콜백함수문에서 발생하는 에러를 처리할 수 없어서 .catch를 사용하는 것을 권장한다.

퀴즈

프로미스는 체이닝이 가능하다. 실습 JSON 서버에서 posts/1에 대한 comments를 얻는 과정을 프로미스 체이닝으로 구현해보자.

fetch

fetch함수는 XMLHttpRequest객체와 마찬가지로 HTTP요청 전송 기능을 제공하는 Web API이다.

인자로는 url + 객체(HTTP 요청 메서드, HTTP 요청 헤더, 페이로드...)

그러면 이제 앞선 promisePost를 fetch를 사용해서 리팩토링해보자fetch 함수 또한 Promise를 반환한다.

profile
안녕하세요 개발자 윤승록입니다. 내 성장을 가시적으로 기록하기 위해 블로그를 운영중입니다.

0개의 댓글