Promise 다양한 응용

Promise의 성질을 이용하여 다양한 응용을 할 수 있다.

시간초과 추가하기

1
2
3
4
5
6
7
8
9
10
const awaitTimeout = (delay) =>
new Promise((resolve) => setTimeout(resolve, delay));

awaitTimeout(300).then(() => console.log("Hi"));
// 300ms 뒤에 Hi가 출력됨

const f = async () => {
await awaitTimeout(300);
console.log("Hi");
};

위의 코드는 delay를 받아서 delay 뒤에 resolve 하는 Promise로 래핑한 함수이다.

Promise.race를 이용하여 시간초과 로직을 추가할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const awaitTimeout = (delay, reason) =>
new Promise(
(resolve, reject) =>
setTimeout(() => (reason === undefined ? resolve() : reject(reason))),
delay
);

const wrapPromise = (promise, delay, reason) =>
Promise.race([promise, awaitTimeout(delay, reason)]);

wrapPromise(fetch("https://cool.api.io/data.json"), 3000, {
reason: "Fetch timeout",
})
.then((data) => {
console.log(data.message);
})
.catch((data) => console.log(`Failed with reason: ${data.reason}`));

promise.race는 가장 먼저 resolve된 데이터만을 사용하는 함수이다.

이러한 성질을 이용하여 delay안에 promise가 resolve되지 못할 시 에러를 발생시킨다.

시간초과를 위한 함수들을 하나의 객체로 정의하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
class Timeout {
constructor() {
this.ids = [];
}

set = (delay, reason) =>
new Promise((resolve, reject) => {
const id = setTimeout(() => {
if (reason === undefined) resolve();
else reject(reason);
this.clear(id);
}, delay);
this.ids.push(id);
});

wrap = (promise, delay, reason) =>
Promise.race([promise, this.set(delay, reason)]);

clear = (...ids) => {
this.ids = this.ids.filter((id) => {
if (ids.includes(id)) {
clearTimeout(id);
return false;
}
return true;
});
};
}

const myFunc = async () => {
const timeout = new Timeout();
const timeout2 = new Timeout();
timeout.set(6000).then(() => console.log("Hello"));
timeout2.set(4000).then(() => console.log("Hi"));
timeout
.wrap(fetch("https://cool.api.io/data.json"), 3000, {
reason: "Fetch timeout",
})
.then((data) => {
console.log(data.message);
})
.catch((data) => console.log(`Failed with reason: ${data.reason}`))
.finally(() => timeout.clear(...timeout.ids));
};

더욱 더 독립적으로 사용할 수 있게 되었다.

디바운싱

클로저와 Promise를 이용하여 디바운싱을 구현할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const debouncePromise = (fn, ms = 0) => {
let timeoutId;
const pending = [];
return (...args) =>
new Promise((res, rej) => {
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const currentPending = [...pending];
pending.length = 0;
Promise.resolve(fn.apply(this, args)).then(
(data) => {
currentPending.forEach(({ resolve }) => resolve(data));
},
(error) => {
currentPending.forEach(({ reject }) => reject(error));
}
);
}, ms);
pending.push({ resolve: res, reject: rej });
});
};

const fn = (arg) =>
new Promise((resolve) => {
setTimeout(resolve, 1000, ["resolved", arg]);
});
const debounced = debouncePromise(fn, 200);
debounced("foo").then(console.log);
debounced("bar").then(console.log);

위 코드는 ms 만큼의 delay 후에 마지막 호출의 값을 반환한다. 이전에 호출된 함수들은 마지막 호출과 동일한 데이터를 반환한다.

호출될 경우 외부 pending 배열을 복사한 후 이를 초기화한다.

ms동안 새로운 호출이 없을 경우 복사한 pending 배열을 순회하며 resolve나 reject를 호출한다.

Ref

How can I add a timeout to a promise in JavaScript?

Debounce promise

Promise

UsingPromise

debounce function implemented with promises

여러개의 Promise를 다루는 다양한 방법 비교

js에서 비동기 호출을 다루는데엔 여러 방법이 있다.

인간 js엔진되기 2편에서도 다루었지만 더 명확하게 비교하여 정리해보려고 한다.

1. for each

여러개의 Promise가 들어있는 배열을 for each로 순회하며 호출할 수도 있다.

1
2
3
4
5
6
const urls = ["1", "2", "3", "4"];
urls.forEach(async (url) => {
const result = await fetch(url);
console.log(result.json());
});
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
complete
1
2
3
4

complete가 먼저 호출된 후 호출스택이 비었을 떄 1 2 3 4 순서대로 출력된다.
즉 배열 내 모든 Promise가 resolve 혹은 reject될 때까지 기다리지 않는다.

2. for await of

위와 같은 코드를 for await of 를 이용하여 작성해보자

1
2
3
4
5
6
const urls = ["1", "2", "3", "4"];
for await(let url of urls){
const result = await fetch(url);
console.log(result.json());
});
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
1
2
3
4
complete

1 2 3 4가 호출된 후 complete가 호출된다.
즉 배열 내 모든 Promise가 resolve혹은 reject될 때까지 기다린다.

3. Promise.all

위와 같은 코드를 Promise.all을 이용하여 작성해보자

1
2
3
4
5
6
7
8
const urls = ["1", "2", "3", "4"];
await Promise.all(
urls.map(async (ele) => {
result = await fetch(url);
console.log(result.json());
})
);
console.log("complete");

실행결과는 다음과 같다.

1
2
3
4
5
1
2
3
4
complete

즉 Promise.all도 배열 내 모든 Promise가 resolve혹은 reject될 때까지 기다린다.

하지만 for await of와 다르게 Promise의 순서를 보장하지 않는다.

아래와 같은 Timer 함수를 작성한다고 해보자

1
2
3
4
5
6
function timer(delay) {
console.log(`${delay} 타이머 시작`);
setTimeout(() => {
console.log(`${delay} 타이머 종료`);
}, delay);
}

for await of와 Promise.all에서 호출해보았다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
let times = [1000, 2000, 3000, 4000];
Promise.all(times.map((time) => timer(time)));
console.log("complete");

// 1000 타이머 시작
// 2000 타이머 시작
// 3000 타이머 시작
// 4000 타이머 시작
// 1000 타이머 종료
// 2000 타이머 종료
// 3000 타이머 종료
// 4000 타이머 종료
// complete

for await (let time of times) {
timer(time);
}

// 1000 타이머 시작
// 1000 타이머 종료
// 2000 타이머 시작
// 2000 타이머 종료
// 3000 타이머 시작
// 3000 타이머 종료
// 4000 타이머 시작
// 4000 타이머 종료
// complete

즉 for await of는 Promise간의 실행순서를 보장하지만 Promise.all은 그러지 못한다.

4. Promise.allSettled

Promise.allSettled는 Promise.all과 같은 기능을 하지만 Promise.all은 부분적으로 실패할 경우에 배열 전체의 실행이 중지되고 catch문으로 이동한다.
하지만 allSettled는 배열의 모든 Promise를 실행하고 상태와 결과값을 하나의 객체로 resolve문에 넘긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];

Promise.allSettled(promises).then((results) =>
results.forEach((result) => console.log(result))
);

// {status:'fulfilled', value:3}
// {status:'rejected',reason:'foo'}

const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) =>
setTimeout(reject, 100, "foo")
);
const promises = [promise1, promise2];

Promise.all(promises)
.then((results) => results.forEach((result) => console.log(result.status)))
.catch((error) => console.log(error));

// foo

정리하자면 Promise.all은 일부 Promise에서 에러가 발생할 경우 모든 Promise의 실행을 중지하고 전부 에러를 발생시킨다.

하지만 Promise.allSettled는 일부 Promise에서 에러가 발생하더라도 모든 Promise가 실행될 때까지 기다린 후 Promise배열의 상태와 결과를 리턴한다.

5. Promise.race

Promise.race는 가장 먼저 완료된 Promise의 값으로 Promise를 reject하거나 resolve한다.

1
2
3
4
5
6
7
8
9
10
11
12
const promise1 = new Promise((resolve, reject) => {
setTimeout(resolve, 500, "one");
});

const promise2 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, "two");
});

Promise.race([promise1, promise2]).then((value) => {
console.log(value);
// two
});

정리

순서가 보장되어야 하는가? => for await of
일부 Promise에서 에러가 발생할 경우 전체의 실행을 멈추고 싶은가? => Promise.all
일부 Promise에서 에러가 발생하더라도 에러가 발생한 일부 Promise만 따로 예외처리 해주고 싶은가? => Promise.allSettled
가장 먼저 실행이 된 Promise의 결과만 다루고 싶은가? => Promise.race

인간 JS 엔진되기 2 promise

제로초님의 인간 js엔진되기 2를 보고 정리하였다.

Promise

promise는 실행된 결과를 나중에 쓸 수 있다고 생각하면 된다.

실행된 결과가 promise 내부에 저장되기 떄문이다.

아래의 코드는 1초뒤에 console.log 작업을 무조건 해야한다.

1
2
3
4
5
setTimeout(() => {
console.log("a");
}, 1000);

//'a' 1초뒤

promise로 작성할 경우 결과값을 저장하다가 필요할 떄에 쓸 수 있다.

1
2
3
4
5
6
7
8
9
10
11
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve();
}, 1000);
});

//...기타 작업

promise.then(() => {
console.log("a");
});

promise를 이용하면 코드도 더 깔끔하게 작성할 수 있다.

콜백함수는 결과값을 바로 받아서 사용해야하기 떄문에 콜백지옥이 발생할 수 밖에 없다.

1
2
3
4
5
axios.get("서버주소1", function (data1) {
axios.get("서버주소2", function (data2) {
axios.axios.get("서버주소3", function (data3) {});
});
});

하지만 promise를 사용할 경우 결과값을 나중에 사용해도 되기 떄문에 같은 로직도 더 깔끔하게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
const p1 = axios.get("서버주소1");
const p2 = axios.get("서버주소2");
const p3 = axios.get("서버주소3");
const p4 = axios.get("서버주소4");
const p5 = axios.get("서버주소5");

Promise.All([p1, p2, p3, p4, p5, p6])
.then((result) => {})
.catch((error) => {})
.finally(() => {});

하지만 PromiseAll은 여러 요청 중 하나만 실패해도 error블럭으로 가기 때문에 성공한 나머지 응답을 사용하지 못한다는 단점이 있다.

이를 보완하기 위해 나온 것이 Promise.allSettled이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const p1 = axios.get("서버주소1");
const p2 = axios.get("서버주소2");
const p3 = axios.get("서버주소3");
const p4 = axios.get("서버주소4");
const p5 = axios.get("서버주소5");

Promise.allSettled([p1, p2, p3, p4, p5, p6])
.then((result) => {
//실패한 것만 필터링해서 다시 시도할 수 있다.
})
.catch((error) => {})
.finally(() => {});

// [
// {status: "fulfilled", value: 33},
// {status: "fulfilled", value: 66},
// {status: "fulfilled", value: 99},
// {status: "rejected", reason: Error: an error}
// ]

Promise.allSettled를 사용하면 모든 요청을 이행한 후 각 promise에 대한 결과를 나타내는 객체 배열을 반환한다.

비동기

  1. 한 번 비동기는 영원히 비동기이다. 비동기로 하는 작업은 동기 작업으로 바꾸려는 시도를 하지 말아야 한다.

  2. 비동기는 동시가 아닌 순서의 문제다.

즉 코드순서와 실제 실행순서가 다르다.

때문에 비동기코드는 일반적인 호출스택으로 분석할 수 없다.

이벤트 루프를 생각하여 분석해야한다.

1
2
3
4
5
6
7
8
9
10
11
setTimeout(() => {
console.log("a");
}, 0);

setTimeout(() => {
console.log("b");
}, 1000);

setTimeout(() => {
console.log("c");
}, 2000);

비동기함수들은 js엔진에서 실행하여 끝날 때 비동기함수의 반환값을 태스크 큐에 넣는다.

이벤트루프는 호출스택에서 하나의 노드가 pop 될 때 마다 태스크를 확인해서 호출스택에 넣는다.

마이크로 태스크는 매크로 태스크보다 더 높은 우선순위를 가진다.

마이크로 태스크엔 promise와 process.nextTick이 들어간다.
나머지는 매크로 태스크에 들어간다.

비동기, 동기 그리고 Promise의 관계

promise에도 동기가 있다. promise의 실행은 바로 하기 때문이다.

promise 내부의 코드를 실행한 후 결과값을 promise에서 저장하다가 원하는 타이밍에 resolve를 통해 실행한다.

promise가 아닌 값은 자동으로 resolve된다.

정리하자면 promise의 실행은 바로하지만 결과값은 resolve 될 떄에 promise 객체에 저장된다. 저장된 결과값은 then에서 더 나중에 사용할 수 있다.

만약 결과값이 늦게 나온다면 then, await, Promise.all 이런 함수들은 결과값을 기다린 후에 실행된다.( axios.then 하는 상황을 생각하면 된다.)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const p = new Promise((resolve, reject) => {
console.log("제일먼저");
setTimeout(() => {
a = 5;
console.log(a);
resolve(a);
}, 0);
});

// ... 기타 작업

console.log("딴짓");

p.then((result) => {
console.log("result", result);
});

// 제일먼저
// 딴짓
// 5
// result 5

위의 코드는 다음과 같은 순서로 동작한다.

  1. promise p를 만들며 내부의 제일먼저를 출력한다.
  2. setTimeout을 실행한다.
  3. 익명함수에서 딴짓을 출력한다.
  4. p.then을 실행한다.( 프로미스 객체에 then을 등록한다.)
  5. 동기 작업들은 모두 완료되었으므로 호출스택에서 익명함수가 제거된다.
  6. 다른 스레드에서 작업하던 setTimeout 내부 콜백을 매크로 태스크에 넣는다.
  7. 현재 호출스택이 비어있으므로 이벤트루프에서 setTimeout 내부 콜백을 호출스택에 삽입하여 실행하고 5가 출력된다. 이후 promise p의 resolve를 호출한다. 그리고 호출스택에서 setTimeout 내부 콜백이 제거된다.
  8. resolve가 호출되었으므로 다른 스레드에서 마이크로 태스크 큐에 p.then 내부 콜백을 전달한다.
  9. 현재 호출스택이 비어있으므로 이벤트 루프에서 p.then 내부 콜백을 호출스택에 삽입하여 실행하고 result 5가 출력된다.
  10. 호출스택과 큐가 비어있고 js 엔진의 스레드에서도 하는 작업이 없기 때문에 프로그램이 종료된다.

async/await을 Promise로 바꾸기

then에서 에러가 발생하면 아래의 then은 실행되지 않고 바로 reject되어서 catch블록으로 이동하게 된다.

아래의 코드에서 첫 번쨰 then에서 에러가 발생했따면 이후의 then은 무시된다. 마지막 catch 블록에서 전부 담당해야 한다.

1
2
3
4
5
6
7
8
9
10
11
p.then((result) => {
console.log("result", result);
})
.then(() => {
// 에러 발생
})
.then(() => {})
.then(() => {})
.then(() => {})
.then(() => {})
.catch((e) => {});

때문에 아래와 같이 작성하는 것이 바람직하다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
p.then((result) => {
console.log("result", result);
})
.then(() => {
// 에러 발생
})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {})
.then(() => {})
.catch((e) => {});

await에도 똑같이 적용된다.
다음과 같이 promise 분기를 적용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

async function a() {
try {
await delay(1000);
} catch (error) {
console.error(error);
}
try {
await delay(1000);
await delay(1000);
await delay(1000);
} catch (error) {
console.error(error);
}
}

또한 then 내부의 반환값에 따라 다음 then 블럭에서의 값이 달라진다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
p.then((result) => {
console.log("result", result);
})
.then(() => {
//return undefined
})
.then((result) => {
console.log(result); // undefined
return 1;
})
.then((result) => {
console.log(result); // 1
return Promise.resolve(1);
})
.then((result) => {
console.log(result); // 1
})
.then(() => {})
.then(() => {})
.catch((e) => {});

마찬가지로 await에서도 똑같이 적용된다.

1
2
3
4
async function a() {
const a = await 1; //1
const b = await Promise.resolve(1); // 1
}

위 두가지 코드를 참조하여 async/await을 promise로 바꿀 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async function a() {
const a = await 1;
console.log("a", a);
console.log("hmm");
await null;
const b = await Promise.resolve(1);
console.log("b", b);
return b;
}

a()
.then((result) => {
console.log(result);
})
.then((result2) => {
console.log(result2);
});

// a 1
// hmm
// b 1
// 1

Promise.resolve(1) // await 1이기 때문에 promise를 만들어주어야 한다.
.then((a) => {
console.log("a", a); //await 아래 함수들은 첫 번째 await이 resolve된 이후 실행된다.
console.log("hmm");
return null;
})
.then(() => {
Promise.resolve(1);
})
.then((b) => {
console.log("b", b);
return b;
});

// a 1
// hmm
// b 1
// 1

하지만 실행컨텍스트가 다르기 떄문에 구조는 같지만 완전히 같은 코드는 아니다.

실행컨텍스트와 스코프체인을 고려하여 async/await은 제너레이터 문법으로 구현되었다.

await을 연달아 쓰면 안된다.

promise는 결과값을 나중에 사용할 수 있는 것이다.

병렬로 처리해야할 경우에는 await을 연달아 쓰는 것보다 Promise.allSettled를 사용하여 처리하는 것이 훨씬 낫다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function delay(ms) {
return new Promise((resolve, reject) => {
setTimeout(resolve, ms);
});
}

async function a() {
await delay(3000);
await delay(6000);
await delay(9000);
} // 토탈 18초

async function b() {
const p1= delay(3000);
const p2 delay(6000);
await Promise.allSettled([p1,p2]);
await delay(9000);
} // 토탈 15초

Promise의 다양한 활용

순서가 보장되어야 할 경우와 그렇지 않은 경우에 대해 다르게 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
const results = await Promise.all([p1, p2, p3]);
// 동시에 된다.

result.map(async () => {
//await result....
}, []);
// 동시에 된다.

for (let result of results) {
//await result....
}
// 순서가 보장된다. p1 후 p2 후 p3

인간 JS 엔진되기 1

제로초님의 인간 js 엔진되기를 보고 정리한 게시글이다.

1
2
3
4
5
6
7
8
9
10
const add = (a, b) => a + b;

function calculator(func, a, b) {
return func(a, b);
}

add(3, 5); //8
calculator(add, 3, 5); //8

document.querySelector("#header").addEventListener("click", add);

함수 호출과 함수의 실행

호출은 함수의 결과값으로 대체해서 생각하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import {useCallback}from 'react';

export const App=()=>{
const onClick=useCallback((e)=>{
console.log(e.target);
},[])

return (
<div onClick={onClick()}></div>
// 즉시 실행되어서 e is undefined 에러가 발생한다.
<div onClick={onClick}></div>
// <div onClick={(e)=>{console.log(e.target)}></div>
}
)
}

호출스택

함수의 호출은 스택에 하나의 노드를 삽입한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const x = "x";

function c() {
const y = "y";
console.log("c");
debugger; // call Stack을 확인할 수 있다.
}

function a() {
const x = "x";
consoe.log("a");
function b() {
const z = "z";
console.log("b");
c();
}
b();
}

a(); // 익명함수 a b c
c(); // 익명함수 c

// a b c c

스코프체인

js에서 scope는 변수에 접근할 수 있는 범위이다.

함수객체로부터 시작해서 상위 실행컨텍스트를 연결리스트 형식으로 관리한다.

변수나 선언에 접근할 때 해당 함수 객체의 실행컨텍스트를 탐색하고 없을 경우 연결리스트의 다음 노드의 실행컨텍스트를 검색한다. 이를 스코프체인이라고 한다.

호이스팅

const나 let 선언보다 위에서 변수에 접근을 했을 경우 해당 영역을 TDZ(temporal dead zone)라고 부른다.

실행컨텍스트를 구성하면서 인터프리터가 변수 내의 메모리공간을 미리 할당해놓기 때문에 발생하는 현상이다.

var의 경우 호이스팅하면서 undefined로 초기화하지만 let과 const는 호이스팅시에 변수를 초기화하지 않는다.

funcion의 경우 선언과 내용 둘 다 호이스팅된다.

this

nodejs와 js에서 this는 기본적으로 globalThis를 가리킨다.

아래의 네 경우가 아닐 경우에 this는 globalThis라고 봐도 무방하다.

  1. this는 함수가 호출될 때 정해진다.
1
2
3
4
5
6
7
8
9
10
11
12
const obj = {
name: "박성현",
sayName() {
console.log(this.name);
},
};

obj.sayName(); // 박성현

const sayN = obj.sayName;

sayN(); // '' (window.name)

##2. 화살표 함수의 this는 무조건 상위 실행컨텍스트의 this를 가리킨다.

1
2
3
4
5
6
7
8
const obj = {
name: "박성현",
sayName: () => {
console.log(this.name);
},
};

obj.sayName(); // '' (window.name)
  1. new로 객체를 만들게 되면 this는 인스턴스를 가리킨다.
1
2
3
4
5
6
7
function Human(name) {
this.name = name;
}

new Human("박성현");

//Human(name:'박성현')
  1. call,bind,apply로 this를 명시적으로 바인딩 할 수도 있다.
    다만 화살표함수는 바인딩할 수 없다.
1
2
3
4
5
6
function sayName() {
console.log(this.name);
}
sayName.bind({ name: "박성현" })(); //박성현
sayName.apply({ name: "박성현" }); //박성현
sayName.call({ name: "박성현" }); //박성현

스코프와 매개변수

변수에 접근할 때 스코프체인을 순회하며 탐색한다.

실행컨텍스트는 매개변수를 포함하여 생성된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const x = true;
const y = false;

function a(){
let a=4;
if(x){
let a=3;
for(let i=0;i<a;i++>){
console.log(i);
}
if(y){
kkk();
}
}
// z(); error
}

a();
const z=()=>{};

js canvas를 이용하여 text의 길이 계산하기

HTML canvas를 사용하여 문자열의 너비를 알아낼 수 있다.

ctx.measureText(text)는 text를 파라미터로 하는 TextMetrics를 반환한다. 이 객체의 width 속성으로 문자열의 너비를 알아낼 수 있다.

1
2
3
4
5
6
function getTextWidth(text, fontSize, fontFace) {
var canvas = document.createElement("canvas");
var canvasContext = canvas.getContext("2d");
canvasContext.font = fontSize + "px " + fontFace;
return canvasContext.measureText(text).width;
}

Ref

https://developer.mozilla.org/en-US/docs/Web/API/CanvasRenderingContext2D/measureText

new 연선자와 생성자 함수

new연산자와 생성자 함수를 사용하여 유사한 객체 여러개를 쉽게 만들 수 있다.

생성자 함수

생성자 함수는 아래 두 관례를 따른다.

  1. 함수 이름의 첫 글자는 대문자로 시작한다.
  2. 반드시 new연산자를 붙여 실행한다.

생성자 함수는 다음과 같은 방식으로 동작한다.

1
2
3
4
5
6
7
8
function User(name) {
// this={}; (빈 객체가 암시적으로 만들어짐)
this.name = name;
this.isAdmin = false;
// return this;
}

let user = new User("sunghyeon");

js 조건문과 비교연산자

배열의 sort함수를 사용하면서 condition함수를 작성하던 중 실수가 있었다.

사소한 실수지만 실수를 통해 공부한 것들을 정리해두려고 한다.

자바스크립트의 비교 연산자는 두 가지가 존재한다.

==

==는 값을 비교하여서 같으면 true, 다르면 false이다.

아래의 코드를 실행하였을 때 0과 -0은 false, 양수인 1은 true, 음수는 true, false 아무 값도 가지지 않았다.

1
2
3
4
5
6
7
8
9
console.log("0==:", 0 == true, 0 == false);
console.log("-0==:", -0 == true, -0 == false);
console.log("1==:", 1 == true, 1 == false);
console.log("-1==:", -1 == true, -1 == false);

// 0==: false true
// -0==: false true
// 1==: true false
// -1==: false false

===

=== 연산자는 값과 데이터의 타입이 모두 같으면 true, 다르면 false이다.

아래의 코드를 실행하여 보았다.
숫자는 데이터타입이 Number이므로 콘솔에는 다음과 같은 결과가 찍혔다.

1
2
3
4
5
6
7
8
9
console.log("0===:", 0 === true, 0 === false);
console.log("-0===:", -0 === true, -0 === false);
console.log("1===:", 1 === true, 1 === false);
console.log("-1===:", -1 === true, -1 === false);

// 0===: false false
// -0===: false false
// 1===: false false
// -1===: false false

if문

자바스크립트의 조건문인 if문에서는 falsy값(false,0,-0,null,undefined,NaN,"",0n)을 제외한 모든 값은 true로 간주된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (0) {
console.log("0 is true");
}
if (-0) {
console.log("-0 is true");
}
if (1) {
console.log("1 is true");
}
if (-1) {
console.log("-1 is true");
}

// 1 is true
// -1 is true

결론

오름차순 정렬을 위해서는 다음과 같이 적어야한다..!
적고보니 당연한 거지만 코드를 적을때는 미처 찾지 못하였다.

1
2
3
arr.sort((a, b) => {
if (a - b < 0) return -1;
});

js Array zip함수

파이썬에는 배열에 적용할 수 있는 zip함수가 있다.

이 함수는 배열을 다음과 같이 만든다.

1
2
3
4
5
arr1 = [1, 2, 3];
arr2 = [4, 5, 6];
arr3 = [7, 8, 9];

zip(arr1, arr2, arr3); //[[1,4,7],[2,5,8],[3,6,9]];

js로는 다음과 같이 zip함수를 만들 수 있다.

1
zip = (rows) => rows[0].map((_, c) => rows.map((row) => row[c]));

실제 실행결과는 다음과 같다..!

1
2
3
4
5
6
7
8
9
10
11
arr1 = [1, 2, 3];
arr2 = [4, 5, 6];
arr3 = [7, 8, 9];

zip[(arr1, arr2, arr3)];

/* result
0: (3) [1, 4, 7]
1: (3) [2, 5, 8]
2: (3) [3, 6, 9]
*/

js 이벤트 루프

전 게시글에선 js의 타이머 함수에 대해서 알아보았다.

이번엔 이런 실행의 기반이 되는 자바스크립트의 동시성 모델과 이벤트 루프에 대해서 정리하려 한다.

런타임 개념

  1. 함수 호출은 자바스크립트의 콜스택 위에 하나의 프레임을 만든다.
  2. 자바스크립트는 콜스택의 맨 위부터 하나씩 실행하며 실행 후에는 제거한다.
  3. 자바스크립트는 메시지 큐( 처리할 메시지의 대기열)을 사용한다.
    큐에서 가장 오래된 메시지와 연결된 함수를 호출하여 실행 후 제거한다.
    마찬가지로 새로운 스택 프레임도 생성된다.

실행환경과 자바스크립트

자바스크립트와 자바스크립트 실행환경(nodejs,브라우저)는 다른 구조를 갖고있다.

실행환경은 싱글스레드가 아니다

자바스크립트는 싱글스레드를 사용하지만 실제 실행환경은 멀티스레드로 실행된다.

브라우저 환경을 간략하게 그림으로 표현하면 다음과 같다.

browser

위의 그림과 같이 비동기 호출을 위한 함수들은 webAPI 영역에 정의되어있다.

또한 이벤트 루프와 태스크 큐 등도 자바스크립트 엔진 외부에 구현되어있다.

nodejs또한 마찬가지이다.

자바스크립트 엔진

하지만 자바스크립트 엔진은 단일 호출 스택과 동시에 여러 함수를 실행할 수 없다.

자바스크립트 엔진에서의 함수 호출은 스택 프레임을 형성한다.

또한 스택의 모든 함수들이 실행을 마치고 스택에서 제거되기 전까지는 어떤 함수도 실행될 수 없다.

태스크 큐와 이벤트 루프

앞서 설명했듯이 실행환경은 멀티스레드로 동작하고 자바스크립트 엔진은 싱글스레드로 동작한다.

이벤트루프와 태스크 큐는 다른 방식으로 동작하는 실행환경과 자바스크립트 엔진이 상호작용하는 방법이다.

태스크 큐

코드가 실행되면 이 코드는 현재 실행중인 태스크가 된다.

코드를 실행하는 중에 호출된 함수들은 태스크 큐에 추가된다.

마이크로 태스크 큐

마이크로 태스크 큐는 일반 태스크보다 더 높은 우선순위를 갖는 태스크이다.

대표적으로 promisethen()과 같은 메서드들은 태스크 큐가 아닌 마이크로 태스크 큐에 추가된다.

이벤트 루프

이벤트루프를 간략하게 나타내면 다음과 같다.

1
2
3
while (queue.waitForMessage()) {
queue.processNextMessage();
}

waitForMessage는 현재 실행중인 태스크가 없을 때 다음 태스크가 큐에 추가될 때 까지 대기한다.

현재 실행중인 태스크가 없는지태스크 큐에 태스크가 있는지를 반복적으로 확인한다.

현재 실행중인 함수의 실행이 다 끝나 호출스택이 비워지면 이벤트 루프는 다음과 같이 실행된다.

  1. 마이크로 태스크 큐가 비었는지 먼저 확인한다. 비어있지 않을 경우 마이크로 태스크 큐에 있는 콜백을 실행한다.( 호출스택에 추가한다.)
  2. 비어있을 경우 (일반)태스크 큐에 대기중인 가장 오래된( 맨 앞의 ) 태스크를 실행한다(호출스택에 추가한다.)

정리하자면 이벤트루프는 태스크 큐와 자바스크립트의 단일 호출 스택을 상호작용하게 해준다.

ref

Event Loop mdn
자바스크립트와 이벤트 루프
코드종 영상
Tasks, microtasks, queues and schedules

js의 타이머는 실행시간을 보장하지 못한다

자바스크립트에서 타이머가 동작하는 방식을 정리해보았다.
자바스크립트의 싱글스레드 이해해도 많은 도움이 되는 것 같다.

타이머 함수

자바스크립트 타이머 관련 함수는 3 가지가 있다.
하지만 자바스크립트는 해당 delay의 정확한 시간을 보장하지 않는다.

또한 타이머는 외부의 웹 api로 브라우저에서 제공한다.

1
2
3
4
5
6
let id = setTimeout(fn, delay);
// delay 만큼의 지연 후 fn을 호출한다. 나중에 취소할 수 있는 고유 ID를 반환한다.
let id = setInteral(fn, delay);
// setTimeout과 유사하지만 delay 마다 fn을 계속 호출한다.
clearInterval(id);
// id에 해당하는 타이머의 콜백을 발생하지 않도록 한다.

싱글스레드로 인한 실행예시

이는 자바스크립트가 싱글스레드로 동작하기 때문이다.
이는 한 번에 하나의 코드블럭만 실행할 수 있다는 것을 의미한다.

이는 실행 가능할 때(빈 자리가 있을 때)만 실행된다.

아래의 그림은 이를 설명하는 그림이다.

call stack

위에서부터 실행된다.
첫 번째 자바스크립트 블록은 18ms동안 실행되며 마우스 클릭 블록은 11ms동안 실행된다.

첫 번째 자바스크립트 블럭의 실행순서는 다음과 같다.

  1. setTimeout 타이머 설정
  2. 마우스 클릭 이벤트 발생
  3. setInterval 타이머 설정
  4. setTimeout 실행

하지만 1,2는 비동기 이벤트이고 현재 자바스크립트 블럭의 실행이 끝나지 않았으므로 즉시 실행되지 않고 큐에 넣어서 대기한다.

자바스크립트 블록의 실행이 끝났을 경우 내부에선 대기중인 큐를 확인한다.
이 과정이 what's waiting이다.
확인에도 시간이 소요된다.

그 다음 큐에서 클릭 핸들러를 가져와 실행하여 Mouse Click Callback 블럭이 생긴다.

Mouse Click Callback 실행중에
setInterval이 발생하여 대기중인 큐에 넣는다.

Mouse Click Callback의 실행이 끝나게 되면,
내부에서 다시 큐를 확인하여 setTimeout을 실행한다.

setTimeout의 실행 중에 setInterval이 발생하지만 이미 큐에는 setInterval이 대기중이므로 이는 무시된다.

그 후 setTimeout의 실행이 끝난 뒤에 발생한 setInterval은 what's Waiting? 없이 즉시 실행된다.(실행중인 블록과 대기중인 큐에 아무 것도 없기 때문이다.)

정리

  1. 자바스크립트 엔진은 싱글스레드로 동작하여 비동기 이벤트를 큐에 넣는다.
  2. 타이머 함수는 웹 api로 브라우저에서 제공하여 외부에서 delay마다 큐에 넣는다.
  3. setInterval은 마지막 콜백에 상관없이 delay마다 웹API에서 실행을 시도한다. 이미 큐에 있을 경우 무시된다.
  4. setTimeout은 웹 API에서 delay마다 실행을 시도하지만 실행되고 있는 콜백이 있을 경우 해당 콜백의 실행이 끝날 때까지 큐에서 대기한 후 실행을 시도한다. 때문에 delay만큼의 시간을 보장할 수 없다.

ref

How JavaScript Timers Work
자바스크립트 타이머는 지각쟁이? 그 이유는 싱글 콜 스택?