
자바스크립트를 처음 배울 때, 표준 내장 객체를 이용해 코드를 간단히 작성할 수 있다는 점이 매우 신기했다. 반복문이나 조건문을 통해 힘들게 로직을 구성하지 않아도 데이터를 원하는 대로 가공할 수 있었기 때문이다. 마치 마법 같았다. 하지만 콜백 함수를 넘겨주는 방식에 익숙하지 않고, 메소드마다 반환값이 달라서 생소하고 어려운 것도 사실이었다. 그래도 오늘 수업을 통해 조금 더 익숙해 졌다. 특히 메소드 체이닝을 많이 연습할 수 있어서 유익한 시간이었다. 또, reduce를 이용한 연습 문제 풀이가 마음에 든다.
//6. 남학생들의 평균 연령 구하기
const maleTotalInfo = students.reduce(
(accumulator, currentValue) => {
if (currentValue.gender === 'male') {
accumulator.maleTotalAge += currentValue.age;
accumulator.maleTotalNum += 1;
}
return accumulator;
},
{ maleTotalAge: 0, maleTotalNum: 0 }
);
const maleAverageAge = maleTotalInfo.maleTotalAge / maleTotalInfo.maleTotalNum;
console.log(maleAverageAge);
비동기를 처리하는 방법으로는 콜백 함수, Promise, async/await가 있다는 사실을 알고 있었지만, 자세히 설명할 자신은 없었다. 특히 Promise는 다시 공부해야겠다는 생각이 들 정도로 어려웠다. 사실 오늘도 완벽히 이해했다고 말하기는 어려울 것 같다. 집에 돌아오는 지하철에서 관련 동영상을 여러 개 보았지만, 여전히 부족한 느낌이었다. 대신, 조금 더 익숙해진 것은 사실이다. 이제 콜백 지옥에 대해서는 코드 예시를 통해 설명할 수 있을 것 같다. 오늘 특히 좋았던 부분은 콜백 지옥 코드를 Promise와 async/await를 이용해서 개선해보는 시간이었다.
function Shape(color) {
this.color = color;
this.getColor = function () {
return `이 도형의 색상은 ${this.color}입니다.`;
};
}
const shape1 = new Shape('red');
function Rectangle(color, width, height) {
Shape.call(this, color); // Shape를 상속받게 하기
this.color = color;
this.width = width;
this.height = height;
this.getArea = function () {
return this.width * this.height;
};
}
const rect1 = new Rectangle('blue', 20, 20);
console.log(shape1);
class Shape {
constructor(color) {
this.color = color;
}
getColor() {
return `이 도형의 색상은 ${this.color}입니다.`;
}
}
const shape1 = new Shape('red');
class Rectangle extends Shape { // Shape 상속
constructor(color, width, height) {
super(color);
this.width = width;
this.height = height;
}
getArea() {
return this.width * this.height;
}
}
const rect1 = new Rectangle('blue', 20, 20);
console.log(shape1);
console.dir(rect1);
shap1 객체의 메소드 getColor가 프로토타입 내부에 존재함을 알 수 있음rect1 객체는 Shape를 상속 받아서 프로토타입의 프로토타입의 constructor가 class Shape이며 getColor도 여기 존재함을 알 수 있음<콘솔에 출력된 결과 (생상자 함수shape1 / 클래스 shape1 / 클래스 rect1)>
![]() | ![]() | ![]() |
|---|
Setter 메서드는 클래스의 특정 속성에 값을 설정할 때 사용자 정의 검사를 수행할 수 있게 함
Car 클래스에서 speed 속성을 설정할 때 음수 값을 허용하지 않도록 검사하는 setter를 정의함class Car {
constructor(speed) {
this.speed = speed;
}
set speed(speed) {
if (speed < 0) {
throw new Error('속도는 음수가 될 수 없습니다.');
}
this._speed = speed; // 내부 속성에 저장
}
getSpeed() {
return `현재 속도는 ${this._speed}입니다.`;
}
}
const car1 = new Car(100);
console.log(car1.getSpeed()); // 현재 속도는 100입니다.
this.speed = speed를 this._speed = speed로 변경하여 이를 해결함
Getter 메서드는 속성 값을 참조할 때 사용됨
Car 클래스의 예제에서는 speed 속성을 안전하게 참조할 수 있도록 getter를 추가함class Car {
constructor(speed) {
this.speed = speed;
}
set speed(speed) {
if (speed < 0) {
throw new Error('속도는 음수가 될 수 없습니다.');
}
this._speed = speed;
}
get speed() {
return this._speed;
}
getSpeed() {
return `현재 속도는 ${this.speed}입니다.`;
}
}
const car1 = new Car(100);
console.log(car1.getSpeed()); // 현재 속도는 100입니다.
클래스 내에서만 접근 가능한 속성을 정의할 때는 # 기호를 사용하여 private 속성을 만듦, 따라서 setter와 양립할 수 없음
class Car {
#name; // private 속성
constructor(name, speed) {
this.#name = name;
this.speed = speed;
}
set speed(speed) {
if (speed < 0) {
throw new Error('속도는 음수가 될 수 없습니다.');
}
this._speed = speed;
}
get speed() {
return this._speed;
}
get name() {
return this.#name; // private 속성 접근에 이런 식으로 접근 해도 값을 바꿀 수 없음
}
getCarName() {
return `차 이름은 ${this.#name}입니다.`;
}
get getSpeed() {
return `현재 속도는 ${this.speed}입니다.`;
}
}
const car1 = new Car('벤츠', 100);
car1.name = '아우디'; // private 속성은 외부에서 변경되지 않음
console.log(car1.getCarName()); // 차 이름은 벤츠입니다.
정적 메서드와 속성은 클래스 자체에 속하며 멤버 속성이나 프로토타입 속성에 포함되지 않으므로 인스턴스가 아닌 클래스 이름으로 호출해야 함
class Car {
#name; // private 속성
static CREATED = '2022';
constructor(name, speed) {
this.#name = name;
this.speed = speed;
}
set speed(speed) {
if (speed < 0) {
throw new Error('속도는 음수가 될 수 없습니다.');
}
this._speed = speed;
}
get speed() {
return this._speed;
}
get name() {
return this.#name;
}
getCarName() {
return `차 이름은 ${this.#name}입니다.`;
}
get getSpeed() {
return `현재 속도는 ${this.speed}입니다.`;
}
// 정적 메서드
static getSpec() {
return `차는 타이어 4개와 문 4개가 있습니다.`;
}
}
const car1 = new Car('벤츠', 100);
console.log(Car.getSpec()); // 차는 타이어 4개와 문 4개가 있습니다.
console.log(Car.CREATED); // 2022
console.log(car1.getSpec());
Math 객체에는 정적 메서드와 속성만이 포함되어 있음console.log(Math.PI); // Math 객체의 정적 속성
console.log(Math.max(1, 2, 3)); // Math 객체의 정적 메서드
자바스크립트 엔진에 기본으로 내장되어 있는 객체들은 자바스크립트 엔진이 상시적으로 제공하는 기능으로, 어디서든 활용할 수 있음
자바스크립트에서 데이터를 생성하는 두 가지 주요 방법: 리터럴 표기법과 생성자 함수
const str = new String('Hello');const str = "Hello";리터럴 표기법으로 작성된 데이터가 프로토타입 객체를 사용할 수 있는 이유는 자바스크립트 엔진이 일시적으로 인스턴스 객체처럼 래핑(wrapping)하기 때문임
const str = new String('Hello');
console.dir(str);
const str2 = 'Hello';
console.dir(str2);
str2는 기본 타입으로 저장되지만, 메서드를 호출할 때 임시로 객체로 변환됨리터럴 표기법
const str = "Hello";
생성자 함수
const str = new String('Hello');
// 리터럴 표기법
const str1 = "Hello";
console.log(str1.length); // 5
console.log(str1.toUpperCase()); // "HELLO"
// 생성자 함수
const str2 = new String('Hello');
console.log(str2.length); // 5
console.log(str2.toUpperCase()); // "HELLO"
numObject는 객체인데 console.log(numLiteral + numObject);가 왜 20일까?let numLiteral = 10;
let numObject = new Number(10);
console.log(numLiteral + numObject); // 20
console.log(numLiteral == numObject); // true
자바스크립트의 타입 변환 (Type Conversion)
자바스크립트는 피연산자가 서로 다른 타입일 때 자동으로 타입을 변환하는 능력이 있음
이 과정은 "암묵적 타입 변환" 또는 "타입 강제 변환"이라고 부름
객체에서 기본 값으로의 변환:
valueOf 메서드나 toString 메서드를 호출함Number 객체의 경우, valueOf 메서드가 기본 숫자 값을 반환함산술 연산:
+, -, *, /)을 수행할 때, 자바스크립트는 피연산자를 숫자로 변환하려고 시도함비교 연산:
== 연산자는 두 값을 비교할 때 타입을 강제로 변환함. numObject는 객체이지만, 비교를 위해 valueOf 메서드를 호출하여 기본 값인 10을 얻음10 == 10이 되어 true를 반환함참고:
===연산자는 타입을 변환하지 않고 비교하기 때문에,numLiteral === numObject는false가 됨console.log(numLiteral === numObject); // false
기본 타입(Primitive Types):
객체 타입(Object Types):
문자열의 경우:
String 객체로, 힙에 저장됨. String 객체는 래퍼 객체로, 문자열 값을 래핑하고 있으며, 추가 메서드와 프로퍼티를 제공함let strLiteral = "Hello"; // 기본 타입 문자열, 스택에 저장
let strObject = new String("Hello"); // String 객체, 힙에 저장
strLiteral은 기본 타입 문자열로, 스택에 값이 저장됨strObject는 String 객체로, 힙에 객체가 저장되며, 스택에는 힙에 있는 객체를 가리키는 참조(reference)가 저장됨Memory Management in JavaScript
Memory management

인스턴스 메서드를 호출했을 때, 원본 데이터가 변경되는 메서드
const arr = [1, 2, 3];
arr.push(4); // [1, 2, 3, 4]
arr.unshift(0); // [0, 1, 2, 3, 4]
arr.pop(); // [0, 1, 2, 3]
arr.shift(); // [1, 2, 3]
arr.splice(1, 1); // [1, 3] (index 1부터 1개 요소 제거)
arr.reverse(); // [3, 1]
arr.sort(); // [1, 3]
push: 배열의 끝에 요소를 추가unshift: 배열의 시작에 요소를 추가pop: 배열의 끝 요소를 제거하고 반환shift: 배열의 첫 요소를 제거하고 반환splice: 배열의 특정 위치에 요소를 추가하거나 제거reverse: 배열의 요소 순서를 반대로 변경sort: 배열의 요소를 정렬이 메서드를 호출했을 때, 원본 데이터가 변경되지 않는 메서드
const arr = [1, 2, 3, 4];
const filteredArr = arr.filter(num => num > 2); // [3, 4]
const mappedArr = arr.map(num => num * 2); // [2, 4, 6, 8]
const sum = arr.reduce((acc, num) => acc + num, 0); // 10
const concatenatedArr = arr.concat([5, 6]); // [1, 2, 3, 4, 5, 6]
const slicedArr = arr.slice(1, 3); // [2, 3]
const hasSome = arr.some(num => num > 2); // true
const allAboveZero = arr.every(num => num > 0); // true
const found = arr.find(num => num === 3); // 3
const foundIndex = arr.findIndex(num => num === 3); // 2
filter: 조건에 맞는 요소들로 새로운 배열을 만듦map: 모든 요소에 대해 주어진 함수를 호출한 결과로 새로운 배열을 만듦reduce: 배열을 순회하며 누산기(accumulator)를 사용해 값을 하나로 줄임concat: 두 배열을 합쳐서 새로운 배열을 만듦slice: 배열의 일부분을 잘라내어 새로운 배열을 만듦some: 조건에 맞는 요소가 하나라도 있는지 여부를 확인하여 true 또는 false를 반환every: 모든 요소가 조건에 맞는지 여부를 확인하여 true 또는 false를 반환find: 조건에 맞는 첫 번째 요소를 반환findIndex: 조건에 맞는 첫 번째 요소의 인덱스를 반환console.log(Math.random()); // 0과 1 사이의 난수 생성
console.log(Math.max(1, 2, 3)); // 3
console.log(Math.min(1, 2, 3)); // 1
console.log(Math.pow(2, 3)); // 8Math.random(): 0과 1 사이의 난수를 반환Math.max(): 전달된 인수 중 최대값을 반환Math.min(): 전달된 인수 중 최소값을 반환Math.pow(): 제곱 값을 반환자바스크립트는 싱글 스레드 언어로 한 번에 하나의 작업만 처리할 수 있음을 의미하며 동기적으로 실행됨
다른 함수의 매개변수로 전달되어 그 함수가 실행되는 동안 특정 시점에 호출되는 함수
동기 콜백 함수는 즉시 실행되는 콜백 함수를 의미함
function greeting(callbackFn) {
console.log('Hello');
callbackFn();
}
function goodbye() {
console.log('goodbye');
}
greeting(goodbye);
greeting 함수는 goodbye 콜백을 즉시 실행함비동기 콜백 함수는 비동기 작업이 끝난 후 호출되는 콜백 함수를 의미함
function task1(callback) {
setTimeout(() => {
console.log('task1 시작');
callback();
}, 1000);
}
function task2() {
console.log('task2 시작');
}
task1(task2);
task1함수는 task2콜백을 비동기 작업이 끝난 후 실행함비동기 콜백을 연속으로 사용하면 코드가 복잡해지고 가독성이 떨어지는 "콜백 지옥"에 빠질 수 있음
function task1(callback) {
setTimeout(() => {
console.log('task1 시작');
callback();
}, 1000);
}
function task2(callback) {
setTimeout(() => {
console.log('task2 시작');
callback();
}, 1000);
}
function task3(callback) {
setTimeout(() => {
console.log('task3 시작');
callback();
}, 1000);
}
function task4(callback) {
setTimeout(() => {
console.log('task4 시작');
callback();
}, 1000);
}
task1(() => {
task2(() => {
task3(() => {
task4(() => {
console.log('모든 작업 끝');
});
});
});
});
Promise는 비동기 작업을 처리할 수 있도록 도와주는 자바스크립트 내장 객체이며 다음과 같은 세 가지 상태를 가짐:
Promise 객체를 사용하면 비동기 작업을 쉽게 관리할 수 있음 Promise 객체를 생성할 때 전달되는 콜백 함수는 즉시 실행됨resolve 또는 reject 함수를 호출하여 Promise 객체의 상태를 업데이트할 수 있음const promise = new Promise((resolve, reject) => {
console.log('doing something...'); // 콜백 함수에 있는 코드를 즉시 실행함
setTimeout(() => {
resolve('success');
}, 1000);
});
promise
.then((value) => console.log(value)) // 'success' 출력
.catch((error) => console.error(error))
.finally(() => console.log('finally'));
Promise 객체가 생성됨console.log('doing something...')이 출력됨setTimeout의 콜백이 실행되어 resolve('success')가 호출됨resolve가 호출되면 Promise 객체의 상태가 pending에서 fulfilled로 변경됨then 메서드 내부의 콜백 함수가 실행되어 console.log(value)에서 'success'가 출력됨catch 메서드는 호출되지 않으며, finally 메서드가 실행되어 console.log('finally')가 출력됨Promise 객체는 비동기 작업의 결과를 처리하기 위해 then, catch, finally 메서드를 제공함
then 메서드는 Promise가 fulfilled 상태일 때 호출됨resolve 함수의 결과 값을 받는 콜백 함수를 지정함reject 함수의 결과 값을 받는 콜백 함수를 지정할 수도 있음catch 메서드를 사용하는 것이 가독성 측면에서 더 좋음const promise = new Promise((resolve, reject) => {
const isSuccess = true;
setTimeout(() => {
isSuccess ? resolve('success') : reject(new Error('fail'));
});
});
promise
.then(
(value) => console.log(value),
(error) => console.error(error)
)
.catch((error) => console.error(error)) // 만약 에러가 발생해도 catch에서는 에러가 안 걸림
.finally(() => console.log('finally'));
console.log('hello');

catch 메서드는 Promise가 rejected 상태일 때 호출됨reject 함수의 결과 값을 받음const promise = new Promise((resolve, reject) => {
const isSuccess = false;
setTimeout(() => {
isSuccess ? resolve('success') : reject(new Error('fail'));
}, 1000);
});
promise
.then((value) => console.log(value))
.catch((error) => console.error(error))
.finally(() => console.log('finally'));
console.log('hello');

finally 메서드는 Promise가 완료되면 무조건 호출됨then에서 어떤 값을 반환하면 자동으로 resolve가 처리되므로 연속해서 resolve를 처리할 수 있음then()에서 에러가 발생하면 이후의 then은 실행되지 않는 문제가 있음const fetchNumber = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
fetchNumber
.then((num) => new Promise((resolve, reject) => resolve(num * 2))) // 2 promise resolve(2)
.then((num) => num * 3) // 6
.then((num) => num * 2) // 12
.then((num) => console.log(num))
.catch((error) => console.error(error));
catch를 사용하여 에러 이후의 then을 계속 실행할 수 있지만 일반적인 방법은 아님 const fetchNumber = new Promise((resolve, reject) => {
setTimeout(() => {
resolve(1);
}, 1000);
});
fetchNumber
.then((num) => new Promise((resolve, reject) => reject(num))) // 에러 발생
.catch((num) => num) // 에러 처리
.then((num) => num * 3) // 3
.then((num) => num * 2) // 6
.then((num) => console.log(num));
function task1() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('task1 시작');
resolve();
}, 1000);
});
}
function task2() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('task2 시작');
resolve();
}, 1000);
});
}
function task3() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('task3 시작');
resolve();
}, 1000);
});
}
function task4() {
return new Promise((resolve) => {
setTimeout(() => {
console.log('task4 시작');
resolve();
}, 1000);
});
}
task1()
.then(() => task2())
.then(() => task3())
.then(() => task4())
.then(() => {
console.log('모든 작업 끝');
})
.catch((error) => {
console.error('에러 발생:', error);
});
async 키워드를 사용하면 함수가 항상 Promise를 반환함async 함수 내에서 반환된 값은 자동으로 Promise.resolve로 감싸짐async 함수 내에서 발생한 에러는 Promise.reject로 처리됨Promise를 사용하는 코드:const getStarIcon = () =>
new Promise((resolve) => {
resolve('⭐');
});
getStarIcon().then((star) => console.log(star));
getStarIcon 함수는 Promise 객체를 반환하며, then 메서드를 사용하여 resolve된 값을 출력함async function example() {
throw new Error('Something went wrong');
}
example().catch((error) => console.error(error)); // Error: Something went wrong
example 함수는 에러를 던짐async 키워드를 사용하여 동일한 기능을 구현한 코드:const getStarIcon = async () => '⭐'; // 무조건 resolve()
getStarIcon().then((star) => console.log(star));
여기서 getStarIcon 함수는 async 키워드를 사용하여 자동으로 Promise 객체를 반환하며, 반환된 값은 resolve됨. 따라서 동일하게 then 메서드를 사용하여 값을 출력할 수 있음
await 키워드는 async 함수 내부에서만 사용할 수 있음await는 Promise가 처리될 때까지 기다린 다음, Promise가 처리되면 resolve된 값을 반환함const getStarIcon = async () => setTimeout(() => '⭐', 1000); // 값이 즉시 반환되지 않음
aysyc는 Promise를 반환할 뿐 비동기 함수를 기다려 주지는 않음setTimeout함수를 기다려 주지 않음setTimeout함수는 1초 후에 '⭐'을 반환하지만 async가 기다려 주지 않아서 undefined를 즉시 반환함Promise가 처리될 때까지 기다려 주는 await가 필요함setTimeout을 Promise로 감싸야 함비동기 처리를 위해 await를 사용하는 예시:
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getStarIcon = async () => {
await delay(1000);
return '⭐'; // resolve("⭐")
};
getStarIcon().then((star) => console.log(star));
delay 함수는 Promise를 반환하며 이 Promise는 setTimeout을 사용하여 지정된 시간(ms) 후에 resolve됨getStarIcon 함수 내부에서 await delay(1000)는 delay 함수가 반환한 Promise가 resolve될 때까지 1초 동안 기다림delay 함수의 Promise가 resolve되면 getStarIcon 함수는 ⭐을 반환함getStarIcon().then((star) => console.log(star));는 getStarIcon 함수가 반환한 Promise가 resolve될 때, 즉 ⭐이 반환될 때 then 메서드가 실행되어 star를 출력함async와 await를 사용하지 않은 경우의 콜백 지옥:const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getStarIcon = () => delay(1000).then(() => '⭐');
const getWaveIcon = () => delay(1000).then(() => '🌊');
const getFaceIcon = () => delay(1000).then(() => '🥰');
const getAllIcon = () => {
getStarIcon().then((star) => {
getWaveIcon().then((wave) => {
getFaceIcon().then((face) => {
console.log(`${star} ${wave} ${face}`);
});
});
});
};
getAllIcon();
async와 await를 사용하여 콜백 지옥을 해결한 예시:const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const getStarIcon = async () => {
await delay(1000);
return '⭐';
};
const getWaveIcon = async () => {
await delay(1000);
return '🌊';
};
const getFaceIcon = async () => {
await delay(1000);
return '🥰';
};
const getAllIcon = async () => {
const star = await getStarIcon();
const wave = await getWaveIcon();
const face = await getFaceIcon();
console.log(`${star} ${wave} ${face}`);
};
getAllIcon();
async와 await를 사용하여 비동기 작업을 순차적으로 실행하면서도 코드의 가독성을 높임const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const task1 = async () => {
await delay(1000);
return 'task1 시작';
};
const task2 = async () => {
await delay(2000);
return 'task2 시작';
};
const task3 = async () => {
await delay(1000);
return 'task3 시작';
};
const task4 = async () => {
await delay(1000);
return 'task4 시작';
};
const startTasks = async () => {
console.time();
const msg1 = await task1();
const msg2 = await task2();
const msg3 = await task3();
const msg4 = await task4();
console.log(msg1, msg2, msg3, msg4);
console.timeEnd();
};
startTasks();

const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function task1() {
await delay(1000);
return 'task1 시작';
}
async function task2() {
await delay(2000);
return 'task2 시작';
}
async function task3() {
await delay(1000);
return 'task3 시작';
}
async function task4() {
await delay(1000);
return 'task4 시작';
}
async function startTasks() {
console.time('병렬 실행');
const task1Promise = task1();
const task2Promise = task2();
const task3Promise = task3();
const task4Promise = task4();
const msg1 = await task1Promise;
const msg2 = await task2Promise;
const msg3 = await task3Promise;
const msg4 = await task4Promise;
console.log(msg1, msg2, msg3, msg4);
console.timeEnd('병렬 실행');
}
startTasks();

task2가 완료되는 2초 후에 모든 작업이 완료됨Promise.all을 사용하여 병렬로 비동기 작업을 처리할 수 있음const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function task1() {
await delay(1000);
return 'task1 시작';
}
async function task2() {
await delay(2000);
return 'task2 시작';
}
async function task3() {
await delay(1000);
return 'task3 시작';
}
async function task4() {
await delay(1000);
return 'task4 시작';
}
async function startTasks() {
console.time('병렬 실행');
const tasks = await Promise.all([task1(), task2(), task3(), task4()]);
console.log(tasks.join(', '));
console.timeEnd('병렬 실행');
}
startTasks();

Promise.all을 사용하여 모든 작업을 병렬로 실행하고, 모든 작업이 완료되면 결과를 출력함Promise 중 하나라도 rejected 상태가 되면, 나머지 Promise가 완료되지 않았더라도 Promise.all은 즉시 rejected 상태로 변함Promise.allSettled는 모든 Promise의 완료 여부와 상관없이 결과를 반환함Promise의 성공과 실패를 구분하고, 각각의 결과를 처리할 수 있게 함const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
async function task1() {
await delay(1000);
return 'task1 시작';
}
async function task2() {
await delay(2000);
throw new Error('에러');
}
async function task3() {
await delay(1000);
return 'task3 시작';
}
async function task4() {
await delay(1000);
return 'task4 시작';
}
async function startTasks() {
console.time('병렬 실행');
const tasks = await Promise.allSettled([task1(), task2(), task3(), task4()]);
console.log(tasks);
console.timeEnd('병렬 실행');
}
startTasks();

Promise.allSettled를 사용하여 모든 Promise가 완료될 때까지 기다리며, 각 Promise의 결과를 배열로 반환함Promise도 포함되므로, 모든 결과를 확인할 수 있음