본문 바로가기
📌 Front End/└ TypeScript

[TypeScript] 비동기 처리 방식 - Callback Function, Promise, Async, Await, Fetch

by 쫄리_ 2024. 8. 7.
728x90
반응형

🏷️ 요약

구분 동기(Synchronous) 비동기(Asynchronous)
특징 - 한 번에 하나의 작업을 수행

한 작업이 실행되는 동안 
  다른 작업은 멈춘 상태로 유지하고,
  자신의 차례를 기다림
- 어떠한 요청을 보내면 
  그 요청이 끝날 때까지 기다리는 것이 아닌,
  응답에 관계없이 바로 다음 동작이 실행

- 흐름이 멈추지 않아서 
  동시에 여러 가지 작업을 처리할 수 있음
  기다리는 과정에서 다른 함수도 호출 가능

 

⏰ 타이머 API

특정 시간마다 또는 특정 시간 이후에 콜백 함수를 호출할 수 있는 함수들을 타이머(timer) 함수라고 한다.
이 함수들을 이용해서 시간과 관련된 처리를 할 수 있게 된다.

함수 종류 특징
setInterval(함수, 시간) 시간 간격마다 함수 반복 실행
setTimeout(함수, 시간) 일정 시간 후 함수 한 번만 실행
clearInterval() 설정된 Interval 함수를 종료
clearTimeout() 설정된 Timeout 함수를 종료

 

🎯 비동기 API

코드를 쉽게 작성하는 Promise 클래스 async/await 구문

함수 종류 특징
Promise 클래스 - 비동기 작업을 더 구조화하고 관리하기 위한 방법을 제공

- 비동기 작업의 성공 또는 실패를 처리하기 위한
  성공 시 resolve() 호출 → then()
  실패 시 rejet() 호출 catch() 메서드를 제공

- 체이닝(Chaining)을 통해 여러 비동기 작업을 연결하고
  더 읽기 쉽고 관리하기 쉬운 코드를 작성
Async · Await 구문 - 응답 값을 명시적인 변수에 담아 사용하므로 
  직관적인 변수를 인식할 수 있다.

- Promise에 비해 가독성이 좋고, 문법이 간결하다.
  간결하다는 장점과 에러 핸들링에 유용하다.

- 함수에 async 키워드를 적고, 비동기 대상에 await를 추가

- 비동기 대상 함수에 await를 추가하면, 
  '이 함수의 처리를 기다려!' 라는 의미가 되기에
  await 함수 아래에 해당 함수의 응답값을 사용하는 구문을 추가해주면 된다.

동기(Synchronous)

동기 방식은 서버에 요청을 보냈을 때, 응답이 돌아와야 다음 동작을 수행할 수 있다.
즉, A 작업이 끝나야 B 작업이 실행된다.

 

비동기(Asynchronous)

비동기 방식은 서버에 요청을 보냈을 때, 응답 상태와 상관없이 다음 동작을 수행할 수 있다.
즉, A 작업이 진행되면서 B 작업이 실행되고, A 작업이 끝나면 B 작업과 상관없이 결괏값이 출력된다.


스레드

스레드란, CPU가 프로그램을 동작시키는 최소 단위이다.
운영체제에서 프로그램이 실행되고 있는 상태를 프로세스라고 하고, 

프로세스는 1개의 메인 스레드와 여래 개의 작업 스레드를 동작시킨다.
웹 브라우저나 NodeJs 자체는 다중 스레드로 동작하지만, 

자바스크립트(타입스크립트)는 단일 스레드로 동작한다.

 

단일(싱글) 스레드

단일(싱글)스레드는 프로세스 내에서 하나의 메인 스레드만으로 작업을 처리한다.
즉, 작업을 순서대로 처리하고, 만약 선행 스레드의 작업이 매우 길어진다면, 

후행 스레드는 선행 작업이 끝날 때까지 기다려야 한다.

 

다중(멀티) 스레드

멀티 스레드는 CPU의 최대 활용을 위해 2개 이상의 스레드를 동시에 실행시켜 작업을 처리한다.
문맥 교환(context switching)을 통해서 각 스레드의 작업을 조금씩 처리하고, 

사용자의 입장에서 프로그램들이 동시에 수행되는 것처럼 보인다.

 

 


⏰ 타이머 API - 함수형 비동기(setInterval, setTimeout)

타이머 함수는 일정 시간이 지난 후 특정 코드 또는 함수가 실행될 수 있도록 해주는 

함수와 일정 시간마다 함수가 실행될 수 있도록 해주는 함수를 말합니다.

※ 시간은 ms 단위이기 때문에 초 단위로 확인하려면 1,000을 곱셈해주어야 한다.

  • setInterval() / clearInterval()
  • setTimeout() / clearTimeout()

 

📌 제어권 - 실행 시점

타이머 함수는 콜백 함수의 호출 시점을 정해서 원하는 때에 호출한다.
이에 대해서 "콜백함수의 제어권 중 하나인 실행 시점을 위임받는다" 라고 표현할 수 있다.


setInterval()

setInterval(콜백 함수, 시간) 메소드는 특정 시간마다 콜백 함수를 호출한다.

return 값 : 임의의 타이머 ID

 

📝 setInterval() 형태

setInterval(함수/코드[, 지연시간, 파라미터1, 파라미터2, ...]);

 

사용예시

let count = 0;
setInterval(() => {
	console.log(`1초마다 실행됩니다(${count}번째)`);
	count++;
}, 1*1000);

여기에서 즉시 출력되는 294는 해당 setInterval()의 식별자(ID)다.

 

 

clearInterval()

clearInterval(타이머ID) 메소드는 setInterval() 메소드로 설정한 타이머를 제거한다.

return 값 : 없음

clearInterval(294);


setTimeout()

setTimeout(콜백 함수, 시간) 메소드는 특정 시간 후에 콜백 함수를 한 번 호출한다.

return 값 : 임의의 타이머 ID

 

📝 setTimeout() 형태

setTimeout(함수/코드[, 지연시간, 파라미터1, 파라미터2, ...]);

 

사용예시

setTimeout(() => {
	console.log(`1초 후에 실행됩니다`);
}, 1*1000);

여기에서 즉시 출력되는 287은 해당 setTimeout()의 식별자(ID)다.

 

 

clearTimeout()

clearTimeout(타이머ID) 메소드는 setTimeout() 메소드로 설정한 타이머를 제거한다.

return 값 : 없음

setTimeout(315);

 

314는 예정대로 3초 후 실행된 반면
314는 3초가 흐르기 이전에 clearTimeout(315) 를 해줌으로써 중단된 것을 확인할 수 있다.

 


📞 콜백지옥 (Callback)

비동기로 작동하는 코드를 제어할 수 있는 첫 번째 방법은 Callback 을 활용하는 것이다.

callback 함수를 통해 비동기 코드의 순서를 제어할 수 있지만, 
코드가 길어지고 복잡해질 수록 가독성이 낮아지는 Callback Hell이 발생하는 단점이 있다.

 

사용예시

Callback Function은 특정 조건에서 실행되는 함수로, 

특정 시간에 도달하거나 특정 이벤트가 실행됐을 때 시스템에서 호출하는 함수이다.

console.log('Hello Server');

setTimeout((): void => {
    console.log('Hello Server 2');
}, 3000); // 3초

console.log('Hello Server 3');

 

setTimeout()은 특정 시간 이후에 콜백 함수를 실행시키는 내장함수이다.
만약 위 코드가 동기처리된다면 Hello Server, 2, 3 순서대로 출력되겠지만,

위와 같이 비동기처리된다.

 

위와 같은 Callback Function을 활용한 비동기처리의 문제점은 코드가 복잡해진다는 점이다.

위와 같은 코드는 안으로 심하게 파여 흔히 '콜백 지옥(Callback hell)'이라고 표현한다.

let serverList: string[] = [];

setTimeout((name): void => {
    serverList = [...serverList, name];
    console.log(serverList);
    
    setTimeout((name): void => {
        serverList = [...serverList, name];
        console.log(serverList);
        
        setTimeout((name): void => {
            serverList = [...serverList, name];
            console.log(serverList);
        }, 500, 'name3');
    }, 500, 'name2');   
}, 500, 'name1');

 


🎯 비동기 API - 콜백형 비동기(Promise)

Callback hell을 극복하기 위해 만들어진 것 이다.

Callback Hell을 방지하면서 비동기로 작동하는 코드를 제어할 수 있는 

두 번째 방법은 Promise를 활용하는 것이다.
Pending(대기), Fulfilled(이행), Rejected(실패)의 3가지 상태가 있다.

 

📌 프로미스 생성

프로미스는 Promise 생성자 함수를 통해 인스턴스화한다. 

Promise 생성자 함수는 비동기 작업을 수행할 콜백 함수를 인자로 전달받는데 

이 콜백 함수는 resolve와 reject 함수를 인자로 전달받는다.

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});

new Promise

  • Promise는 class이기 때문에 new 키워드를 통해 Promise 객체를 생성한다.
  • Promise는 비동기 처리를 수행하는 콜백 함수를 전달받는데
    이 콜백 함수는 resolve , reject 를 인수로 전달 받는다.
const promise = new Promise(콜백 함수)
(resolve, reject) => { }

Promise 객체의 내부 프로퍼티

new Promise가 반환하는 Promise 객체는 state, result 내부 프로퍼티를 갖는다.

  • state
    기본 상태는 pendin(대기) 이다. 
    비동기 처리를 수행할 콜백 함수가 정상적으로 작동했다면, 
    fulfilled(이행) 로 변경이 되고,
    에러가 발생했다면 rejected(거부)가 된다.
  • result
    처음은 undefined 이다.
    비동기 처리를 수행할 콜백 함수가 
    성공적으로 작동하여 resolve(value)가 호출되면 value로, 
    에러가 발생하여 reject(error)가 호출되면 error로 변환다.

 

Promise는 비동기 처리가 

성공(fulfilled)하였는지 또는 실패(rejected)하였는지 등의 상태(state) 정보를 갖는다.

상태 의미 구현
pending 비동기 처리가 아직 수행되지 않은 상태 resolve 또는 reject 함수가 
아직 호출되지 않은 상태
fulfilled 비동기 처리가 수행된 상태 (성공) resolve 함수가 호출된 상태 → .then()
rejected 비동기 처리가 수행된 상태 (실패) reject 함수가 호출된 상태 → .catch()
settled 비동기 처리가 수행된 상태 (성공 또는 실패) resolve 또는 reject 함수가 호출된 상태

 

Promise의 세 가지 상태

 

📌 대기(Pending) 상태

- Promise 객체가 생성되고, 비동기 작업이 아직 완료되지 않은 초기 상태입니다.
- 이 상태에서는 resolve 또는 reject 함수가 호출되지 않은 상태입니다.

const myPromise = new Promise(function(resolve, reject) {
  // 비동기 작업 수행
  // 아직 작업이 완료되지 않음
});

 

📌 이행(Fulfilled) 상태

- 비동기 작업이 성공적으로 완료되어 Promise 객체가 결과 값을 반환하는 상태입니다.
- 이 때 resolve 함수가 호출되며, 결과 값이 전달됩니다.

const myPromise = new Promise(function(resolve, reject) {
  // 비동기 작업 성공 시
  resolve('성공적으로 완료된 결과');
});

 

📌 거부(Rejected) 상태

- 비동기 작업이 실패하거나 오류가 발생한 경우 Promise 객체가 거부 상태가 됩니다.
- 이때 reject 함수가 호출되며, 에러 정보가 전달됩니다.

const myPromise = new Promise(function(resolve, reject) {
  // 비동기 작업 실패 시
  reject('에러 발생');
});

 

 

하지만,

직접 접근할 수 없고 .then, .catch, .finally 의 메서드를 사용해야 접근이 가능하다.

Promise는 then과 catch 메소드를 사용하여 비동기 작업의 성공과 실패를 처리한다. 
then 메소드는 Promise가 처리되었을 때 호출되며, 

catch 메소드는 Promise가 처리되지 못했을 때 호출된다.

  • then
    콜백 함수(executor)에 작성했던 코드들이 
    정상적으로 처리가 되었다면 
    resolve 함수를 호출하고
    .then 메서드로 접근할 수 있다.
  • catch
    콜백 함수(executor)에 작성했던 코드들이 
    에러가 발생할 경우에는 
    reject 함수를 호출하고 
    .catch 메서드로 접근할 수 있다.
  • finally
    콜백 함수(executor)에 작성했던 코드들의 
    정상 처리 여부와 상관없이 
    .finally 메서드로 접근할 수 있다.

프로미스 체이닝(Promise Chaining)

Promise chaining가 필요하는 경우는 비동기 작업을 순차적으로 진행해야 하는 경우다.
Promise chaining이 가능한 이유는 

.then, .catch, .finally 의 메서드들은 Promise를 반환하기 때문이다.

따라서 .then 을 통해 연결할 수 있고, 에러가 발생할 경우 .catch 로 처리하면 된다.

프로미스는 체이닝(Chaining)을 통해 여러 비동기 작업을 연결하고 

더 읽기 쉽고 관리하기 쉬운 코드를 작성할 수 있게 해줍니다.

let promise = new Promise(function(resolve, reject) {
	resolve('성공');
	...
});

promise
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .then((value) => {
    console.log(value);
    return '성공';
  })
  .catch((error) => {
    console.log(error);
    return '실패';
  })
  .finally(() => {
    console.log('성공이든 실패든 작동!');
  });

프로미스 라이프사이클(LifeCycle)


🎯 async / await

Promise를 더 간결하게 사용할 수 있게 해 줍니다.
async 함수는 비동기 작업을 수행하고 

await 키워드를 사용하여 Promise의 완료를 기다립니다. (성공 리턴값 받아옴)
이로써 코드는 동기식처럼 보이면서도 실제로는 비동기 작업을 수행합니다.

async / await 구문이 동기 실행 코드처럼 생겼다고 해도 

그 속에는 promise 객체가 존재한다는 것을 유의
try-catch 블록을 사용하여 에러 처리가 가능하며, 

코드의 가독성을 높이고 콜백 지옥을 피할 수 있습니다.

  • async
    - asynchronous(비동기)의 줄임말.
    async함수는 항상 promise 객체를 리턴한다.
  • await
    - promise 객체를 리턴하는 코드 앞에 붙어 있다.
    - 해당 코드를 실행하고 그 코드가 리턴하는 
       promise 객체가 fulfilled(성공) / rejected(실패) 상태가 될 때까지 기다려준다.
    - 작업성공 결과 / 작업실패 결과를 리턴한다. 
       async 함수 안에서만 사용할 수 있는 키워드.

async / await 키워드

  • 함수 앞에 async 키워드를 사용하고, 
    async 함수 내에서만 await 키워드를 사용한다.
  • await 키워드가 작성된 코드가 동작하고 나서 다음 순서의 코드가 동작한다.
  • await 키워드는 피연산자(operand)의 값을 반환해 준다.
    피연산자가 Promise 객체이면 then 메서드를 호출해 얻은 값을 반환해 준다.
// 함수 선언식
async function funcDeclarations() {
	await 작성하고자 하는 코드
	...
}

// 함수 표현식
const funcExpression = async function () {
	await 작성하고자 하는 코드
	...
}

// 화살표 함수
const ArrowFunc = async () => {
	await 작성하고자 하는 코드
	...
}

async / await 사용

📌 async 키워드 사용 - 함수 선언

        async 키워드를 함수 앞에 붙여 함수를 선언

async function process(){ ... }

 

📌 await 키워드 - 리턴값 반환

        await은 Promise가 실행될 때까지 기다리겠다는 뜻이다.

        반환하는 Promise의 앞 부분에 await 키워드를 작성

        파라미터로 넣어준 시간만큼 기다리는 Promise를 수행하는 함수 sleep(ms)

async function process(){
	console.log("안녕하세요!");
	await sleep(1000);
	console.log("반갑습니다!");    
}


try-catch 블록을 사용하여 에러 처리

📌 async-await와 try-catch

        에러를 발생시킬 때에는 throw 를 사용하고, 에러를 잡아낼 때에는 try/catch 문 사용한다.

        rejected 상태가 되는 것을 대비할 수 있다.

const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
 
async function makeError() {
	await sleep(1000);
	const error = new Error();
	throw error;
}
 
async function process() {
	try {
		await makeError();
	} catch (e) {
		console.error(e);
	}
}
 
process();


 

await을 만나는 순간 함수 바깥의 코드를 실행하고 돌아온다.

       코드에 await이 존재하면 코드가 보이는 순서대로 실행되는 것이 아니라는 걸 이해하자.

async function fetchAndPrint() {
	console.log(2);
	const response = await fetch('https://tooktak.com/doit');
    console.log(7);
    const result = await response.text();
	console.log(result); // (8)
}

console.log(1);
fetchAndPrint();
console.log(3);
console.log(4);
console.log(5);
console.log(6);

 


📜 Fetch API

fetch로 리소스를 비동기 요청을 할 수 있습니다. 

주로 API를 호출하고 응답 데이터를 받아오는 데에 주로 사용합니다.

 

Fetch는 JavaScript 내장 API로 비동기 통신 네트워크를 가능하게 하는 기술이다. 

비교적 최근에 나온 기술로 XMLHttpRequest 보다 훨씬 간단하고 보기에도 간결하다. 

jQuery.ajax()와도 비슷하지만 요즘 jQuery를 잘 안 쓰는 추세이므로, 

Fetch가 훨씬 많이 쓰인다.

 

📌 fetch() 사용

fetch() 함수는 2개의 인자를 받습니다.

  • url (필수) : 접근하고자 하는 url입니다.
  • [options] : 선택 매개변수, 생략할 경우 default는 'GET'방식입니다.
fetch(url, [options]);

 

 

사용예시

- fetch 에는 기본적으로 첫 번째 인자에 요청할 url이 들어갑니다.

  default로 http 메소드 중 GET으로 동작합니다.
- 호출 시 해당 주소에 요청을 보낸 다음, 응답 객체(object Response)를 받습니다.

   첫 번째 then에서 그 응답을 받고

   .json() 메서드로 파싱한 json 값을 리턴합니다.
- 그러면 그 다음 then에서 리턴받은 json 값을 받고, 원하는 처리를 할 수 있습니다.

 

※ fetch() 를 이용하면 url을 통해 원하는 API의 결과값을 받아올 수 있다.

    콘솔창에 fetch를 입력하면 Promise 라는 결과가 반환된다. (정확히는 Promise 객체가 반환된다)

    이 결과값을 제대로 사용하거나 보기 위해서는

     .json()을 사용하여 response를 JSON 으로 바꿔주고, 다시 .then()을 사용해 주어야 한다.

fetch("https://jsonplaceholder.typicode.com/posts")
.then(function(res){
  return res.json();
})
.then(function(json){
  console.log(json);
});

 

위 코드를 ES6 문법에서 도입된 화살표 함수로 간소화하면 다음과 같습니다.

fetch("https://jsonplaceholder.typicode.com/posts")
.then(res => res.json())
.then(json => console.log(json));

📌 fetch()와 async/await로 비동기 처리

- getTitle 함수는 fetch를 통해 호출한 API로부터 응답 데이터 받고

  JSON 파싱하여 리턴하는 부분만 담당합니다.

  그리고 그 데이터를 받아서 처리하는 함수가 exec입니다.

- 이 때, await는 비동기 작업의 결과값을 얻을 때까지 기다려 줍니다.

   그리고 getTitle 함수로부터 정상적으로 값을 받은 다음,

   text 변수에 그 값을 저장하고 있습니다.

- await 는 Promise 객체를 리턴하는 부분 앞에만 붙일 수 있습니다.

  이 때, fetch API를 통해 받은 response 데이터는 Promise 객체라서 사용이 가능합니다.

- 또한 await를 함수 내에서 사용하려면 그 함수에는 반드시 async 키워드가 붙어 있어야 합니다.

function getTitle(){
  const response = fetch("https://jsonplaceholder.typicode.com/posts");
  return response.then(res => res.json());
}

async function exec(){
  var text;
  try {
    text = await getTitle();
    console.log(text[0].title);
  }
  catch(error){
    console.log(error);
  }
}

exec();

 

async 가 붙은 함수는 항상 리턴값이 Promise가 됩니다. 

따라서 아래와 같은 코드도 가능합니다.

async function getTitle(){
  const response = fetch("https://jsonplaceholder.typicode.com/posts");
  return response;
}

getTitle()
.then(res => res.json())
.then(json => console.log(json));

 

getTitle 함수가 리턴하는 response는 프로미스 객체가 되므로 

then을 사용하여 값을 처리할 수 있습니다.

 

 

사용예시

fetch('https://huggingface.co/datasets/HuggingFaceTB/smollm-corpus/raw/main/.gitattributes')
    .then((data)=>{
        return data.text();
    })
    .then((data)=>{
        console.log(data);
    })

// 비동기를 이용한 웹사이트를 읽어오는 방법
async function Crawl(url:string){
    let data:string = await fetch(url).then(v=>v.text());
    console.log(data);
}

Crawl('https://huggingface.co/datasets/HuggingFaceTB/smollm-corpus/raw/main/.gitattributes');
async function FirstChar(url:string){
    // 첫글자만 떼어서 출력
    let data:string = await fetch(url).then(v=>v.text());
    // console.log(data.charAt(0));

    // 각 줄을 줄별로 따로 배열로 출력
    let lines: string[] = data.split("\n").filter(v=>v.length);
    console.log(lines);

    // 각줄 띄어쓰기별로 구분 배열화
    let tokens: string[][] =
        lines.map(v=>[v.split("")[0], v.split("")[1]]);
    console.log(tokens);
}

FirstChar('https://huggingface.co/datasets/lewtun/dog_food/raw/main/dataset_infos.json');

 


728x90
반응형