비동기 통신

Yeji·2023년 8월 31일
0

1. HTTP (Hypertext Transfer Protocol)

웹에서의 클라이언트 - 서버 통신은 기본적으로 HTTP 프로토콜 위에서 이루어진다.

[Hypertext]
웹 브라우저 상에서 쓰이는 HyperLink 이외에도 문서, 이미지와 같은 리소스를 말한다.

[Hyperlink]
웹 페이지나 문서에서 다른 웹페에지나 문서로 이동할 수 있는 링크

1-1. 정의

웹에서 데이터를 주고받기 위한 규약이다.

1-2. 특징

  • 요청과 응답

    클라이언트가 서버에 요청을 보내고, 서버는 그에 대한 응답을 반환하는 형식으로 통신이 이루어진다.
    
    각 메세지는 헤더와 바디로 구성되며,
    
    헤더에는 요청 혹은 응답에 대한 정보가 포함되고 바디에는 실제 데이터가 들어간다.
  • 헤더

    인증, 캐싱, 쿠키 등 다양한 기능을 지원한다.
  • 무상태 Stateless

    서버는 클라이언트의 상태를 기억하지 못하며, 원하는 데이터가 있다면 그 때마다 다시 요청을 보내야 한다.
  • 비연결성 Connectionless

    클라이언트와 서버는 한 번의 연결로 요청과 응답을 진행하고, 응답을 반환할시 바로 연결을 해제한다.
  • 상태 코드

    서버는 응답으로 상태코드를 반환해 클라이언트 요청에 대한 결과를 알려준다.
    
    200은 성공, 401은 unAuthorized, 403은 Forbidden, 404는 Not Found, 300번대는 Redirect를 의미한다.
  • 메소드

    GET, POST, PUT, DELETE 등의 메소드를 통해 클라이언트가 서버에게 원하는 동작을 알릴 수 있다.
  • URL

    URL을 사용해 원하는 리소스의 위치를 지정한다.
    
    프로토콜, 도메인, 포트 번호로 이루어져 있다.

2. AJAX (Asynchronous JavaScript And XML)

기본적으로 HTTP 프로토콜은 요청-응답이 끝나고 나면 연결이 끊기는 비연결성이 특징이다.
따라서 화면의 내용을 업데이트하기 위해 다시 요청-응답 과정을 거쳐 전체 페이지를 새로 그리게 된다.

그러나 업데이트가 필요 없는 부분도 다시 로드되어 엄청난 자원과 시간을 낭비하게 되는데, 이를 해결하기 위해 AJAX를 사용한다.

2-1. 정의

브라우저 API에서 제공하는 XMLHttpReqeust 객체를 이용해 비동기적으로 요청을 날리고 응답을 처리하는 기술이다.

이를 통해 웹페이지를 다시 로드하지 않고 동적으로 데이터를 업데이트 할 수 있다.

2-2. XML

HTML과 같은 마크업 언어 중 하나로 태그를 이용해 데이터를 나타낸다.

통신 과정에서 XML을 사용하면 불필요한 태그로 인해 파일 사이즈가 커질뿐만 아니라, 가독성도 좋지 않아 최근 웹통신에는 JSON을 이용한다.

2-3. JSON (JavaScript Object Notation)

자바스크립트 객체 형식 key:value에서 영감을 받아 만들어진 데이터 타입이다.

  • 데이터를 주고받을 수 있는 가장 간단한 형식이다.
  • 문자열 기반으로 가볍다.
  • 가독성이 좋다.
  • key:value로 이루어져있다.
  • 프로그래밍 언어나 플랫폼에 상관 없이 사용될 수 있다. (C, C#, JAVA, Python, Kotlin etc)

2-4. 장단점

장점

1. UX 개선

2. 서버 부하 감소 : 필요한 데이터만 부분적으로 재요청

단점

1. 검색 엔진 최적화 어려움 : 동적으로 생성되는 내용을 크롤링할 수 없음

2. 코드 복잡성 : 콜백 함수 중첩 등에 주의해야 함

3. Callback 함수

3-1. 정의

다른 함수의 인자로 전달되는 함수를 말한다.

비동기 통신의 결과를 이용해 특정 작업을 수행해주기 위해서는 전통적으로 콜백 함수를 사용했다. 결과를 얻은 이후 실행할 함수를 계속해서 인자로 넘겨주는 동작 방식이다.

3-2. 콜백 지옥

그런데 반환된 결과를 가지고 처리해야할 작업이 늘어난다면, 여기에 에러 처리까지 해줘야 한다면 말도 안되게 코드가 복잡해지며 분명 가독성도 떨어질 것이다.

이를 해결하기 위해 ES6에서 Promise가 도입되었다.

4. Promise 객체

4-1. 정의

비동기를 간편하게 처리할 수 있도록 ES6이후 JavaScript에서 제공하는 객체다.

작업 수행이 성공적으로 끝나면 성공 메세지와 응답 데이터를 반환하고, 오류가 발생했다면 에러를 반환한다.

4-2. 특징

Promise 객체가 담고 있는 정보는 다음과 같다.

1. 작업의 성공 실패 결과
2. 성공 실패의 결과 값(데이터)

4-2-1. Promise 생성

Promise 클래스로 Promise 객체를 생성한다.
executor라는 함수를 콜백함수로 전달하고, executor는 다시 resolve와 reject 두 가지 콜백함수를 인자로 받는다.

// Promise 객체의 executor는 선언 되자마자 바로 실행된다.
const promise = new Promise(function(resolve, reject) {
  console.log('바로 실행 !')
})

// Arrow function으로 표현했을 경우
const promise = new Promise((resolve, reject) => {
  console.log('바로 실행 !')
})

4-2-2. 상태

Promise의 상태는 세 가지가 있다.

pending 작업중

fulfilled 작업 성공 & 완료

reject 작업 실패

4-2-3. resolve & reject

  • resolve는 작업이 정상적으로 수행되었을 경우 호출되어 최종 데이터를 전달하는 함수다.
const promise = new Promise((resolve, reject) => {
  // network, 파일 읽기 등 오래 걸리는 작업 수행
  console.log('바로 실행 !')
  setTimeout(() => {
    resolve('SUCCESS')  // SUCCESS라는 값을 전달
  }, 2000)
})
  • then 은 promise가 정상적으로 잘 수행되어 최종적으로 resolve 함수를 통해 전달한 res의 값('SUCCESS')에 접근할 수 있다.
promise
  .then((res) => {
  console.log(res)	// SUCCESS
})
  • reject 작업 중 문제가 발생했을 경우 호출하게 될 함수다. Error 라는 객체를 통해서 값을 전달한다.
const promise = new Promise((resolve, reject) => {
  // network, 파일 읽기 등 오래 걸리는 작업 수행
  console.log('바로 실행 !')
  setTimeout(() => {
    reject(new Error('No network connection'))  // SUCCESS라는 값을 전달
  }, 2000)
})
  • catch 함수를 통해 reject 함수가 보내는 에러에 대한 정보를 얻을 수 있다.
promise
  .then((res) => {
    console.log(res)
  })
// then은 똑같은 Promise 객체를 반환하기 때문에 
// chaining을 통해 catch로 접근할 수 있다.
  .catch((error) => console.error(error))
  • finally 는 작업의 성공 실패와 관계 없이 무조건 호출되는 함수다.
promise
  .then((res) => {
    console.log(res)
  })
  .catch((error) => console.error(error))
  .finally(() => console.log('finally'))

4-4. fetch API

브라우저에서는 비동기 통신을 처리하기 위해 fetch() 내장 함수를 제공한다.

브라우저에서 전역적으로 동작하기 때문에 그냥 호출해서 사용하면 된다.

브라우저 DOM Tree의 최상위 객체인 window를 통해 window.fetch()로도 사용이 가능하다.

// 첫 번째 인자로는 url, 두 번째 인자로는 options 객체를 받는다.
// 요청에 대한 응답으로 Promise 객체를 반환한다.
fetch(url, options)
  .then((res) => console.log(res))
  .catch((err) => console.log(err));

4-4-1. json()

response 객체의 JSON 형식의 응답 데이터를 자바스크립트 객체로 변환해 얻을 수 있다.

우리가 흔히 사용하는 axios 라이브러리는 이 작업을 자동으로 진행한다.

fetch(url, options)
  .then((res) => res.json())
  .then((data) => console.log(data))
  .catch((err) => console.log(err))

5. async & await

5-1. async

비동기 처리에서 Promise 체이닝이 너무 길어진다 싶을 때, 혹은 코드 구조상 적절한 곳에 사용할 수 있다.

함수 앞에 async 키워드만 붙여주면 알아서 Promise 객체를 반환한다.

// 다음의 두 함수는 똑같이 동작한다.
function fetchYeji(){
  return new Promise((resolve, reject) => {
    resolve('Yeji')
  })
}

async function fetchYeji() {
  return 'Yeji'
}

fetchYeji(res).then(console.log(res)) // Yeji
// 아무런 인자도 없으면 then에서 받아오는 값을 바로 전달 받는다.
fetchYeji().then(console.log)	// Yeji

5-2. await

function delay(ms) {
  // ms 후에 resolve 함수 호출
  return new Promise((resolve) => setTimeout(resolve, ms))
}

async function getApple(){
  await delay(1000)	// 1초 후에 resolve 호출, 사과 반환
  return '🍎'
}

async function getBanana(){
  await delay(1000)
  return '🍌'
}

사과와 바나나를 따서 결과를 출력해보자.
사과를 땄다면 => 바나나를 따러 간다 => 바나나를 땄다면 => 사과와 바나나를 반환한다

만약 이렇게 계속해서 다른 과일을 차례로 따야 한다면 콜백 지옥과 똑같이 복잡한 구조가 될 것이다.

function pickFruits() {
  return getApple().then((apple) => {
    return getBanana().then((banana) => `${apple} + ${banana}`)
  })
}

pickFruits().then(console.log) // 🍎 + 🍌

이를 async와 await을 통해 다음과 같이 간단하게 만들 수 있다.

async function pickFruits() {
  const apple = await getApple()
  const banana = await getBanana()
  return `${apple} + ${banana}`
}

pickFruits().then(console.log) // 🍎 + 🍌

에러 처리를 해주고 싶다면 다음과 같이 할 수 있다.

async function pickFruits() {
  try {
    const apple = await getApple()
    const banana = await getBanana()
    return `${apple} + ${banana}`
  } catch (err) {
    console.log(err)
  }
}

그런데 getApple과 getBanana 함수는 둘 사이에 아무런 연관이 없다.

즉, 위와 같이 작성하게 되면 then 체이닝으로 인해 동기적으로 실행돼 사과와 바나나를 따는데 2초가 걸릴 것이다.

두 함수를 동시에 실행시키기 위해서 선언되자마자 executor 함수를 실행시키는 Promise의 특징을 이용할 수 있겠다.

async function pickFruits() {
  const applePromise = getApple()
  const bananPromise = getBanana()
  const apple = await applePromise
  const banana = await bananPromise
  return `${apple} + ${banana}`
}

그런데 이 방법도 뭔가 지저분하다. 이와 같은 상황에서 사용할 수 있는 것이 바로 Promise API다.

6. Promise API

6-1. all

Promise 배열을 전달하면 모든 Promise를 병렬적으로 실행시켜 결과를 반환한다.

function pickAllFruits() {
  return Promise.all([getApple(), getBanana()]).then((fruits) =>
    fruits.join(' + ')
  )
}

pickAllFruits().then(console.log)

6-2. race

만약 먼저 완료된 결과만을 반환하고 싶다면 race라는 API를 이용하면 된다.

async function getApple(){
  await delay(2000)	// 2초 후에 resolve 호출, 사과 반환
  return '🍎'
}

async function getBanana(){
  await delay(1000) // 1초 후에 resolve 호출, 바나나 반환
  return '🍌'
}

그러면 1초 후에 resolve가 호출되는 바나나가 결과로 들어가 호출될 것이다.

function pickOnlyOne() {
  return Promise.race([getApple(), getBanana()])
}

pickOnlyOne().then(console.log) // 🍌
profile
채워나가는 과정

0개의 댓글