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);
}
}
참고 자료
아래 사이트를 참조하면 콜백 함수에 대해 좀 더 깊이 알게 됨
'Language > Javascript' 카테고리의 다른 글
[개념] ES6이란? (1) | 2024.12.24 |
---|---|
[Javascript] DOM 렌더링 시점 (1) | 2024.12.09 |
[Javascript] DOM (Document Object Model)의 개념 (0) | 2024.12.09 |
[Javascript] Javascript 동작 원리 [동기, 비동기] (0) | 2024.12.06 |
[Javascript] 객체(Object)와 배열(Array) (0) | 2024.12.05 |