웹 애플리케이션의 꽃 - 비동기(asynchronous) 쉽게 이해하기

서론

여태껏 당연하게 사용해 왔던 '비동기' 혹은 '비동기 통신'. 문득 스스로 '비동기가 뭐야?'라고 질문했다. 명쾌히 대답이 나오지 않았다. 부끄러움은 뒤로 한 채, 그 의미를 다시 한번 알아보고 여태껏 사용해 왔던 비동기 방식에 대해 학습해 보기로 한다.

 

 

비동기(Asynchronous) vs 동기(Synchronous)

비동기(asynchronous)는 컴퓨터 프로그래밍에서 특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 실행하는 방식이다. 즉, 작업의 완료와 상관없이 프로그램의 흐름이 계속 진행되는 것을 뜻한다. 이는 동기(synchronous) 방식과 대비되는 개념이다.

 

동기(Synchronous) :

  • 동기 방식에서는 한 작업이 완료될 때까지 기다렸다가 그 다음 작업을 실행한다.
  • 즉, 작업이 순차적으로 이루어 진다는 것이다.
  • 예를 들어서, 데이터베이스에서 데이터를 조회한 후 결과를 응답받을 때까지 다음 코드로 넘어가지 않는 것이 동기 방식이다.

비동기 (Asynchronous) :

  • 비동기 방식에서는 한 작업을 시작하고 완료되기를 기다리지 않고 바로 다음 코드를 실행한다.
  • 이러한 비동기 작업은 종종 콜백(call back) 함수, 프로미스 (Promise), async/await 등을 사용하여 구현한다.
  • 예를 들어서, 웹 API로부터 데이터를 가져오는 동안 사용자 인터페이스가 멈추지 않고 다른 작업을 계속할 수 있는 것이 비동기 방식의 대표적인 예이다.

 

 

비동기 방식의 중요성

비동기(Asynchronous)는 웹 어플리케이션에서 매우 중요한 개념이다. 웹 어플리케이션은 네트워크 요청, 사용자 이벤트 처리 등 다양한 작업을 동시에 처리해야 하며, 이들 작업이 서로를 차단하지 않아야 한다.

 

예를 들어서, 서버로부터 데이터를 받아오는 동안에도 사용자는 웹 페이지 내부에서 스크롤을 하고, 버튼을 클릭한다. 이때 모든 네트워크 요청이 동기적으로 처리된다면, 데이터를 받아오는 동안 페이지가 멈추게 되어서 사용자는 어떠한 액션도 취할 수 없게 되어 사용자 경험 (UX)가 크게 저하될 것이다.

 

비동기 프로그래밍은 복잡할 수 있지만, JavaScript의 'Promise' 및 'asnyc', 'await' 과 같은 기능을 사용하면 비교적 쉽게 처리할 수 있다. 이러한 기능들은 비동기 작업의 결과를 효과적으로 관리하고, 코드를 더 읽기 쉽고 이해하기 쉬운 형태로 작성할 수 있도록 도와준다.

 

 

async/await 그리고 Promise

'Promise'는 비동기 작업의 최종 완료(응답 성공 또는 에러 실패 등)와 그 결과값을 나타낸다. 간단히, 어떤 작업이 완료되기를 '약속'하고 그 작업이 끝난 후 결과를 반환한다.

let myPromise = new Promise((resolve, reject) => {
    // 비동기 작업을 수행한다.

    let condition; // 어떤 조건
    if (condition) {
        resolve('성공'); // 작업이 성공하면 resolve 호출
    } else {
        reject('실패'); // 작업이 실패하면 reject 호출
    }
});

myPromise
    .then((result) => {
        console.log(result); // '성공' 출력
    })
    .catch((error) => {
        console.log(error); // '실패' 출력
    });

 

'async'와 'await'은 'Promise'를 더 쉽게 사용할 수 있게 해주는 JavaScript의 문법이다. 'async' 키워드는 함수 앞에 사용하는데 이는 함수가 비동기적으로 작동하도록 만든다. 'await' 키워드는 'Promise'의 응답을 기다리게 한다.

async function myAsyncFunction() {
    try {
        let result = await myPromise; // Promise가 해결될 때까지 기다림
        console.log(result); // '성공' 출력
    } catch (error) {
        console.log(error); // 오류 발생 시 '실패' 출력
    }
}

myAsyncFunction();

 

 

 

Promise , async/await을 각각 사용하여 데이터 가져오기

예를 들어서, 웹 API에서 데이터를 가져오는 비동기 작업을 'fetch' API를 사용해서 수행한다고 가정한다.

 

Promise 사용 예제 : 비동기 작업을 대표하는 객체로, 해당 작업이 완료되면 결과를 반환하거나 오류를 발생시킨다.

function fetchData() {
    return fetch('https://api.example.com/data') // 데이터를 가져온다.
        .then(response => response.json()) // 응답을 JSON으로 변환
        .then(data => console.log(data)) // 데이터 출력
        .catch(error => console.error('Error fetching data:', error)); // 오류 처리
}

fetchData();

 

async/await 사용 예제 : 'Promise'를 사용하기 더 쉬운 방법으로, 코드를 동기적으로 작성하는 것처럼 보이게 해준다.

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

fetchDataAsync();

 

'async'와 'await'은 'Promise' 기반 코드를 좀 더 읽기 쉽고 이해하기 쉬운 방식으로 작성할 수 있게 해준다. 하지만 내부적으로는 'Promise'를 기반으로 동작한다.

 

 

Fetch API란?

Fetch API는 웹 브라우저에서 HTTP 요청을 보내고 받기 위한 현대적인 JavaScript 인터페이스이다. 이 API를 사용하면 서버로부터 리소스를 가져오거나, 서버에 새로운 리소스를 생성할 수 있다. Fetch API는 기존에 많이 사용했던 XMLHttpRequest(XHR) 보다 간결하고 유연한 대안으로 널리 사용되고 있다.

 

Fetch API의 주요 특징으로는, 비동기적으로 동작하며 'Promise' 객체를 반환한다. 이를 통해 비동기 요청의 결과를 쉽게 처리할 수 있다. 그리고 Fetch API를 사용하여 다양한 HTTP 요청(GET,POST,PUT,DELETE 등)을 보낼 수 있으며, JSON, TEXT, Blob 등 다양한 형식의 응답을 처리할 수 있다는 점이다. 또한 사용자 정의 헤더를 추가하거나, 쿼리 매개변수를 조작할 수 있고 CORS(Croos-Origin Resource Sharing) 관련 설정을 다룰 수 있다.

 

GET 사용 예시 :

fetch('https://api.example.com/data')
    .then(response => response.json()) // 응답을 JSON 형태로 파싱
    .then(data => console.log(data)) // 데이터 처리
    .catch(error => console.error('Error:', error)); // 오류 처리

 

POST 사용 예시 :

fetch('https://api.example.com/data', {
    method: 'POST', // HTTP 메서드 지정
    headers: {
        'Content-Type': 'application/json'
    },
    body: JSON.stringify({ key: 'value' }) // 보낼 데이터
})
.then(response => response.json())
.then(data => console.log(data))
.catch(error => console.error('Error:', error));

 

XMLHttpRequest와 비교한다면, Fetch API는 XMLHttpRequest(이하 XHR)보다 더 간결하고, Promise 기반이라 비동기 처리가 더 편리하다. 또한 Fetch API는 스트림을 사용하여 요청과 응답을 처리할 수 있어, 대용량 데이터를 효율적으로 다룰 수 있고, 모든 최신 브라우저에서 Fetch API를 지원하기 때문에 지원 범위가 넓다.

 

Fetch API를 사용하여 스트림 처리를 하는 예제는 아래와 같다. 스트림(Stream)은 Fetch API에서 큰 데이터를 효율적으로 처리하는데 유용하게 사용된다. 스트림을 사용하면 데이터를 조각조각 나누어서 처리할 수 있게 되는데, 메모리를 절약하고 대용량 데이터를 더 효율적으로 다룰 수 있게 된다.

fetch('https://api.example.com/large-data')
    .then(response => {
        // 응답 본문을 스트림으로 가져온다.
        const reader = response.body.getReader();

        // 읽기 과정을 처리하는 함수
        function read() {
            return reader.read().then(({ done, value }) => {
                if (done) {
                    console.log('스트림 처리 완료');
                    return;
                }

                // 'value'는 Uint8Array입니다. 이를 사용해 필요한 처리를 한다.
                // 예: 데이터 조각 처리

                // 다음 데이터 조각을 읽는다.
                return read();
            });
        }

        // 스트림 읽기 시작
        return read();
    })
    .catch(error => console.error('Error:', error));

이 예제에서 fetch()는 네트워크 요청을 보내고, 응답을 스트림으로 받아온다. 'response.body.getReader()'를 호출하여 응답 본문을 조각별로 읽을 수 있는 'ReadableStream'의 'reader'를 생성한다. 'reader.read()' 코드는 Promise를 반환하며, 이는 스트림에서 다음 데이터 조각을 읽는다.

 

'done' 값이 'true'가 되면 스트림의 끝에 도달한 것이고, 'false'라면 'value'에 데이터 조각이 포함된다. 이 데이터를 사용하여 필요한 처리를 할 수 있으며, 스트림의 끝에 도달할 때까지 반복하는 코드이다.

 

이 방식은 특히 대용량 데이터를 처리할 때 유용하다. 예를 들어서, 큰 이미지나 비디오 파일, 큰 JSON 데이터 등을 서버로부터 받아올 때 메모리 사용량을 최적화하며 데이터를 처리할 수 있게된다. 스트림을 사용하면 전체 데이터를 한 번에 메모리에 로드하는 대신 필요한 만큼 조금씩 읽어 처리할 수 있게 되어 효율성이 향상된다.

 

Fetch API는 현대 웹 개발에서 HTTP 요청을 처리하는 표준 방식으로 자리 잡았으며, 대부분의 웹 어플리케이션에서 필수적으로 사용된다.

 

 

Fetch API와 fetch() 메서드

Fetch API랑 fetch() 함수는 밀접한 연관이 있지만, 약간의 차이가 있다.

 

Fetch API는 웹 브라우저에서 서버와의 HTTP 통신을 수행하기 위한 인터페이스의 집합을 의미한다. 이 API는 네트워크 통신을 위한 여러 기능과 개념을 포함하고 있다. Fetch API는 HTTP 요청을 보내고 받는 방법, HTTP 요청과 응답을 처리하는 방법, 헤더, 본문, CORS 설정 등을 다루는 데 필요한 모든 기능을 제공한다. 즉 FetchAPI는 fetch() 함수를 포함해 HTTP 통신을 수행하는데 필요한 전체 인터페이스와 메커니즘을 의미한다.

 

fetch()함수는 Fetch API의 일부로, HTTP 요청을 간편하게 보낼 수 있는 메소드이다. 주어진 URL로 네트워크 요청을 보내고, Promise 객체를 반환한다. 이 Promise는 요청의 응답을 처리하는 데 사용된다. fetch()함수는 Fetch API가 제공하는 많은 기능 중 가장 핵심적인 기능을 담당한다.

 

결론적으로 fetch() 함수는 Fetch API의 구성 요소 중 하나로, 이들은 서로 밀접하게 연결되어 있다.