우선 동영상 하나를 보고오자. 이해가 쉬워진다.
https://www.youtube.com/watch?v=MbYShFxp-j0
비동기 처리와 콜백함수
JS는 위에서 아래로 차례대로 코드를 실행한다. 이를 '동기'라고 한다. 그러나 적은 순서대로 실행되지 않을 때도 있는데 이를 '비동기'라고 한다. 왜 순서대로 실행되지 않느냐면 만약 처리가 완료될 때까지 기다린다면 JS는 싱글 스레드를 사용하므로 스레드를 점유하는 동안 아무것도 하지 않고 기다리는 일이 발생하기 때문이다.
때문에 JS는 차단하지 않고 쭉 흐르기 때문에 실행이 완료되기까지 기다리지 않는다. 이 문제를 해결하기 위해 async/await를 사용한다.
비동기 처리를 이용하는 대표적인 예로 '콜백함수'가 있다. 일이 처리된 후에 알려주는 함수라고 해서 Call Back이다. 무언가 처리가 오래 걸리는 일을 맡겨놓고 끝나면 해당 함수(콜백 함수)가 실행되게 함으로써 프로그램이 정지 상태에 머물지 않고 다음 코드를 계속 실행하게 만든다.
var callBackExample = function() {
setTimeout(() => console.log("what"), 0);
};
callBackExample();
console.log("first");
출력결과는 first가 먼저 출력되고 what이 그 다음에 출력된다. (setTimeout이 0이라고 하더라도 이후에 실행된다.) 이와 같은 코드는 워낙 짧고 간단해서 콜백함수의 필요성을 인지할 수 없으나 코드가 길어질수록 콜백함수의 효용성은 높아진다.
콜백함수는 호출 스택에 바로 쌓이는 것이 아니라 백그라운드에 존재했다가 지정된 업무, 시간을 다하면 태스크 큐(Task Queue)를 거쳐 차례대로(FIFO) 호출 스택으로 올라간다. 여기서 Queue는 FIFO(Fisrt In First Out)의 자료구조이다.
스코프
Scope는 말 그대로 범위를 말함. 변수에 접근할 수 있는 범위.
- 글로벌(전역), 로컬(지역)
const a = "global";
function test() {
const a = "local";
}
test();
console.log(a); // global 출력
-var는 함수 스코프, const, let은 블록 스코프(블록 바깥에서 접근 불가)
if (true) {
var a = 3;
}
console.log(a); // 3 출력
if (true) {
const b = 3;
}
console.log(b); // b is not defined 오류 발생
블록 스코프인 변수를 바깥에서도 사용하기 위해서는 블록 외부에서 정의하고 사용하면 된다.
let hello
if (true) {
hello = "hello";
}
console.log(hello);
-스코프 체인(Scope Chain)
var name = "darren";
function outer() {
console.log("외부", name);
function inner() {
console.log("내부", name);
}
inner();
}
outer();
outer 함수 내부에 있는 inner 함수의 스코프에서 name은 정의되어 있지 않다. 따라서 다음은 outer 함수의 스코프에서 name을 찾는다. outer 함수에도 없으니 이제는 전역 스코프에서 찾는다. 이렇게 자기 스코프로부터 가까운 스코프로 확대해나가면서 값을 찾는 것을 '스코프 체인'이라고 한다.
클로저(closure)
클로저(closure)는 스코프를 계속 들고 있는 것이다. 본래 함수 내부에 선언한 변수는 함수가 끝나면 사라지지만, 클로저가 스코프를 계속 들고 있으므로 그 함수 내부의 변수를 참조할 수 있게 된다. (때문에 메모리 문제의 주범이기도 하다)
function outer() {
var a = 1;
var b = "B";
function inner() {
var a = 2;
console.log(b);
}
return inner;
}
var execute = outer();
execute();
호출스택(call stack) = 콜 스택
tip) JS는 싱글 스레드이기 때문에 호출 스택이 1개다.
큐와 달리 스택은 LIFO(Last Input First Out)이다. stack의 본 의미를 생각해보면 이해하기 쉽다. 쌓는 것이니 빼내기 위해서는 마지막에 넣은 것부터 때내는 것이다. 큐(queue)는 맛집에 선 줄, 스택(stack)은 물건 쌓기정도로 이해하면 된다. JS에서 함수를 호출하면 호출 스택에 쌓고 실행 후 처리가 완료되면 삭제된다.
아래 코드를 통해 console에 적힐 문자를 예상해보자
function a() {
function b() {
function c() {
console.log("c");
}
c();
console.log("b")
}
b();
console.log("a");
}
a();
우선 call stack에 호출()된 순서로 a()가 먼저 쌓인 후 b()가 쌓인 후 c()가 쌓인다. 고로 실행 순서는 c(), b(), a()대로 실행된다. LIFO(Last Input First Out). 그 결과 콘솔에 찍히는 문자는 c, b, a순으로 찍힌다. 호출() 되면 쌓이고 실행} 되면 지워진다고 생각하자.
호출 스택을 기반으로 실행 순서를 생각하면 머리를 써가며 실행 순서를 예측하는 것보다 더 유용하고 빠르게 결과를 예측할 수 있다.
호출 스택은 공간이 한정되어 있다. 아래와 같은 재귀 함수를 조심하라. 다음 코드를 실행하면 Maximum call stack error가 난다
function a() {
a();
}
a(); //Maximum call stack error
태스크 큐
호출 스택에서 태스크 큐로 옮겨가는 것은 이해했다고 하자. 그런데 무슨 함수들이 태스크 큐로 옮겨가게 하는 것일까?
setTimeout, setInterval, setImmediate
Promise resolve, reject, async/await
eventListener
이다.
이벤트 루프
호출 스택 - 백그라운드 - 태스크 큐의 흐름을 도는 알고리즘내지는 처리자를 이벤트 루프라고 한다.
콜백함수의 종류에 따라 태스크 큐가 여러 개 형성되는데 우선 순위를 정하여 호출 스택으로 올리는 것도 이벤트 루프의 역할이다.
setTimeout(() => console.log("what"), 0);
console.log("lalalala");
이와 같은 코드의 실행값은 lalalala 다음에 what이 출력된다. (0초 임에도 불구하고)
이유는 setTimeout이 실행되면 우선 Web API라는 백그라운드에 담기고 일정 시간이 지나면(여기서는 0ms) 큐에 담긴다. 그런데 큐에 있는 명령이 스택에 올라가려면 스택이 비어있어야 한다.
https://nodejs.org/ko/docs/guides/event-loop-timers-and-nexttick/
종종 큐에 할 일이 밀려서 타이머가 제 시간에 콜백함수를 실행하지 않는 경우가 있다. 아래 영상을 참고하자
https://www.youtube.com/watch?v=iNH4UQxZexs
IIFE (immediatly invoked function expressions )
(함수)() 꼴로 사용한다. 코드 작성자 외에 브라우저로 접근한 유저가 함수의 내용을 수정할 수 없게 방지한다.
(() => {
const Admin = ["solid", "who", "mao"];
console.log(Admin);
})();
좋은 글
https://new93helloworld.tistory.com/358
https://github.com/yjs03057/33-js-concepts
https://www.youtube.com/watch?v=JaHlR1IGLN8&list=PL7jH19IHhOLMmmjrwCi7-dMFVdoU0hhgF
'Programming Language > 🟨 Javascript' 카테고리의 다른 글
Object, Array, String (0) | 2020.04.07 |
---|---|
JS 객체 지향 프로그래밍 : 객체와 클래스 (0) | 2020.03.17 |
프로미스(Promise) 사용법 (0) | 2020.03.06 |
ES6+ 문법 (2) (0) | 2020.01.28 |
ES6+ 문법 (1) (0) | 2020.01.28 |