본문 바로가기
TIL

[자바스크립트] 비동기 처리(Ajax, Pjax)와 Promise, Async/Await문법과 axios와 fetch 개념들 핥아먹기

by 잼민타치 2024. 2. 11.

다음은 프론트엔드를 공부하며 궁금했던 사항들을 찾아보고 간략하게 정리한 것입니다.

틀린 부분들이 존재할 수도 있으며 피드백해주신다면 감사하겠습니다!


 

 

 

 

 

 

코드를 동기적으로 처리한다는 것은 위에서부터 아래로 모든 코드가 순서대로 처리되는 것을 의미한다.

비동기적으로 처리한다는 것은 특정 코드의 연산이 끝날 때 까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행함을 의미한다.

 

 

자바스크립트는 이렇게 비동기적으로 처리하는 특성을 가진다.

왜냐하면 동기적으로 처리했을 때는 코드 파악이 쉬워지고 유지보수나 디버깅이 쉬워지는 장점이 있다.

하지만 싱글 스레드 방식의 자바스크립트에서는 런타임 시 발생하는 지연시간이 큰 문제가 된다.

그래서 자바스크립트는 비동기적으로 코드를 처리하게 되며 이를 위한 다양한 통신 기법이 존재한다.

 

 

 

 

 

1. Ajax

서버와 브라우저가 비동기 방식으로 XML 데이터를 교환할 수 있는 통신 기능으로 데이터를 이동하고 화면을 구성하는데 있어서 전체 페이지를 새로 고치지 않고도 페이지의 일부만을 위한 데이터를 로드하는 기법이다.

 

어랍쇼? 이거 SPA의 장점 아니었나요?

=> 맞아용. SPA 개발에서 핵심적인 역할을 하는 것이 JavaScript의 비동기 처리 능력과 AJAX, PJAX 와 같은 기법입니다. 이러한 기법으로 만든 것이 SPA이기 때문에 SPA가 자바스크립트의 통신 기법의 장점을 가지게 된 것입니다.

 

ajax lifecycle, 출처 : https://poiemaweb.com/js-spa

 

 

 

 

예를 들어보자.

우리의 어플리케이션에 존재하는 네비게이션에서

사용자가 행한 클릭이라는 이벤트가 캐치되면

서버를 향해 href 어트리뷰트에 path를 사용하여 ajax 요청을 하게 된다.

그러면 서버는 그에 해당하는 Response를 Json 형태로 주게 된다.

 

이렇게 되면 불필요한 리소스의 중복 요청을 방지할 수 있고, 페이지 전체를 리렌더링 할 필요 없이 갱신이 필요한 일부만 갱신하면 된다.

덕분에 빠른 퍼포먼스와 부드러운 화면 표시를 기대할 수 있어 향상된 사용자 경험을 구현할 수 있다.

 

그러나 이러한 기법에도 단점이 존재하게 되는데,

 

 

 

 

1. ajax 요청은 주소창의 url을 변경시키지 않아 Histroy 관리가 되지 않는다.

예를 들어 웹 어플리케이션의 필수 구성요소라고 할 수 있는 뒤로 가기나, 앞으로 가기가 구현되지 않는 것이다.

또한 주소창의 url이 그대로이기 때문에 새로고침을 하게 되면 루트 컴포넌트를 보여주게 된다.

 

2. 또한 동일한 하나의 URL로 동작하기에 SEO 이슈에서도 자유로울 수 없다.

 

 

 

이러한 단점을 보완하기 위해 등장한 방식이 바로 pjax다.

 

 


 

 

 

 

 

 

 

2. pjax (pushState + ajax)

pushState 메서드는 HTML5 History API의 일부로 웹 애플리케이션에서 클라이언트 측의 브라우징 히스토리를 조작할 수 있게 해준다.

 

예를 들어 위에서의 ajax 예시와 마찬가지로 네비게이션에서 클릭 이벤트가 캐치되면

href 어트리뷰트에 Path를 사용하여 ajax 요청을 하게 된다.

여기서 pjax는!!!

동시에  pushState 메서드를 사용해서 주소창의 URL을 history entry로 추가하지만 서버로 HTTP 요청을 보내지는 않는다.

 

이렇게 됨으로써 페이지의 콘텐츠 변경시 URL도 함께 변경되기 때문에 검색 엔진이 각각의 페이지를 별개의 콘텐츠로 인식하고 크롤링할 수 있게 된다. 이는 검색 엔진이 동적 콘텐츠를 더 잘 이해하고 인덱싱할 수 있게 하여 SEO를 개선한다.

단, pjax 방식은 브라우저 주소창의 url이 변경되기 때문에 요청이 서버로 전달된다.

즉 pjax 방식은 서버 사이드 렌더링 방식과 ajax 방식이 혼재되어 있는 방식이므로 서버의 지원이 필요하다.


 

 

 

 

 

 

 

 

이러한 pjax의 개념을 사용하여 현대적이고 통합된 접근 방식을 제공하는 것이 ReactNext.js이다.

 

React

리액트 자체는 UI 라이브러리이기 때문에 네트워크 요청을 직접처리하지는 않는다. 대신 Fetch API나 axios와 같은 HTTP 클라이언트 라이브러리를 사용하여 서버로부터 데이터를 비동기적으로 불러오고 이를 컴포넌트의 상태로 관리하여 UI를 업데이트한다.

 

또한 리액트 앱 내에서 URL을 변경하고 라우팅을 관리하기 위해 보통 react-router-dom과 같은 라우팅 라이브러리를 사용한다. 이 라이브러리는 HTML5 History API를 사용하여 프로그래밍 방식으로 브라우저의 주소창 URL을 변경하고 SPA내에서의 페이지 전환을 처리한다.

 

Next.js

getServerSideProps와 getStaticProps 같은 데이터 페칭 함수를 통해 서버사이드 또는 빌드 타임에 데이터를 불러올 수 있으며 클라이언트 사이드에서는 Fetch api를 사용하여 비동기적으로 데이터를 요청한다.

 

또한 Next.js에서는 내장 라우팅 시스템을 가지고 있으며 파일 기반 라우팅을 제공한다. 이 과정에서 HTML5 History API가 사용되며 페이지 전환시 URL이 업데이트 되지만 전체 페이지 리로드는 발생하지 않는다.

(더욱 자세한 Next.js에 대한 설명이 궁금하다면 여기를 참고할 것)


 

 

 

 

 

 

 

이렇게 자바스크립트는 비동기 처리를 통해 다양한 프로덕트를 만들 수 있게 한다.

다만 비동기 처리의 특성 때문에 의도적으로 예외처리를 해줘야 하는 상황이 발생한다.

 

예를 들어 비동기적으로 처리한 데이터를 받아오기도 전에  화면에 표시하려고 하면 오류가 발생하기 때문이다.

이런 문제를 해결하기 위해 콜백 함수를 사용하게 되었다.

 

콜백함수란 명시적으로 호출하는 함수의 개념이 아닌, 어떤 이벤트가 발생했거나 특정 시점에 도달했을 때 시스템에서 호출하는 함수를 말한다. 즉, 문법적 특징을 가진 것이 아닌 호출방식에 의한 구분을 말한다.

 

콜백함수가 실행됐다는 것으로 요청한 작업이 끝났음을 알리고, 작업의 결과물은 콜백함수를 통해 사용가능하게 된다.

 

 

 

 

 

 

 

콜백함수의 단점

이렇게 비동기 작업과 콜백함수를 여러번 중첩하다보니 코드의 들여쓰기가 깊어지고 가독성이 매우 나빠지게 된다.

이러한 현상을 콜백 지옥이라고 부르며 코드를 이해하고 유지보수하는데 큰 어려움을 겪게 된다.

 

콜백 지옥하면 떠오르는 대표적인 그림

 

 

 

 

 

 

 

또한 콜백 패턴에서 에러 처리는 각 콜백 함수마다 에러를 체크하고 처리하는 코드를 반복적으로 작성해야 하므로 중복을 초래하며 중첩된 콜백에서 에러를 적절히 전파하고 처리하는 것을 복잡하게 한다. 더불어 중첩된 콜백은 특정 작업의 결과를 다음 작업에 전달하는 것도 어렵게 만드므로 순차적 실행이 어려워진다.

 

더불어 콜백 패턴에서는 비동기 작업의 제어권이 호출하는 측에서 비동기 작업을 수행하는 라이브러리나 프레임워크로 넘어가게 된다. 이로 인해 예기치 않은 동작이 발생하거나 라이브러리가 콜백을 예상치 못한 방식으로 호출하는 경우(제어의 역전, Inversion of Control) 문제가 발생할 수 있다.

 

이러한 단점을 해결하기 위해 Promise가 라이브러리로 생겨났으며 ES6에서는 언어적 차원에서 지원하게 되었다.


 

 

 

 

 

 

 

 

Promise

약속이라는 뜻을 가진 이름에서도 알 수 있다시피

아직 어떤 값을 받지는 못했지만 그 값을 받기로 약속을 했고 그 값이 올 때까지 약속한 것을 기다리는 행위로 보면 된다.

 

Promise는 자바스크립트에서 비동기 작업을 표현하는 객체로 비동기 작업이 성공적으로 완료될 수도, 실패할 수도 있는데 Promise는 이 두가지 경우를 각각 resolve와 reject로 처리한다. 

 

 

 

promise의 기본 구조는 다음과 같다.

let promise = new Promise(function(resolve, reject) {
  // 비동기 작업을 수행

  if (/* 작업 성공 */) {
    resolve(result);
  } else {
    reject(error);
  }
});

resolve(result) : 작업이 성공적으로 완료되었을 때 호출, value는 성공 결과를 나타낸다.

reject(result) : 작업이 실패했을 때 호출, error는 실패의 원인을 나타낸다.

 

 

 

Promise 객체는 .then(), .catch(), .finally() 메소드를 제공한다.

  • .then(onFulfilled, onRejected): Promise가 성공(resolve)하거나 실패(reject)했을 때 호출될 함수들을 등록합니다. onFulfilled는 성공 시, onRejected는 실패 시 호출됩니다.
  • .catch(onRejected): Promise가 실패했을 때 호출될 함수를 등록합니다. .then(null, onRejected)와 동일합니다.
  • .finally(onFinally): Promise의 성공 여부와 관계없이 마지막에 항상 호출될 함수를 등록합니다.

 

 

 

 

 

 

Async/Await

이러한 Promise를 조금 더 쉽고 직관적으로 사용할 수 있게 해주기 위해

ES2017에서부터 도입된 Async/Await 문법이 등장하게 된다.

이를 통해 비동기 코드를 동기 코드처럼 읽고 쓸 수 있게 된다.

 

 

Async 함수에서는 async 키워드를 함수 앞에 붙여 사용함으로써 함수가 promise 객체를 반환하게 만든다.

함수 내부에서 Promise를 반환하는 비동기 작업 앞에 await 키워드를 사용하면 자바스크립트 엔진은 해당 작업이 완료될 때까지 기다렸다가 다음코드를 실행한다.

 

 

Await은 async 함수 내에서만 사용할 수 있다. await 키워드를 사용하면 Promise의 실행 결과를 기다린 후 해당 결과를 변수에 할당할 수 있다. try...catch 문과 함께 사용하여 비동기 작업중 발생한 에러를 쉽게 처리할 수 있다.

 

 

사용 예시

async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data');
    const data = await response.json();
    console.log(data);
  } catch (error) {
    console.error('Error fetching data:', error);
  }
}

 

이를 통해 복잡한 비동기 로직의 가독성과 유지보수성을 크게 향상 시킬 수 있게 된다.


 

 

 

 

 

 

 

Axios와 Fetch

둘은 모두 자바스크립트에서 HTTP 요청을 위해 사용한다는 공통점이 존재한다. 더불어 둘 다 비동기 통신을 위해 promise를 사용한다.

 

Axios는 비동기로 HTTP 요청을 보내기 위한 자바스크립트 라이브러리로, Promise 기반의 API를 제공한다.

브라우저와 Node.js에서 모두 사용할 수 있으며 요청 취소, 응답 시간 초과 설정, 자동 JSON 변환과 같은 기능을 제공한다.

 

기본 사용법

axios.get('https://api.example.com/data')
  .then(response => {
    console.log(response.data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

 

 

Fetch는 ES6부터 자바스크립트에 내장된 라이브러리로 Axios 처럼 네트워크 요청을 보내고 응답을 받을 수 있도록 해주는 API다.

기본적으로 브라우저 환경에서 사용되며 디폴트로 GET 요청을 보내지만

옵션을 통해 POST, PUT 등의 다른 HTTP 메소드를 사용할 수 있다.

 

 

기본 사용법

fetch('https://api.example.com/data')
  .then(response => {
    if (!response.ok) {
        throw new Error('Network response was not ok');
    }
    return response.json();
  })
  .then(data => {
    console.log(data);
  })
  .catch(error => {
    console.error('Error:', error);
  });

 

 

 

 

 

 

 

Axios와 Fetch 는 다음과 같은 차이점을 가진다.

 

 

Axios

1. 요청을 보낼 때와 응답을 받을 때 JSON 형태로 자동 변환하는 기능이 내장되어 있다.

2. 요청 취소, HTTP 요청 타임아웃 설정, 인터셉터를 통한 요청 및 응답 가공 등 추가적인 기능을 제공한다.

3. 브라우저와 Node.js 환경 모두에서 사용할 수 있다.

Fetch 

1. 별도의 설치 없이 사용할 수 있다.

2. JSON 변환 등의 처리를 수동으로 해야 한다. 예를 들어, 응답 데이터를 JSON으로 파싱하기 위해서는 .json() 메소드를 추가로 호출해야 한다.

3. 요청 취소는 AbortController를 사용하여 구현할 수 있지만, Axios에 비해 사용이 더 복잡할 수 있다.

 

 

에러 처리 부분

Axios

네트워크 에러 발생 시나 서버가 2xx가 아닌 상태 코드를 반환했을 때, Promise가 reject된다. 이는 에러 처리가 상대적으로 간단하다는 것을 의미한다.

Fetch

네트워크 에러 발생 시에만 Promise가 reject된다. 서버가 에러 상태 코드(예: 404, 500)를 반환해도 Promise는 resolve되며, 별도의 에러 처리 로직을 구현해야 한다.

 

 

간략하게 말하면 Axios는 별도의 라이브러리를 설치해야한다는 점 이외에는 쓰기 번거로운 이유가 없다. 간단하게 사용할 때는 Fetch를 쓰고, 이외의 확장성을 염두해둔다면 Axios를 쓰는 것이 좋다.

 

 

 

 

 

참고한 문서: https://velog.io/@eassy/자바스크립트의-비동기처리

https://poiemaweb.com/js-spa