콜백 함수
자바스크립트는 함수도 하나의 자료형이기 때문에 매개변수로 함수를 전달할 수 있다.
이렇게 매개변수로 전달하는 함수를 콜백(callback) 함수라고 말한다.
그리고 동시에 제어권도 함께 전달하는 함수이다.
콜백 함수를 위임받은 코드는 자체적인 내부 로직에 의해서 이 콜백 함수를 적절한 시점에 실행한다.
일단은 콜백 함수에 대해서 가볍게 예시와 함께 알아가보자.
선언적 함수 사용하기
function callThreeTimes (callback) {
for (let i=0; i<3; i++) {
callback(i);
}
}
function print (i) {
console.log(`${i}번째 함수 호출`);
}
callThreeTimes(print);
// ? 0번째 함수 호출
// ? 1번째 함수 호출
// ? 2번째 함수 호출
함수 두 개, callThreeTimes() 와 print() 를 만들었다.
callThreeTimes()는 매개변수로 함수를 받고, 그 함수를 i가 0일 때 부터 3이 되기 전 까지 총 3번 호출한다.
때문에 callThreeTimes(print)를 하면 print(0), print(1), print(2)가 차례로 호출되어 실행 결과와 같은 결과를 낸다.
익명 함수 사용하기
위 예제의 선언적 함수를 익명 함수로 변경한다면 아래와 같이 작성할 수 있다.
function callThreeTimes (callback) {
for (let i=0; i<3; i++) {
callback(i);
}
}
callThreeTimes(function (i) {
console.log(`${i}번째 함수 호출`);
});
// ? 0번째 함수 호출
// ? 1번째 함수 호출
// ? 2번째 함수 호출
콜백 함수를 활용하는 함수
자바스크립트가 기본적으로 제공하는 함수 중에서도 콜백 함수를 활용하는 함수가 많다.
어떠한 형태로 콜백 함수를 활용하는지 알아보자.
forEach()
forEach(콜백 함수) 배열 메소드는 배열 내부의 요소를 사용해서 콜백 함수를 호출해준다.
const numbers = [8, 3, 5];
numbers.forEach(function (value, index) {
console.log(`${index}번째 요소 ${value}`);
});
// ? 0번째 요소 8
// ? 1번째 요소 3
// ? 2번째 요소 5
map()
map(콜백 함수) 배열 메소드는 콜백 함수에서 리턴한 값들을 기반으로 새로운 배열을 만든다.
let numbers = [8, 3, 6];
numbers = numbers.map(function (value) {
return value * value; // 제곱
});
console.log(numbers) // ? [64, 9, 36]
filter()
filter(콜백 함수) 배열 메소드는 콜백 함수에서 리턴하는 값이 true인 값들만 모아서 새로운 배열을 만든다.
let numbers = [8, 0, 5];
numbers = numbers.filter(function (value) {
return value % 2 === 0; // 짝수만 골라냄
});
console.log(numbers) // ? [8, 0]
그리고 위에서 다룬 메소드들을 포함해서 말하자면
콜백함수들을 화살표 함수를 이용해 아래와 같이 쉽게 입력할 수도 있다.
numbers = numbers.filter((value) => value % 2 === 0);
제어권 - 매개변수
여기서 위에서 언급한 제어권이라는 말에 대해서도 다시 보자.
위의 메소드들의 공통점은 콜백 함수가 매개변수로 value, index 를 사용한다는 것.
콜백 함수를 위임 받은 각 함수(forEach, map, filter)는 콜백 함수의 매개변수도 이용할 수 있다는 말이다.
인자에 어떤 값들을 어떤 순서로 넘길 것인지에 대한 제어권을 갖기 때문에
이를 "콜백 함수의 제어권 중 하나인 매개변수를 위임받는다" 라고 표현할 수 있다.
타이머 함수
특정 시간마다 또는 특정 시간 이후에 콜백 함수를 호출할 수 있는 함수들을 타이머(timer) 함수라고 한다.
이 함수들을 이용해서 시간과 관련된 처리를 할 수 있게 된다.
setTimeout()
setTimeout(콜백 함수, 시간) 메소드는 특정 시간 후에 콜백 함수를 한 번 호출한다.
참고로 시간은 ms 단위이기 때문에 초 단위로 확인하려면 1,000을 곱셈해주어야 한다.
setTimeout(() => {
console.log(`1초 후에 실행됩니다`);
}, 1*1000);
여기에서 즉시 출력되는 287은 해당 setTimeout()의 식별자(ID)다.
식별자의 용도는 아래에서 이어서 설명한다.
setInterval()
setInterval(콜백 함수, 시간) 메소드는 특정 시간마다 콜백 함수를 호출한다.
let count = 0;
setInterval(() => {
console.log(`1초마다 실행됩니다(${count}번째)`);
count++;
}, 1*1000);
clearInterval()
clearInterval(타이머의 식별자) 메소드는 setInterval() 메소드로 설정한 타이머를 제거한다.
clearInterval(294);
위에서 setInterval 실습에서 사용했던 타이머 294를 중단해보자.
clearTimeout()
clearTimeout(타이머의 식별자) 메소드는 setTimeout() 메소드로 설정한 타이머를 제거한다.
setTimeout(315);
비교 대상으로 setTimeout()을 두 번 작성하고 하나에만 clearTimeout()을 해주자.
314는 예정대로 3초 후 실행된 반면
314는 3초가 흐르기 이전에 clearTimeout(315) 를 해줌으로써 중단된 것을 확인할 수 있다.
제어권 - 실행 시점
타이머 함수에 대한 정의를 다시 보자.
타이머 함수는 콜백 함수의 호출 시점을 정해서 원하는 때에 호출한다.
이에 대해서 "콜백함수의 제어권 중 하나인 실행 시점을 위임받는다" 라고 표현할 수 있다.
콜백 함수의 제어권 위임
콜백 함수는
다른 함수(A)의 인자로 콜백 함수(B)를 전달하면 A가 B의 제어권을 갖게 된다.
특별한 요청이 없는 이상 A에서 미리 정해놓은 방식에 따라 B를 호출한다.
여기에서 미리 정해놓은 방식이란
- 어떤 시점에서 콜백을 호출할 지,
- 인자에는 어떤 값들을 지정할 지,
- this에 무엇을 바인딩할 지 등이다.
이에 대해 간편하게 "콜백 함수의 제어권에는 실행 시점, 매개변수, this 3가지가 존재한다"고 표현한다.
앞서 실행 시점과 매개변수에 대해서는 정리했고 this의 경우는 this 바인딩에 대해 우선 공부한 뒤 알아보자.
(조만간 this 바인딩 포스팅 업데이트 ... )
콜백 지옥과 비동기 제어
비동기적 처리
동기 (Synchronous) | 비동기 (Asynchronous) |
- 한 번에 하나의 작업을 수행 - 한 작업이 실행되는 동안 다른 작업은 멈춘 상태로 유지하고, 자신의 차례를 기다림 |
- 어떠한 요청을 보내면 그 요청이 끝날 때까지 기다리는 것이 아닌, 응답에 관계없이 바로 다음 동작이 실행 - 흐름이 멈추지 않아서 동시에 여러 가지 작업을 처리할 수 있음 기다리는 과정에서 다른 함수도 호출 가능 |
자바스크립트가 원하는 때에 동작이 시작되도록 하는 것
모든 일을 주어진 순차적으로 해결하는 것이 아닌
동시에 여러 작업을 하면서 효율을 추구하는 처리 방식을 비동기적 (Asynchronous) 처리라고 칭한다.
과거에 이 비동기적 처리를 하기 위해서 주로 사용되는 수단이 콜백 이였다고 이해하면 된다.
콜백 지옥
콜백 함수를 익명 함수로 전달하는 과정이 반복되면서
코드의 들여쓰기 수준이 감당하기 힘들 정도로 과도하게 깊어지는 현상에 대해 콜백 지옥(callback hell) 이라고 칭한다.
콜백 지옥은 또한 값을 전달하는 순서가 아래서 위를 향하고 있기 때문에
가독성도 저하되며 개발자가 어색함을 느낄 수 있어 해결을 권장한다.
콜백 지옥을 해결하는 가장 간단한 방법은
익명의 콜백 함수를 모두 이름을 가진 선언적 함수로 전환하는 것이지만
일회성 함수를 전부 변수에 할당하는 것이 오히려 헷갈릴 소지가 있다는 의견이 있다.
때문에 자바스크립트에 새롭게 도입된 방법이 선호되고 있는데
ES6에서 도입된 Promise 와 ES2017(ES8)에서 도입된 async / await 가 있다.
Promise
Promise는 성공할 수도, 실패할 수도 있다.
성공 할 때에는 resolve를 호출하고, 실패 할 때에는 rejet를 호출한다.
const myPromise = new Promise((resolve, reject) => { /* ... */ })
실패 경우를 처리하지 않을 경우 reject에 대해 생략이 가능하다.
1초 뒤에 성공시키는 경우
resolve를 호출할 때 특정 값을 매개변수로 넣어주면, 이 값은 작업이 끝나고나서 사용 가능하다.
작업이 끝나고 나서 또 다른 작업을 해야할 때에는 Promise 뒤에 then을 붙여서 사용한다.
const myPromise = new Promise((resolve) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
myPromise.then(n => {
console.log(n);
});
1초 뒤에 실패하는 경우
작업이 실패했을 때(reject 시) 수행할 작업에 대해서는 Promise 뒤에 catch 를 붙여서 지정한다.
const myPromise = new Promise((resolve, reject) => {
setTimeout(() => {
reject(new Error());
}, 1000);
});
myPromise.then(n => {
console.log(n);
}).catch(error => {
console.log(error);
});
콜백 지옥 Promise로 개선하기
콜백 지옥 예시
function increaseAndPrint(n, callback) {
setTimeout(() => {
const increased = n + 1;
console.log(increased);
if (callback) {
callback(increased);
}
}, 1000);
}
increaseAndPrint(0, n => { // 지옥문
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
increaseAndPrint(n, n => {
console.log("끝!");
});
});
});
});
});
위 콜백 지옥을 Promise를 이용하도록 수정하면 아래와 같다.
function increaseAndPrint(n) {
return new Promise((resolve) => { // promise 반환 + resolve 설정
setTimeout(() => {
const value = n + 1;
console.log(value);
resolve(value);
}, 1000);
});
}
increaseAndPrint(0)
.then(increaseAndPrint) // 다음 작업 then으로 연결
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(increaseAndPrint)
.then(e => {
console.log("끝!");
});
Promise의 장단점
장점
- 비동기 작업의 갯수가 많아져도 코드의 깊이가 깊어지지 않게 된다.
단점
- 에러 발생 시, 몇번 째에서 발생했는지 잡아내기 어렵다.
- 특정 조건에 따라 분기를 나누는 방식은 어렵다.
- 특정 값을 공유해가면서 작업을 처리하는 것도 까다롭다.
그리고 이 Promise의 단점은 async/await 으로 해결 가능하다.
async / await
Promise 가 콜백함수를 조금 더 쉽게 사용하는 방법이였다면
async / await 은 Promise를 조금 더 쉽게 사용하는 방법이다.
Promise의 resolve, reject, .then() 을 익숙한 동기 함수로 작성하는 것처럼 만들어주기 위해 추가된 문법으로
이 과정에서 평상시 사용하던 try ~ catch 문도 사용할 수 있다.
그리고 이렇게 만들어진 async 함수도 Promise를 반환하기 때문에 Promise를 이용해 만든 함수처럼 사용도 가능하다.
아래 예시로 동작을 조금 더 살펴보자.
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms)); // ms만큼 쉬는 Promise
async function process() {
console.log("안녕하세요!");
await sleep(1000); // 1초 쉬고
console.log("반갑습니다!");
}
process();
- async 키워드를 함수 앞에 붙여 함수를 선언 : async function process(){ ... }
- 파라미터로 넣어준 시간만큼 기다리는 Promise를 수행하는 함수 sleep(ms)
- 반환하는 Promise의 앞 부분에 await 키워드를 작성 : await sleep(1000)
- 해당 Promise가 끝날 때까지 기다렸다가 다음 작업 수행
- then을 이용하는 것도 가능하다
process().then(() => {
console.log("작업이 끝났어요!");
});
async 함수에서
에러를 발생시킬 때에는 throw 를 사용하고, 에러를 잡아낼 때에는 try/catch 문 사용한다.
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();
참고
- 모던 JavaScript 튜토리얼: 콜백
- 벨로퍼트와 함께하는 모던 자바스크립트 : 자바스크립트에서 비동기 처리 다루기
- [책] 혼자 공부하는 자바스크립트
- [책/인강] 코어 자바스크립트
'JAVASCRIPT' 카테고리의 다른 글
[JAVASCRIPT] Execution Context : 실행 컨텍스트 - 콜스택 / 렉시컬 환경 / 호이스팅 / 스코프 체인 (0) | 2024.01.28 |
---|---|
[JAVASCRIPT] var로 알아보는 변수 선언과 할당 - 실행 컨텍스트 / 변수 호이스팅 / TDZ / 가비지 컬렉터 (1) | 2024.01.06 |
[JAVASCRIPT] Function : 함수 (0) | 2023.08.27 |
[JAVASCRIPT] Data Types : 자료형 - primitive type (0) | 2023.07.18 |
[JAVASCRIPT] Variables : 변수와 상수 - let, const, var (0) | 2023.07.15 |