흔히 JavaScript의 비동기를 처리하는 방법은 크게 세 가지가 있다.
1. 콜백 함수(Callback)
2. Promise
3. async awiat
이 중에서 가장 기본이 되는 콜백 함수에 대해서 알아보자.
먼저, 비동기를 어떻게 콜백 함수로 처리할 수 있는지 보기 전에
콜백 함수가 정확히 무엇인지 이해를 해야 된다.
그리고, 콜백 함수를 이해하기 전에 일급 객체가 무엇인지 알아볼 필요가 있다.
1️⃣ 일급 객체 (First Class Object)
다음과 같은 성질을 만족하는 객체를 일급 객체라고 한다.
- 변수에 할당될 수 있다
- 함수의 매개변수로 사용될 수 있다
- 함수의 리턴값이 될 수 있다.
JS에서 함수는 객체로 취급한다. 그리고 함수는 일급 객체이다.
즉, 함수는 변수에 할당될 수 있고,
매개변수로 사용될 수 있고,
어떤 함수의 리턴값이 될 수 있다.
아래는 각 성질의 예시이다.
// foo 함수 선언
function foo() {
console.log("foo");
};
// 새로운 변수에 함수를 할당할 수 있다.
const bar = foo;
// bar 실행시키면 foo가 출력된다
bar();
// foo는 함수를 매개변수로 받고 그 함수를 실행시키는 함수
function foo(myFunc){
myFunc() ;
}
// bar 함수 선언
function bar(){
console.log('test')
}
// foo 에 bar 함수를 매개변수로 전달
// bar 함수가 실행되며 test 출력
foo(bar) ;
// foo 함수 선언
// foo 함수는 test를 출력하는 함수를 return
function foo() {
return function () {
console.log("test");
};
}
// bar를 선언하고, foo의 실행 결과를 bar에 할당
// 즉, bar에는 foo의 return 결과로 test를 출력하는 함수가 할당됨
const bar = foo();
// bar의 실행 결과로 test 출력
bar();
이때, 우리는 두 번째 예시에 주목해야 된다.
두 번째 예시는 함수의 매개변수로 함수를 받은 예시이다.
이때, 매개변수로 전달한 함수를 콜백함수라고 한다.
즉, myFunc 는 콜백함수가 되는 것이다.
또한 이때 콜백함수를 받는 foo 함수를 고차함수라고 부른다.
2️⃣ 콜백 함수의 사용법
다시 말하면 매개변수로 전해지는 함수를 콜백 함수라고 한다.
정의는 아주 단순하다.
콜백 함수를 사용하는 방법은 크게 두 가지가 있다.
하나씩 알아보자.
1. 선언한 함수의 이름을 넘기는 법
이 방법은 이미 위의 예시에 있었다.
위의 예시는 foo 에 bar를 넘겨주었다. bar라는 함수 이름을 넘겨준 것이다.
아래는 이 방법의 또 다른 예시이다.
// myFunc 라는 콜백함수를 받고
// 그 콜백함수 안에 전달 받은 a,b를 매개변수로 하여 실행값을 return 한다
function foo(a, b, myFunc) {
return myFunc(a, b);
}
// a,b 를 더한 값을 return 하는 함수
function add(a, b) {
return a+b
}
// foo에 1,3 add 함수 전달
// add함수가 1,3을 인자로 받아 실행되어 4를 return
// foo는 add 함수의 return 값인 4를 다시 return
foo(1, 3, add)
2. 익명 함수 사용
우리는 1번의 방법을 보고, 그럼 콜백함수를 넘겨주려면 일일이 함수를 선언해야 되나 ?
이러한 생각이 들 수 있다.
예컨대, 위의 예제에서 add 함수를 선언하였는데 저 함수를 사용할 일이 매우 적다면,
굳이 함수를 선언할 필요가 있는지 의문이 들 수 있다.
이때, 우리는 익명 함수를 사용할 수 있다.
익명 함수는 이름이 없는 함수라고 생각하면 된다.
위의 예제에서는 myFunc 자리에 add라는 함수를 넣어주었다,
만약, 익명 함수를 이용한다면 add를 넣지 않고 직접 함수를 넣어줄 수도 있다.
아래 예시를 확인해보자.
// myFunc 라는 콜백함수를 받고
// 그 콜백함수 안에 전달 받은 a,b를 매개변수로 하여 실행값을 return 한다
function foo(a, b, myFunc) {
return myFunc(a, b);
}
// foo에 1,3,익명 함수 할당
// 익명 함수는 x,y라는 두 변수의 합을 return 하는 함수
// 익명 함수의 return 결과인 4를 foo가 다시 return
foo(1, 3, function (x, y) {
return x + y;
});
add라는 함수를 만들지 않고, 그 함수의 내용을 직접 foo의 인자로 넣어주었다.
아마 코드를 보면 왜 익명 함수라고 부르는지 감이 올 것이다.
그저, 인자로 전해준 저 함수의 이름이 없기 때문에 익명 함수인 것이다.
그런데, 단점이라면 코드의 가독성이 떨어진다.
Arrow Function 을 이용하여 익명 함수를 선언하면 위의 문제점을 해결할 수 있다.
Arrow Function은 함수를 선언하는 또 다른 방식이다.
만약 모른다면, 먼저 Arrow Function 을 공부하고 오는 것을 권한다.
아래는 Arrow Function을 이용한 예제이다.
// myFunc 라는 콜백함수를 받고
// 그 콜백함수 안에 전달 받은 a,b를 매개변수로 하여 실행값을 return 한다
function foo(a, b, myFunc) {
return myFunc(a, b);
}
// foo에 1,3,익명 함수 할당
// 익명 함수는 x,y라는 두 변수의 합을 return 하는 함수
foo(1, 3, (x, y) => x + y)
매우 깔끔해졌다.
물론 Arrow Function 이 기존 함수 선언의 모든 것을 대체할 수는 없다.
실제로, this 바인딩에서 두 함수 선언 방식의 차이가 발생한다.
일단, 지금으로서는 Arrow Function 이 기존 함수 선언보다 훨씬 깔끔하다는 것만 생각하자.
3️⃣ 콜백 함수의 사용 예제
정리하자면 콜백 함수는 매개변수로 전달하는 함수를 뜻하며,
단지 어떤 함수가 실행되면 전달 받은 콜백함수가 실행되는 것 뿐이다.
예컨대, 위의 예제에서 foo 함수에서 콜백함수 add를 전달한 것은
foo가 실행되면, add 함수를 실행해달라는 의미이다.
콜백함수는 굉장히 많은 곳에서 사용된다. 위에서 콜백함수를 사용하는 함수를 고차함수라고 하였다.
대표적인 고차함수 map에 대해 알아보자.
map은 배열에서 사용할 수 있는 고차함수(메소드)이며, 콜백 함수를 전달 받고 각각의 원소를 콜백 함수를 거쳐 새로운 값으로 반환한다.
바로 예시를 확인하자.
let arr = [1,2,3]
// arr의 현재 각 원소를 2배한 배열을 doubleArr 변수에 할당
let doubleArr = arr.map(currentValue => currentValue*2)
map은 현재값에 2배 한 결과를 return 하는 콜백함수를 전달 받았다.
실제, map의 결과로 arr 각 원소를 2배한 배열을 return 하게 되고 그 return 값을 doubleArr에 할당하였다.
현재 map의 콜백함수는 currentValue 인자 하나만 사용하지만, 총 3개의 인자를 사용할 수 있다.
첫 번째 인자는 현재 배열의 원소
두 번째 인자는 현재 배열의 인덱스
세 번째 인자는 배열 그 자체이다.
아래 예시를 확인해보자.
let arr = [1, 2, 3];
// map 메소드가 arr의 원소 각각마다 실행
// currentValue에는 현재 map을 진행 중인 arr의 원소 할당
// index에는 현재 map을 진행 중인 arr의 인덱스 값 할당
// currentArr은 map을 사용 중인 배열 그 자체를 반환
arr.map((currentValue, index, curentArr) =>
console.log(
`currentValue : ${currentValue}`,
` index : ${index}`,
`currentArr : ${curentArr}`
)
);
사실 마지막 currentArr 인자까지 사용하는 경우는 적다. 필자는 보통 두 번째 인자까지 사용하는 것 같다.
또 다른 예시를 보자.
let arr = [1, 2, 3];
// arr의 현재 각 원소를 2배 한 후 각 원소에 해당되는 index를 더한 배열을 반환
let newArr = arr.map((currentValue,index)=> currentValue*2 + index)
// newArr -> [2,5,8]
지금까지는 map 안에 익명 함수를 사용했다.
그러나, 위에서 언급했 듯이 콜백 함수에는 함수를 직접 만들고 그 함수의 이름을 전달할 수도 있다.
let arr = [1, 2, 3];
function myFunc(x,y){
return x*2 + y
}
// arr의 현재 각 원소를 2배 한 후 각 원소에 해당되는 index를 더한 배열을 반환
let newArr = arr.map(myFunc)
JS에서 기본으로 제공하는 고차함수는 매우 많다.
map 말고도 reduce, filter, forEach 등 다양한 고차 함수가 존재한다.
실제로, 고차함수는 함수형 프로그래밍에서 사용되므로 알아두면 매우 유용하다.
더 이상 알아보는 것은 이 글의 주제와 벗어나므로 고차 함수에 대한 얘기는 여기서 마무리 하겠다.
4️⃣ 콜백 함수와 비동기
코드의 출력 결과가 실행 순서와 다른 것을 비동기(Asyncronous)라고 한다.
대표적인, 비동기 함수로 Web API 인 setTimeout 함수가 있다.
setTimeout(callback, ms) 다음과 같이 사용할 수 있으며 ms (밀리 세컨드) 이후 실행될 함수를 callback에 전달한다.
아래는 그 예시이다.
console.log('start')
setTimeout(()=>{
console.log(1)
},1000)
console.log('end')
setTimeout은 비동기 함수이므로 먼저 start,end 가 출력되고 1000ms(1초) 후에 1이 출력된다.
여기서 우리가 주목할 점은 setTimeout 안에 콜백 함수를 인자로 전달했다는 것이다.
아래는 1초 후에 1을 출력하고, 다시 1초 후에 2를 출력하는 코드이다.
setTimeout(() => {
console.log(1);
setTimeout(() => {
console.log(2);
}, 1000);
}, 1000);
첫 번째 setTimeout 콜백 함수 안에 1을 출력하고 다시 setTimeout을 넣어주었다.
콜백 함수는 비동기 작업을 마치고 실행하는 함수로 쓰인다.
위의 예제에는 1초 후(비동기 작업) 콜백 함수로 1을 출력하게끔 한 것이다.
그리고 다시 setTimeout을 실행시켜 또 다시 1초 후(비동기 작업) 2를 출력하게끔 한 것이다.
다음은 철수, 영희, 민수, 민주가 릴레이 달리기 경기를 하는 예시이다.
각 선수는 0초 ~ 1초 사이로 완주하게 되며, 마지막 선수인 민주가 도착했을 때 총 2초 이하로 시간이 걸리면 통과하게 된다.
function relayRunning(player, nextFunc, totalTime = 0) {
const runningTime = Math.random() * 1000;
totalTime += runningTime;
setTimeout(() => {
console.log(`${player} 도착 시각 : ${totalTime / 1000}초`);
nextFunc?.(totalTime);
if (!nextFunc) {
totalTime / 1000 < 2 ? console.log("PASS") : console.log("FAIL");
}
}, runningTime);
}
relayRunning("철수", (totalTime) => {
relayRunning(
"영희",
(totalTime) => {
relayRunning(
"민수",
(totalTime) => {
relayRunning("민주", null, totalTime);
},
totalTime
);
},
totalTime
);
});
아마, 초보자라면 위의 코드를 이해하기 힘들 수 있다. 이해 못해도 아무 상관 없다.
결론은 토할 거 같은 코드이다.
콜백 함수를 중첩되게 사용하면 다음과 같이 Callback Hell (콜백 헬)이 발생한다.
코드의 논리적인 오류는 없으나, 가독성이 너무 떨어진다.
JS는 비동기를 콜백으로만 처리할 때 발생하는 콜백 헬을 개선하고자 하였고 그 결과로
Promise, async await 문법이 탄생하게 되었다.
다음에는 콜백 헬을 벗어나고, 비동기를 처리하는 두 번째 방식인 Promise에 대해 알아보겠다.
'JavaScript' 카테고리의 다른 글
[JavaScript] drag (드래그) 관련 이벤트 전격 분석 (0) | 2024.09.10 |
---|---|
[JavaScript] Element.closest 메소드 분석 (0) | 2024.08.31 |
[JavaScript] 이벤트 버블링(Bubbling), 이벤트 위임(Delegation) (2) | 2024.08.31 |
[JavaScript] 모듈 시스템 CommonJS vs ES6 비교 (0) | 2024.08.25 |
[JavaScript] 싱글 쓰레드와 비동기(Asyncronous) (0) | 2024.07.27 |