본문 바로가기

Language/Javascript

[Javascript] 비동기 프로그래밍 [Callback, Promise, async/await]

 

Callback의 개념
  • 동기적 콜백
    • 콜백 함수가 즉시 호출되어 실행 결과를 기다린 뒤 이후 명령이 수행되는 방식
    • 호출한 함수가 끝나기 전에 콜백이 실행되고 종료
  • 비동기적 콜백
    • 콜백 함수가 호출한 함수의 실행이 완전히 끝난 뒤, 또는 특정 시점 이후 실행되는 방식
    • 주로 타이머, 이벤트 핸들러, 네트워크 요청과 같은 비동기 작업에서 사용
    • 호출한 함수는 콜백의 실행 결과를 기다리지 않고 다음 명령을 바로 수행
  • 코드를 통해 명시적으로 호출하는 함수가 아니라, 함수를 등록해놓은 후 어떠한 이벤트가 발생(addEventListener 등)했거나 특정 시점(setTimeout 등)에 도달했을 때 시스템에서 호출하는 함수를 의미
  • 파라미터로 함수를 전달받아 함수의 내부에서 실행

동기적 콜백

function greet(name, callback) {
  console.log("Hello, " + name);
  callback(); // 전달받은 콜백 함수 실행
}

greet("Won", () => {
  console.log("This is a synchronous callback.");
});

// 결과
// Hello, Won
// This is a synchronous callback.

비동기적 콜백

function fetchData(callback) {
  console.log("Fetching data...");
  setTimeout(() => {
    callback("Here is the data!");
  }, 2000); // 2초 후 콜백 실행
}

fetchData((data) => {
  console.log(data);
});

Fetching data...
(2초 후)
Here is the data!

 

콜백함수(Callback Function)의 사용 이유
  • 비동기적 프로그래밍뿐만 아니라 재사용성, 모듈화, 가독성 향상 등의 목적으로도 사용
  • 자바스크립트는 싱글 스레드를 사용하여 블록킹을 하는데, 블록킹을 방지하여 논블록킹으로 동작하게 함 

 

콜백 함수 사용 원칙
익명 함수 사용
  • 함수의 내부에서 실행되기 때문에 이름을 붙이지 않아도 되며, 오히려 이름을 붙일 경우 기존의 변수의 값이 함수로 덮어씌워지는 경우도 발생할 수 있음
  • 단, 간결성을 높이기 위해 익명 함수를 사용하는 경우가 많지만 함수의 재사용이 필요하다면 이름을 붙이는 것을 권장
sayHello("인파", function (name) { // 함수의 이름이 없는 익명 함수
	console.log(name); 
});
let add = 10; // 변수 add

function sum(x, y, callback) {
	callback(x + y); // 콜백함수 호출
}

// 이름 있는 콜백함수 작성
sum(1, 2, function add(result) {
	console.log(result); // 3
});

// 변수 add가 함수 add로 덮어씌워짐
console.log(add); // function add(result) {...}
화살표 함수 모양의 콜백
  • 콜백 함수를 익명 함수로 정의하여 코드의 간결성을 얻을 수 있지만, 
    한 단계 더 간결성을 얻기 위해 다음과 같이 '익명 화살표 함수' 형태로 정의하여 사용 가능
function sayHello(callback) {
    var name = "Alice";
    callback(name); // 콜백 함수 호출
}

// 익명 화살표 콜백 함수
sayHello((name) => {
	console.log("Hello, " + name);
}); // Hello, Alice

 

함수의 이름만 넘기기
  • 자바스크립트는 null과 undefined 타입을 제외한 모든 것을 객체로 다룰 수 있는 1급 객체
  • 함수를 변수 또는 다른 함수의 변수처럼 사용 가능
  • 함수를 콜백함수로 사용할 경우 함수의 이름만 전달하면 됨
// 콜백 함수를 별도의 함수로 정의
function greet(name) {
	console.log("Hello, " + name);
}

function sayHello(callback) {
    var name = "Alice";
    callback(name); // 콜백 함수 호출
}

function sayHello2(callback) {
    var name = "Inpa";
    callback(name); // 콜백 함수 호출
}

// 콜백 함수의 이름만 인자로 전달
sayHello(greet); // Hello, Alice
sayHello2(greet); // Hello, Inpa

 

콜백 지옥(Callback Hell)

  • 콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 들여쓰기 수준이 감당하기 힘들 정도로 깊어지는 현상
  • 주로 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 이런 형태가 자주 등장하는데, 가독성이 떨어지면서 코드의 수정이 어려워짐
  • 비동기적인 작업을 수행하기 위해 콜백 함수를 익명함수로 전달하는 과정에서 생기는 콜백 지옥을 Promise, async/await, Generator 등을 사용해 방지 가능
콜백 지옥 예시
function getData(callback) {
  setTimeout(() => {
    console.log("Data fetched");
    callback();
  }, 1000);
}

getData(() => {
  console.log("Step 1");
  getData(() => {
    console.log("Step 2");
    getData(() => {
      console.log("Step 3");
    });
  });
});

 

Promise 예시
function getData() {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log("Data fetched");
      resolve();
    }, 1000);
  });
}

getData()
  .then(() => {
    console.log("Step 1");
    return getData();
  })
  .then(() => {
    console.log("Step 2");
    return getData();
  })
  .then(() => {
    console.log("Step 3");
  });

 

async/await 예제
async function processSteps() {
  await getData();
  console.log("Step 1");
  await getData();
  console.log("Step 2");
  await getData();
  console.log("Step 3");
}

processSteps();
Generator 예제
  • 일반적으로는 Promise와 async/await에 비해 덜 사용
function* processSteps() {
  yield getData();
  console.log("Step 1");
  yield getData();
  console.log("Step 2");
  yield getData();
  console.log("Step 3");
}

const steps = processSteps();
steps.next();
steps.next();
steps.next();

Promise의 개념
  • 자바스크립트 비동기 처리의 완료 또는 실패를 나타내는 값을 가지는 객체
  • 싱글스레드인 자바스크립트에서 비동기 처리를 위해 사용한 Callback 함수의 에러/예외처리의 어려움과 중첩으로 인한 코드 복잡도 증가(콜백 지옥)라는 문제를 해결하기 위해 ES6에서 Promis 객체를 언어적 차원에서 지원하게 됨
  • Promise가 콜백을 완전히 대체하는 것은 아니지만, 콜백을 체계적이고 예측 가능한 패턴으로 사용할 수 있게 해주어 단순한 콜백 사용 시 발생할 수 있는 예상치 못한 동작이나 디버깅이 어려운 버그를 해결하는데 도움을 줌
Promise 사용 예시
const promise = new Promise(resolve, reject) => {
  /*
  비동기 작업 성공 시 resoleve() 호출,
  비동기 작업 실패 시 reject() 호출하도록 구현
  */
}
  • then
    • resolve로 전달된 값을 받아서 작업을 처리
    • 비동기 작업이 성공했을 때 호출
  •  catch
    • reject로 전달된 에러를 받아 처리
    • 비동기 작업이 실패했을 때 호출
  • finally
    • 작업 성공 여부와 상관없이 항상 실행
    • 자원 정리 또는 후처리에 유용
const promise = new Promise((resolve, reject) => {
  // 처리 내용
})

promise.then(
  // resolve가 호출되면 then 실행
)
.catch(
  // reject가 호출되면 catch 실행
)
.finally(
  // 콜백 작업을 마치고 무조건 실행되는 finally (생략 가능)
)
const flag = true;
const promise = new Promise((resolve, reject) => {
  if (flag) {
    resolve('resolve 실행')
  }
  else {
    reject('reject 실행')
  }
})

promise.then((resolveMessage) => {
  console.log(resolveMessage)
})
.catch((errorMessage) => {
  console.log(errorMessage)
})
// 결과 : resolve 실행
Promise 객체로 비동기 처리 연결하기 (Promise Chaining)
  • then()과 catch() 뒤에 또 다른 then()과 catch()를 연결하여 비동기 처리 연결 가능
const flag = true;
const promise = new Promise((resolve, reject) => {
  if (flag) {
    resolve('resolve 실행')
  }
  else {
    reject('reject 실행')
  }
})

promise.then((resolveMessage) => {
  console.log(resolveMessage)
  return new Promise((resolve, reject) => {
  if (flag) {
    resolve('resolve 실행2')
  }
  else {
    reject('reject 실행2')
  }
  });
})
.then((resolveMessage2) => {
  console.log(resolveMessage2)
})
.catch((errorMessage) => {
  console.log(errorMessage)
})

// resolve 실행
// resolve 실행2
Promise의 3가지 상태(states)
  • 프로미스의 상태(states)란 프로미스의 처리 과정을 의미
  • new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 가짐
Pending(대기 상태)
  • 프로미스가 생성 되었으나 아직 resolve나 reject가 호출되지 않은 상태
new Promise(function(resolve, reject) {
  // 아무것도 호출하지 않음 → 여전히 Pending 상태
})
Fulfilled(이행 상태)
  • 프로미스가 성공적으로 완료되어 resolve가 호출된 상태
  • resolve로 전달된 값은 then을 통해 받을 수 있음
new Promise(function (resolve, reject) {
  resolve('Success'); // resolve 호출 → Fulfilled 상태로 전환
}).then(function (result) {
  console.log(result); // 결과: Success
});

 

Rejected(실패)
  • 프로미스가 실패하거나 에러가 발생 reject가 호출된 상태
  • 실패 사유는 catch를 통해 받을 수 있음
function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error('Request is failed'))
  })
}

getData()
  .then(function (result) {
    console.log(result) // 실행되지 않음
  })
  .catch(function (err) {
    console.log(err) // 결과 : Error : Request is failed
})
Settled(종료 상태)
  • 프로미스가 더 이상 Pending(대기) 상태가 아니며 처리 결과가 결정된 상태
  • 즉, 프로미스가 성공적으로 완료(Fulfilled) 되었거나 실패(Rejected)로 끝났을 때를 포괄
    → Settled = Fulfilled + Rejected
  • finally 메서드는 Settled 상태에서 항상 실행

 

 

async & await
  • 비동기식 코드를 동기식으로 표현하여 간단하게 나타내는 것
  • 기존의 비동기 처리 방식인 Callback 함수의 단점을 보완하기 위해 Promise를 사용했지만 코드가 장황하다는 단점이 존재
  • Promise의 단점을 해결하기 위해 ES2017(ES8)에 도입비동기 처리 방식의 가장 최신 문법
  • async & await는 Promise 객체를 반환 (then 사용 가능)
async function myAsync() {
  return 'async';
}

myAsync().then(result => {
  console.log(result);
})

// 결과 : async
async & await 기본 문법
  • 함수의 앞에 async 라는 예약어 추가
  • 함수의 내부 로직 중 HTTP 통신을 하는 비동기 처리 코드 앞에 await 추가
  • 비동기 처리 메서드가 반드시 프로미스 객체를 반환해야 await가 의도한 대로 동작
  • 일반적으로 await의 대상이 되는 비동기 처리 코드는 Axios 등 프로미스를 반환하는 API 호출 함수
  • async 함수는 화살표 함수로도 정의가 가능하고, 함수 표현식으로도 정의가 가능
async function 함수명() {
  await 비동기_처리_메서드명()
}
async 함수
  • async로 동작하는 함수는 항상 프로미스를 반환
  • 프로미스가 아닌 값을 반환하더라도 이행 상태의 프로미스(resolve promise)로 감싸 이행된 프로미스가 반환되도록 함
  • 즉, async가 붙은 함수는 반드시 프로미스를 반환하고, 프로미스가 아니더라도 프로미스로 감싸 반환
async function foo() {
  return 'bar'; // 반환값은 Promise.resolve('bar')로 감싸짐
}

foo().then(console.log); // 결과: bar
await
  • async 함수 내부에서만 사용 가능
  • 자바스크립트는 await 키워드를 만나면 프로미스가 처리(settled)될 때까지 기다리고, 결과는 그 이후 반환
  • 일반 함수나 전역에서 사용하면 에러 발생
async function validUse() {
  const result = await Promise.resolve('OK');
  console.log(result);
}

function invalidUse() {
  // const result = await Promise.resolve('Not OK'); // SyntaxError 발생
}
async & await 사용 예제
function delay(ms) {
  return new Promise(resolve => setTimeout(resolve, ms));
}

// 2초동안 기다리게 하고 사과를 리턴하는 메서드
async function getApple() {
  await delay(2000)
  return 'apple'
}

// 1초동안 기다리게 하고 바나나를 리턴하는 메서드
async function getBanana() {
  await delay(1000)
  return 'banana'
}

getApple().then(console.log)
getBanana().then(console.log)

// 결과
// banana (1초뒤 출력)
// apple (2초뒤 출력)

 

예외 처리
  • async & await의 예외 처리 방법 try catch 구문 사용
  • catch {}를 사용 (Promise는 .catch() 사용)
function fetchData() {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      return resolve('success')
    }, 1000)
  })
}

async function loadData() {
  try {
    const result = await fetchData()
    console.log(result)
  } catch (e) {
    console.log(e)
  }
}
Promise와 async/await 비교
  • Async/Await는 코드의 가독성을 높이고, 비동기 흐름을 동기식처럼 표현
// Promise
fetchData()
  .then(result => {
    console.log(result);
  })
  .catch(err => {
    console.error(err);
  });

// async & await
async function fetchDataWithAwait() {
  try {
    const result = await fetchData();
    console.log(result);
  } catch (err) {
    console.error(err);
  }
}

 

참고 자료

https://hi-zini.tistory.com/entry/%EB%B9%84%EB%8F%99%EA%B8%B0%EC%A0%81-%EB%B0%A9%EC%8B%9D-%EC%B2%98%EB%A6%AC-%EB%B0%A9%EB%B2%95-Callback-Promise-async-await

 

비동기적 방식 처리 방법 (Callback, Promise, async &await)

비동기적 방식 처리 방법 Callback 함수 Promises async & await Callback 함수 Callback 이란? 다른 함수가 실행을 끝낸 뒤 실행(call back)되는 함수(⇒ 나중에 호출되는 함수)를 말한다. 다시 말해 코드를 통해

hi-zini.tistory.com

 

아래 사이트를 참조하면 콜백 함수에 대해 좀 더 깊이 알게 됨

 

 

https://inpa.tistory.com/entry/JS-%F0%9F%93%9A-%EC%9E%90%EB%B0%94%EC%8A%A4%ED%81%AC%EB%A6%BD%ED%8A%B8-%EC%BD%9C%EB%B0%B1-%ED%95%A8%EC%88%98#%EC%BD%9C%EB%B0%B1_%ED%95%A8%EC%88%98_%EC%82%AC%EC%9A%A9_%EC%9B%90%EC%B9%99

 

📚 콜백 함수(Callback) 개념 & 응용 - 완벽 정리

자바스크립트 콜백 함수 란? 콜백(Callback) 함수는 간단히 말하면 매개변수로 함수 객체를 전달해서 호출 함수 내에서 매개변수 함수를 실행하는 것을 말한다. 예를 들어 아래 코드와 같이 sayHello(

inpa.tistory.com