콜백 함수
다른 함수의 인자로 넘겨지는 함수 (함수가 부른 다른 함수)
콜백 함수를 둘러싼 콜백 수신 함수에 의해서 특정 시점에 실행이 된다.
동기 콜백 함수라면 호출 즉시 실행되지만, 비동기 콜백 함수라면 특정 조건을 만족했을 때 실행된다.
아래 코드처럼 userInput의 인자로 넘겨지고 있는 greeting 함수를 콜백 함수라고 할 수 있다.
function greeting(name) {
console.log("안녕하세요" + name);
}
function userInput(callback) {
const name = prompt("이름을 입력하세요.");
callback(name)
}
userInput(greeting);
비동기 콜백 함수의 예시로는 크게 아래 3가지를 들 수 있다.
1. setTimeout: 특정 시간 후 콜백 함수를 호출하는 함수
2. 이벤트 리스너: 특정 이벤트가 발생했을 때 콜백 함수를 실행함
3. HTTP 요청: 서버로 HTTP 요청을 보낼 때 비동기적으로 요청하고 응답이 도착할 때 콜백 함수를 실행함
아래 코드에서 setTimeout을 써보았다.
1, 3... 그리고 5초 후에 2가 출력되는 것을 알 수 있다.
function one() {
console.log(1);
}
function two() {
setTimeout(() => console.log(2), 5000);
}
function three() {
console.log(3);
}
one();
two();
three();
어떻게 이 작업이 이루어지는지는 아래에서 천천히 정리해보겠다.
싱글 스레드 언어인 JS는 동기적이다.
동기적이라 함은 호이스팅이 된 이후 우리가 코드를 작성한 순서대로 코드가 실행된다는 말이다.
위 그림처럼 해야 하는 작업이 있을 때 A를 끝내야 B를 처리할 수 있고, B가 끝나야 C를 처리할 수 있다.
이러한 방식을 동기적 처리, 블로킹 방식이라고 한다.
그런데 동기적 처리 방식에는 치명적인 단점이 있다.
만약 A라는 일이 0.3초가 아니라 10초가 걸린다면 어떻게 될까?
10초가 지난 후에야 B를 처리할 수 있게 되면서 하나의 작업이 딜레이 되게 된다.
JS는 유저와 상호작용을 하기 위해 쓰는 프로그래밍 언어다.
즉각적으로 작업이 처리되지 않으면 대화, 즉 상호작용이 어려워진다.
따라서 동시에 여러 가지 일을 수행할 수 있어야 한다.
이런 단점을 보안하기 위해 나온 것이 비동기 처리, 논 블로킹 방식이다.
이런 식으로 한 번에 여러 개의 작업을 동시에 실행 시키는 것을 비동기 작업이라고 한다.
하지만 JS는 싱글 스레드 작업 수행 방식을 따르고 있는데 어떻게 비동기 작업을 할까?
이와 관련해 JS 엔진에 대해 간단히 알 필요가 있다.
먼저 JS 코드는 브라우저에 내장된 JS 엔진(JS 코드를 해석하고 실행하는 인터프리터)을 사용해 실행된다.
브라우저마다 JS 엔진이 다른데, 크롬의 v8이 가장 대표적이다.
JS 엔진은 위 그림처럼 메모리 힙과 콜 스택으로 이루어져 있다.
메모리 힙은 변수나 상수들의 메모리를 저장하는 영역이라고만 우선 설명할 수 있다.
지금 중요한 것은 콜 스택이다.
콜 스택은 호출 스택이라고도 불리는데, 함수가 호출되는 순서대로 쌓이는 스택이기 때문이다.
JS 엔진이 JS 코드를 쭉 보면서 함수를 실행할 때 호출 스택에 해당 함수를 집어 넣는다.
그리고 함수가 return (끝나면) 호출 스택의 맨 위에 있을 해당 함수를 없앤다.
콜 스택에는 가장 먼저 Main Context, 그러니까 전역 콘텍스트가 들어온다.
이 전역 콘텍스트가 들어와야 시작이 되고, 나가야 모든 코드가 끝이 난다고 보면 된다.
왼쪽에는 one, two, three 함수가 있으니 JS 엔진은 위부터 코드들을 주르륵 읽어 나간다.
그러다 three() 를 보고 함수 three를 실행해야 하니 콜 스택에 추가한다.
그런데 three 안에는 two()가 있으니, two를 추가하고, two 안에는 one이 있으니 차례대로 콜 스택에 추가한다.
그렇다면 콜 스택에 쌓인 모양은 아래부터 [Main Context] [three()] [two()] [one()] 이 될 것이다.
스택이기 때문에 가장 위에 있는 one이 실행돼 종료되면 콜 스택에서 사라지고 Main Context까지 나가면 코드가 끝나게 된다.
이처럼 JS는 하나의 콜스택으로 모든 작업을 수행하기 때문에 동시에 하나의 일만 처리할 수 있다.
이렇게 작업을 수행하는 일꾼(스레드)가 하나인 언어를 싱글 스레드 언어라고 하며, 그래서 JS가 싱글 스레드 언어인 것이다.
비동기 작업은 어떻게 이루어지나?
JS는 동기적 언어인데 콜백 함수에서 들었던 코드 예시가 어떻게 수행되는 것일까?
아래 그림은 웹 브라우저의 구조다.
웹 브라우저는 JS 엔진 외에도 Web APIs, 콜백 큐, 이벤트 루프를 가지고 있다.
Web API 메소드들은 모두 비동기 메소드라 작동을 마치면 콜백 함수를 콜백 큐에 넣는다.
콜백 큐는 test queue라고도 하는데, 이 곳에서 Web API의 콜백 함수들이 대기를 한다.
이렇게 보면, 웹 브라우저는 JS와 달리 여러 개의 스레드가 사용된다는 걸 알 수 있다.
(정확히는 Web APIs가 멀티 스레드로 동작하는 것)
즉 JS 엔진과 Web API가 상호작용을 하기 위해 사용되는 것이 콜백 큐와 이벤트 루프라고 보면 된다.
왼쪽 코드를 동기적으로 수행하려고 보면 Main Context 다음에 asyncAdd()가 쌓인다.
그런데 이 함수를 실행하려고 보니 setTimemout을 호출하고 있고, cb라는 콜백 함수가 들어 있다.
순서대로 콜 스택에 들어가지만, setTimeout의 타이머인 3000ms가 Web API에 들어간다.
여기서 3초를 기다린 후 콜백 함수인 cb가 콜백 큐로 옮겨진다.
콜백 큐와 콜 스택 사이의 이벤트 루프는 콜 스택에 Main Context 외에 다른 함수가 있는지 확인한다.
만약에 더 이상 실행할 함수가 없다면 그 때 콜백 큐에 있는 콜백 함수를 실행 시킨다.
function one() {
console.log(1);
}
function two() {
setTimeout(() => console.log(2), 5000);
}
function three() {
console.log(3);
}
one();
two();
three();
위 코드가 실행되는 순서는 아래와 같다.
1. one() 함수가 호출되어 콜 스택 진입 후 실행 되어 console.log(1) 출력 -> 콜 스택에서 제거
2. two() 함수가 호출되어 콜 스택 진입
3. setTimeout을 사용해 비동기 작업을 시작하고 콜 스택에서 제거
4. setTimeout 함수는 웹 API에 의해 처리되며 거기서 5000ms, 5초 기다린 후 콜백 함수가 콜백 큐에 추가됨
5. three() 함수가 호출되어 콜 스택 진입 후 실행되어 console.log(3) 출력 -> 콜 스택에서 제거
6. 이벤트 루프를 통해 콜 스택이 비어 있는지 확인한 후 콜백 큐에 있는 콜백 함수가 콜 스택으로 이동
7. console.log(2) 출력 -> 콜 스택에서 제거
추가로 setTimeout의 타이머가 0초라면 어떻게 될까?
console.log("메타몽");
setTimeout(function() {
console.log("메타메타);
}, 0);
console.log("메타몽 귀여워!")
메타몽
메타메타
메타몽 귀여워!
가 나올 것 같지만 실제 코드의 결과는
메타몽
메타몽 귀여워!
메타메타
가 나오게 된다.
1. 메타몽이 콜 스택에 들어와 바로 실행되고 사라진다.
2. 메타메타가 콜 스택에 들어온다.
3. Web API에서 0초를 기다린다.
4. 0초가 지나자마자 콜백 큐로 옮겨진다.
5. 이벤트 루프가 콜 스택에 비어 있는지 확인한다.
6. 아직 메타몽 귀여워! 가 남은 상황이기 때문에 메타메타가 콜 스택에 들어올 수 없다.
7. 그래서 메타몽 귀여워! 가 실행된 후에 메타메타가 실행된다.
콜백 함수가 계속 되면 가독성이 떨어지고 유지보수에 어려움이 있어 콜백 함수가 중첩된 코드를 콜백 지옥이라고 한다.
지금 간단한 코드도 흐린 눈 하고 봐야 하는데 콜백 지옥은 넘나 무서운 일이다.
이런 콜백 지옥을 벗어나게 해주는 것이 있는데, 그게 바로 약속을 뜻하는 Promise다.
Promise는 다음 게시글에서 다루도록 하겠다.
**참고
1) 한입 크기로 잘라 먹는 리액트
2) 우아한테크
'👋🏻 JavaScript > 📖 자바스크립트 ES6+' 카테고리의 다른 글
[JS] Symbol 오브젝트를 알아보자 (0) | 2023.06.24 |
---|---|
[JS] yield와 제너레이터 오브젝트의 메소드 return(), throw()를 알아보자 (0) | 2023.06.23 |
[JS] Generator 오브젝트를 알아보자 : Generator 함수, Generator Function, yield, next() (0) | 2023.06.23 |
[JS] Math 오브젝트를 알아보자 (0) | 2023.06.22 |
[JS] Array 오브젝트의 이터레이터 오브젝트 생성을 알아보자 (0) | 2023.06.22 |