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

requestAnimationFrame 메소드

주기적으로 실행되는 애니메이션을 window.requestAnimationFrame()으로 최적화 해보자

requestAnimationFrame(callback)

브라우저에게 수행하기를 원하는 애니메이션을 알리고 다음 리페인트가 진행되기 전에 해당 애니메이션을 업데이트 하는 함수를 호출하게 한다. 이 메소드는 리페인트 이전에 실행할 함수를 인자로 받는다.

  • 다음 리페인트에서 그 다음 프레임을 애니메이트 하려면 콜백 루틴이 반드시 스스로 requestAnimationFrame()을 호출해야 한다.

대부분의 브라우저에서 W3C 권장사항에 따라 디스플레이 주사율만큼 호출되게 된다.

callback

파라미터인 callback은 다음 리페인트를 위한 애니메이션을 업데이트 할 때 호출할 함수이다. 콜백함수에는 requestAnimationFrame()이 콜백함수 실행을 시작할 때의 시점을 나타내는 DOMHighResTimeStamp() 단일 인자를 전달한다.

반환 값

requestAnimationFrame()을 취소할 수 있는 고유 id인 long 정수값이 반환된다.
window.cancelAnimationFrame()함수로 전달하여 취소할 수 있다.

차이점

setInterval과는 다음과 같은 차이점이 있다.

주사율만큼의 interval

setInterval을 사용하여 구현할 경우 interval을 손수 설정해주어야 한다.
requestAnimationFrame은 주사율만큼의 Interval을 가지게 된다.(설정해줄 필요가 없다.)

동시 실행

여러개의 setInterval을 사용할 경우 콜백이 겹쳐져서 버벅임이 발생할 수 있다. requestAnimationFrame을 사용할 경우 계속 실행하기 위해선 내부 callback에서 반드시 재호출해야 하므로 여러개의 애니메이션을 써도 버벅이지 않는다.

비동기

js-que

위의 동시실행과 같은 이야기지만 setTimeout, setInterval은 마이크로 태스크큐에서 작동한다. 하지만 requestAnimationFrame은 Animation Frame에서 동작한다. 때문에 setInterval과 달리 callback이 유실될 가능성이 없다.

예시

1
2
3
4
5
6
7
8
9
10
11
12
let start = null;
let hi = 0;
function callback(timestamp) {
if (!start) start = timestamp;
console.log("cnt", hi++);

if (timestamp - start <= 1000) {
window.requestAnimationFrame(callback);
}
}
window.requestAnimationFrame(callback);
// 61번 출력되었다.

Ref

window.requestAnimationFrame()

여러개의 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

함수형 프로그래밍 5 코드를 값으로 다루기

go

첫 번째 parameter를 두 번째 parameter로 전달한다.
이를 마지막 parameter까지 반복하는 함수이다.
즉 이전 함수의 실행결과를 다음 함수의 인자로 전달하는 함수이다.
값을 반환한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);
// 111

고차함수가 한 번에 이해되지 않아서 콘솔로 실행 순서를 추적해보았다.

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
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
console.log(f, acc, a);
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

go(
0,
(a) => a + 1,
(a) => a + 10,
(a) => a + 100,
console.log
);

// (a, f) => f(a) 0 (a) => a + 1
// (a, f) => f(a) 1 (a) => a + 10
// (a, f) => f(a) 11 (a) => a + 100
// (a, f) => f(a) 111 ƒ log() { [native code] }
// 111

실행 순서는 다음과 같다.

  1. reduce에 콜백함수 c (a,f)=>f(a)와 인자배열을 전달하는 함수인 go를 만든다.
  2. reduce로 전달된 인자가 2 개밖에 없으므로 args[0]이 acc가 된다. 이 때 args는 콜백함수만 존재하는 이터러블이 된다.
  3. args[0]은 이미 평가되었으므로 args[1]부터 args를 순회하며 acc에 c에 args 이러터블의 value를 전달한 값을 저장한다. 즉 acc에는 (acc,args[Symbol.iterator]().next().value)=>args[Symbol.iterator]().next().value(a)가 실행되어 저장된다.
  4. 모든 콜백함수의 실행을 마친 후 acc를 반환한다.

pipe

go와 같은 역할을 하지만 값이 아닌 함수를 반환한다.

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
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);

const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);

console.log(f(0, 1));
///111

마찬가지로 콘솔로 실행순서를 추적해보았다.

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
const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const go = (...args) => reduce((a, f) => f(a), args);

const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);

const f = pipe(
(a, b) => a + b,
(a) => a + 10,
(a) => a + 100
);

console.log(f);

// (...as) =>
// go(f(...as), ...fs)

console.log(f(0, 1));
///111

실행순서는 다음과 같다.

  1. pipe에 함수를 평가한 값을 parameter로 받도록 하기위해 args의 첫 번째 함수를 따로 분리한다.
1
2
3
4
const pipe =
(f, ...fs) =>
(...as) =>
go(f(...as), ...fs);
  1. f에 pipe와 콜백을 전달한다. f엔 다음과 같은 함수객체가 저장된다.
    f의 실행컨텍스트엔 pipe에 저장된 첫 번째 함수가 있다.
1
f = (...as) => go(f(...as), ...fs);
  1. f에 0,1을 전달한다. f(0,1)은 add(0,1)이므로 1로 평가되어 전달된다.
    즉 acc의 초기값은 1이 된다.
1
f = (0,1) => go(f(0,1), ...fs);
  1. go 내부에서 acc를 계산한후 반환한다.
1
2
console.log(f(0, 1));
// 111

curry

함수를 parameter로 받아서 원하는 개수만큼의 parameter가 들어왔을 때 평가시켜 반환하는 함수이다.
코드의 재사용성을 높여준다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const curry =
(f) =>
(a, ..._) =>
_.length ? f(a, ..._) : (..._) => f(a, ..._);
//parameter가 2개 이상이라면 바로 실행하고 두 개보다 작다면 이후 parameter를 받아서 실행하는 함수이다.

const mult = curry((a, b) => a * b);
console.log(mult);
//(a, ..._) => _.length ? f(a, ..._) : (..._) => f(a, ..._)
console.log(mult(1));
// (..._) => f(a, ..._)
console.log(mult(1)(2));
// 2

const mult3 = mult(3);
console.log(mult3(10));
// 30
console.log(mult3(5));
// 15
console.log(mult3(3));
// 9

go 응용하기

다음의 코드는 아래와 같이 go를 이용해서 다시 작성할 수 있다.

1
2
3
4
5
6
7
8
9
console.log(
reduce(
add,
map(
(p) => p.price,
filter((p) => p.price < 20000, products)
)
)
);

다음 파라미터의 인자로 이전 파라미터의 실행값이 전달되는 go를 이용해서 다시 작성하였다.

1
2
3
4
5
6
7
go(
products,
(products) => filter((p) => p.price < 20000, products),
(products) => map((p) => p.price, products),
(prices) => reduce(add, prices),
console.log
);

filter,reduce,map에 모두 curry를 적용했을 경우엔 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
8
9
10
go(
products,
filter((p) => p.price < 20000),
// filter((p) => p.price < 20000)(products) 가 되고 filter에 2개의 parameter가 전해져서 filter가 실행된다.
map((p) => p.price),
reduce(add),
console.log
);

// 파라미터가 전달되기 때문에 생략하여도 가능하다.

함수 조합으로 함수 만들기

pipe를 이용하여 중복을 제거할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
log
);

go(
products,
filter((p) => p.price < 20000),
map((p) => p.price),
reduce(add),
log
);

같은부분을 함수로 작성하여 중복을 제거하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const total_price = pipe(
map((p) => p.price),
reduce(add)
);

const base_total_price = (predi) => pipe(filter(predi), total_price);

go(
products,
base_total_price((p) => p.price < 20000),
console.log
);

go(
products,
base_total_price((p) => p.price >= 20000),
console.log
);

Ref

함수형 프로그래밍과 JavaScript ES6+

함수형 프로그래밍 4 map,filter,reduce

map

아래의 함수는 이터러블에 사용 가능하게 작성된 map 함수이다.

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 products = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];

const map = (f, iter) => {
// f를 통해 어떤 값을 반환할지 정한다.
let res = [];

for (const p of iter) {
res.push(f(p));
}

return res;
};

console.log(map((p) => p.name, products));
// ['반팔티','긴팔티','핸드폰케이스','후드티','바지']

console.log(map((p) => p.price, products));
// [15000,20000,15000,30000,25000]

filter

아래의 함수는 이터러블에 사용 가능하게 작성된 filter 함수이다.

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
const products = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];

const filter = (f, iter) => {
let res = [];
for (const a of iter) {
if (f(a)) res.push(a);
}
return res;
};

log(...filter((p) => p.price < 20000, products));
// [{name:'반팔티',price:15000},,{name:'핸드폰케이스',price:15000}]

console.log(filter((p) => p.price >= 20000, products));
// [{name:'긴팔티',price:20000},{name:'후드티',price:30000},{name:'바지',price:25000}]

log(filter((n) => n % 2, [1, 2, 3, 4]));
// [1, 3];

log(
filter(
(n) => n % 2,
(function* () {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
})()
)
);
// [1,3,5]

reduce

아래의 함수는 이터러블에 사용 가능하게 작성된 filter 함수이다.

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
const nums = [1, 2, 3, 4, 5];

const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
// parameter가 2 개만 들어왔을 경우 iter가 없는 경우이다.
// 이 때는 자기 자신을 반환하는 iterable의 성질을 이용해서 iter를 만들어준다.
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

const add = (a, b) => a + b;

console.log(reduce(add, 0, [1, 2, 3, 4, 5]));
// 15

console.log(add(add(add(add(add(0, 1), 2), 3), 4), 5));
// 15

console.log(reduce(add, [1, 2, 3, 4, 5]));
// == console.log(reduce(add, 1, [2, 3, 4, 5]))
// 15

단순 숫자 계산이 아니여도 활용할 수 있다.
아래는 products배열의 price를 더하는 코드이다.(다형성)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const products = [
{ name: "반팔티", price: 15000 },
{ name: "긴팔티", price: 20000 },
{ name: "핸드폰케이스", price: 15000 },
{ name: "후드티", price: 30000 },
{ name: "바지", price: 25000 },
];

const reduce = (f, acc, iter) => {
if (!iter) {
iter = acc[Symbol.iterator]();
acc = iter.next().value;
}
for (const a of iter) {
acc = f(acc, a);
}
return acc;
};

console.log(
reduce((total_price, product) => total_price + product.price, 0, products)
);
// 105000

다형성

위에서 작성한 map,filter,reduce함수는 이터러블 프로토콜을 따르는 객체에 전부 적용할 수 있다.
관련된 연산자도 사용할 수 있다.

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 map = (f, iter) => {
let res = [];
for (const p of iter) {
res.push(f(p));
}
return res;
};
console.log(document.querySelectorAll("*").map((ele) => ele.nodeName));
// 배열이 아니기 때문에 에러가 발생한다.
console.log(map((el) => el.nodeName, document.querySelectorAll("*")));
// nodeList는 이터러블 프로토콜을 따르기 때문에 동작한다.

function* gen() {
yield 2;
yield 3;
yield 4;
}

console.log(
map((a) => a * a),
gen()
);
// 제너레이터에도 동작한다
// [4,9,16]

제너레이터 또한 이터러블 프로토콜을 따르기 때문에 제너레이터에도 적용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const map = (f, iter) => {
let res = [];
for (const p of iter) {
res.push(f(p));
}
return res;
};

function* gen() {
yield 2;
yield 3;
yield 4;
}

console.log(
map((a) => a * a),
gen()
);
// 제너레이터에도 동작한다
// [4,9,16]

마찬가지로 map과 set에도 적용할 수 있다.

1
2
3
4
5
let m = new Map();
m.set("a", 10);
m.set("b", 20);
console.log(new Map(map(([k, a]) => [k, a * 2], m)));
// [[a,20],[b,40]]

Ref

함수형 프로그래밍과 JavaScript ES6+

함수형 프로그래밍 3 generator와 iterator

함수형 프로그래밍의 필수개념인 generator에 대해 알아보았다.

generator

generator는 iterator이자 iterable을 생성하는 함수이다.

  • 함수를 선언할 때 함수명 앞에 *를 붙여 만든다
  • yield를 통해 iterator를 반환한다.
  • generator를 이용하며 어떤 값이든 순회할 수 있는 형태로 작성할 수 있다.
  • generator 내부 반환되는 값은 done:true일 때의 value로 반환된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function* gen() {
yield 1;
if (false) yield 4;
// 출력이 이루어지지 않는다.
yield 2;
yield 3;
return 100;
// 반환값은 순회할 경우 출력되지 않는다. done이 true이기 때문이다.
}

let iter = gen();
console.log(iter[Symbol.iterator]() == iter); // true
console.log(iter.next()); // {value:1,done:false}
console.log(iter.next()); // {value:2,done:false}
console.log(iter.next()); // {value:3,done:false}
console.log(iter.next()); // {value:100,done:true}

for (const a of gen()) console.log(a);
// 1
// 2
// 3

generator를 작성하는 여러가지 방법

아래의 oods 함수는 홀수를 value로 하는 iterable을 반환하는 generator이다.

1
2
3
4
5
6
7
8
9
10
11
function* oods() {
yield 1;
yield 3;
yield 5;
}

let iter2 = oods();
console.log(iter2.next()); // {value:1,done:false}
console.log(iter2.next()); // {value:3,done:false}
console.log(iter2.next()); // {value:5,done:false}
console.log(iter2.next()); // {value:undefined,done:true}

generator는 for문을 이용하여 작성할 수 있다.

1
2
3
4
5
6
7
8
9
10
function* oods(l) {
for (let i = 0; i < l; i++) {
if (i % 2) yield i;
}
}
let iter2 = oods(6);
console.log(iter2.next()); // {value:1,done:false}
console.log(iter2.next()); // {value:3,done:false}
console.log(iter2.next()); // {value:5,done:false}
console.log(iter2.next()); // {value:undefined,done:true}

또한 generator는 while을 이용하여 작성할 수 있다.

아래의 infinity는 무한수열을 만드는 generator이고 limit 함수는 l까지는 a를 yield하다가 l이 될 경우 return 하는 generator이다.
infinity generator와 limit generator를 이용하여 odds generator도 새로 정의하였다.

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
function* infinity(i = 0) {
while (true) yield i++;
}

function* limit(l, iter) {
for (const a of iter) {
yield a;
if (a == l) return;
}
}

function* oods(l) {
for (const a of limit(l, infinity(1))) {
if (a % 2) yield a;
}
}

let iter4 = oods(6);
iter3.next(); // {value:1,done:false}
iter3.next(); // {value:3,done:false}
iter3.next(); // {value:5,done:false}
iter3.next(); // {value:undefined,done:true}

for (const a of oods(6)) console.log(a);
// {value:1,done:false}
// {value:3,done:false}
// {value:5,done:false}
// {value:undefined,done:true}

iterable/iterator 프로토콜

generator는 iterable/iterator 프로토콜을 따르고 있기 떄문에 iterable/iterator 프로토콜을 따르는 여러 연산이나 함수들을 적용할 수 있다.

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
function* infinity(i = 0) {
while (true) yield i++;
}

function* limit(l, iter) {
for (const a of iter) {
yield a;
if (a == l) return;
}
}

function* oods(l) {
for (const a of limit(l, infinity(1))) {
if (a % 2) yield a;
}
}

console.log(...odds(10));
// 1 3 5 7 9
console.log([...oods(10), ...oods(20)]);
// 1 3 5 7 9 1 3 5 7 9 11 13 15 17 19

const [head, ...tail] = oods(5);
console.log(head);
// 1
console.log(tail);
// 3 5

const [a, b, ...rest] = oods(10);
console.log(a);
// 1
console.log(b);
// 3
console.log(rest);
// 5 7 9

Ref

함수형 프로그래밍과 JavaScript ES6+

함수형 프로그래밍 2 Iterator 프로토콜

리스트를 순회하는 방법엔 여러방법과 Iterator에 대해 알아보려고 한다.

for i++

ES6 이전에는 for문을 통하여 리스트를 순회하였다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 배열 순회
const list = [1, 2, 3];
for (var i = 0; i < list.length; i++) {
console.log(list[i]);
}
// 유사 배열 순회
const str = "abc";
for (var i = 0; i < str.length; i++) {
console.log(str[i]);
}

// 1
// 2
// 3
// a
// b
// c

for of

ES6에선 for of문이 추가되어 좀 더 명령형에서 선언형으로 바뀌었다.
인덱스로 직접 접근할 수 없는 리스트도 순회할 수 있다.

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
//배열 순회
const list = [1, 2, 3];
for (const a of list) {
console.log(a);
}
//유사배열 순회
const str = "abc";
for (const a of str) {
console.log(a);
}

// 1
// 2
// 3
// a
// b
// c

const set = new Set([1, 2, 3]);
for (const a of set) console.log(a);
// 1
// 2
// 3

const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
for (const a of map) console.log(a);
// ['a',1]
// ['1',2]
// ['1',3]

Iterable/Iterator

for of는 Iterable을 이용하여 구현 되어있어 동작원리를 이해하기 위해선 Iterable에 대해 먼저 이해해야한다.

Iterable

실행시에 Iterator를 리턴하는 함수인 Symbol.iterator 를 가진 값이다.

Iterable이 되기 위해선 다음과 같은 조건을 만족해야한다.

  • 객체 내에 [Symbol.iterator] 메서드가 존재해야 한다.
  • [Symbol.iterator] 메서드는 Iterator 객체를 반환해야 한다.

Iterator

{value,done} 객체를 리턴하는 next()를 가진 값이다.

  • 객체 내에 next 메서드가 존재해야 한다.
  • next 메서드는 IteratorResult 객체를 반환해야 한다.
  • IteratorResult 객체는 done: boolean과 value: any 프로퍼티를 - 가진다.
  • 이전 next 메서드 호출의 결과로 done 값이 true를 리턴했다면, 이후 호출에 대한 done 값도 true여야 한다.

Well-formed iterable

Iterator면서 Iterable인 객체를 Well-formed iterable이라고 한다.
즉 이터레이터의 이터러블이 자기 자신을 반환하면 Well-formed iterable이다.

Iterable/Iterator 프로토콜

Iterable을 for…of, 전개 연산자 등과 함께 동작하도록 한 규약이다.

for of의 동작원리

  1. for of는 Iterable 객체에 사용할 수 있다.
  2. for of를 호출하게 되면 Symbol.iterator를 통해 Iterator를 반환한다.
  3. 즉 for of는 Iterable/Iterator 프로토콜에 따라 동작한다.
1
2
3
4
5
6
7
const list = [1, 2, 3];
for (const a of list) {
// for of는 Iterable/Iterator 프로토콜에 따라 동작한다.
// 즉 반환된 Iterator의 value를 a로 참조한다.
// done이 true가 되면 반복문을 빠져나온다.
console.log(a);
}

따라서 코드를 다음과 같이 수정할 수 있다.

1
2
3
4
5
6
7
let list = [1, 2, 3];
let iterator = list[Symbol.iterator]();
while (1) {
const { value, done } = iterator.next();
if (done === true) break;
console.log(value);
}

for of 응용하기

Map과 Set에는 Iterator를 반환하는 여러 메서드가 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);
for (const a of map.keys()) console.log(a);
// Map,Set의 key를 value로 하는 Iterator를 반환하는 메서드이다.
// a
// b
// c
for (const a of map.values()) console.log(a);
// Map,Set의 value를 value로 하는 Iterator를 반환하는 메서드이다.
// 1
// 2
// 3
for (const a of map.entries()) console.log(a);
// Map,Set 전체를 value로 하는 Iterator를 반환하는 메서드이다.
// ['a',1]
// ['1',2]
// ['1',3]

사용자 정의 Iterable

위에서 언급한 조건을 준수하는 Iterable 객체를 정의하여 사용할 수 있다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? { done: true } : { value: i--, done: false };
},
};
},
};

let iterator = iterable[Symbol.iterator]();
console.log(iterator.next()); // {value: 3, done: false}
console.log(iterator.next()); // {value: 2, done: false}
console.log(iterator.next()); // {value: 1, done: false}
console.log(iterator.next()); // {done:true}

for (const a of iterable) console.log(a);

// Uncaught TypeError: iterator is not iterable
// Well-formed Iterable이여야 에러가 발생하지 않는다.

Well-formed Iterable을 정의하려면 자기 자신을 반환하는 iterator를 추가하면 된다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const iterable = {
[Symbol.iterator]() {
let i = 3;
return {
next() {
return i == 0 ? { done: true } : { value: i--, done: false };
},
[Symbol.iterator]() {
return this;
},
};
},
};

for (const a of iterable) console.log(a);

// Well-formed iterator여야 에러가 발생하지 않는다.

전개 연산자

전개연산자도 Iterable 프로토콜을 따르고 있다.

1
2
3
4
5

const a=[1,2=];
console.log([...a,...[3,4]]) ///[1,2,3,4];
a[Symbol.iterator]=null;
console.log([...a,...[3,4]]) // Uncaught TypeError: a is not iterable

Iterable 프로토콜을 따르고 있기 때문에 Map,Set과 같은 여러 Iterable객체에 적용된다.

1
2
3
4
5
6
7
8
9
const set = new Set([1, 2, 3]);
const map = new Map([
["a", 1],
["b", 2],
["c", 3],
]);

console.log([...set, ...map]);
// [1,2,3,["a", 1],["b", 2],["c", 3]]

Ref

[Javascript] Iterable과 Iterator 이해하기

함수형 프로그래밍과 JavaScript ES6+

함수형 프로그래밍 1 기본개념

함수형 프로그래밍의 기본 개념에 대해 정리해보았다.

평가

  • 코드가 계산(Evaluation) 되어 값을 만드는 것

일급

  • 값으로 다룰 수 있다.
  • 변수에 담을 수 있다.
  • 함수의 인자로 활용될 수 있다.
  • 함수의 결과로 사용될 수 있다.
1
2
3
4
5
const a = 10;
const add10 = (a) => a + 10;
const r = add10(a);
console.log(r);
// 20

일급 함수

  • 함수를 값으로 다룰 수 있다.
  • 조합성과 추상화의 도구
1
2
3
4
5
6
7
8
9
10
const add5 = (a) => a + 5;
console.log(add5); // (a) => a + 5;
console.log(add5(5)); //10

const f1 = () => () => 1;
console.log(f1); //()=>1;

const f2 = f1();
console.log(f2); //()=>1;
console.log(f2()); //1

고차함수

  • 함수를 값으로 다루는 함수

함수를 인자로 받아서 실행하는 함수(Applicative Programming)

고차함수의 두 가지 유형중 하나 함수를 인자로 받아서 실행하는 함수이다.

apply1

apply1은 함수를 인자로 받아서 1을 넣은 결과값을 리턴해주는 함수이다.

1
2
3
4
5
6
const apply1 = (f) => f(1);
// 함수를 parameter로 다루므로 고차함수
const add2 = (a) => a + 2;

console.log(apply(add2)); //3;
console.log(apply((a) => a - 1)); //0;

times

times는 함수를 인자로 받아서 n만큼 반복하여 실행하는 함수이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const times = (f, n) => {
let i = -1;
while (++i < n) f(i);
};

times(console.log, 3);

// 0
// 1
// 2

times((a) => console.log(a + 10), 3);
// 10
// 11
// 12

함수를 만들어 리턴하는 함수 (클로저를 만들어 리턴하는 함수)

  • addMaker

addMaker는 두 수를 더하는 함수를 만들어서 반환하는 함수이다.
자바스크립트에서 함수객체는 함수가 만들어질 떄의 환경을 가지고 있기 때문에 클로저를 가지고 있다.

1
2
3
4
5
const addMaker = (a) => (b) => a + b;
const add10 = addMaker(10);
console.log(add10); // b => a + b;
console.log(add10(5)); // 15
console.log(add10(10)); // 20

Ref

함수형 프로그래밍과 JavaScript ES6+

인간 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=()=>{};