[유데미x스나이퍼팩토리] 프로젝트 캠프 : Next.js 2기 - 4일차(Class, 표준 내장 객체, 비동기)

김하은·2024년 7월 19일
0
post-thumbnail

자바스크립트를 처음 배울 때, 표준 내장 객체를 이용해 코드를 간단히 작성할 수 있다는 점이 매우 신기했다. 반복문이나 조건문을 통해 힘들게 로직을 구성하지 않아도 데이터를 원하는 대로 가공할 수 있었기 때문이다. 마치 마법 같았다. 하지만 콜백 함수를 넘겨주는 방식에 익숙하지 않고, 메소드마다 반환값이 달라서 생소하고 어려운 것도 사실이었다. 그래도 오늘 수업을 통해 조금 더 익숙해 졌다. 특히 메소드 체이닝을 많이 연습할 수 있어서 유익한 시간이었다. 또, 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를 이용해서 개선해보는 시간이었다.

12.Class

  • 자바스크립트는 프로토타입 기반의 언어로 원래 클래스가 없었음
  • 기존의 문법을 활용해 편의성이나 기능을 더한 문법을 "syntactic sugar"라고 하는데, ES6에서 추가된 클래스가 바로 이러한 문법적 설탕 중 하나
    • 생성자 함수와 클래스의 작동 원리는 같으나, 보다 유용하게 사용하기 위한 문법적 기능들이 추가된 것

생성자 함수 vs 클래스

생성자 함수로 작성한 코드를 클래스로 바꿔 보기

생성자 함수

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);

비교

  1. 클래스는 프로토타입을 명시하지 않아도 자체적으로 프로토타입에 포함되므로 인스턴스 메모리 최적화가 자동으로 이루어짐
    • 클래스로 만든 shap1 객체의 메소드 getColor가 프로토타입 내부에 존재함을 알 수 있음
    • 클래스로 만든 rect1 객체는 Shape를 상속 받아서 프로토타입의 프로토타입의 constructorclass Shape이며 getColor도 여기 존재함을 알 수 있음

<콘솔에 출력된 결과 (생상자 함수shape1 / 클래스 shape1 / 클래스 rect1)>

생성자 함수클래스
  1. 클래스가 보다 직관적이어서 코드 가독성이 좋고 유지보수가 용이함

setter

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입니다.
  • Setter 메서드를 올바르게 사용하지 않으면 무한 호출이 발생할 수 있으므로this.speed = speedthis._speed = speed로 변경하여 이를 해결함

Getter

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 속성

클래스 내에서만 접근 가능한 속성을 정의할 때는 # 기호를 사용하여 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()); // 차 이름은 벤츠입니다.

Static 메서드와 속성

정적 메서드와 속성은 클래스 자체에 속하며 멤버 속성이나 프로토타입 속성에 포함되지 않으므로 인스턴스가 아닌 클래스 이름으로 호출해야 함

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 객체에는 정적 메서드와 속성만이 포함되어 있음
    • Math 객체는 인스턴스를 만들 수 없으며, Math의 메서드와 속성은 항상 Math 이름을 통해서만 접근할 수 있음
console.log(Math.PI); // Math 객체의 정적 속성
console.log(Math.max(1, 2, 3)); // Math 객체의 정적 메서드

13. 표준 내장 객체 (Standard Built-in Objects)

자바스크립트 엔진에 기본으로 내장되어 있는 객체들은 자바스크립트 엔진이 상시적으로 제공하는 기능으로, 어디서든 활용할 수 있음

리터럴 표기법과 생성자 함수

자바스크립트에서 데이터를 생성하는 두 가지 주요 방법: 리터럴 표기법과 생성자 함수

생성자 함수

  • 생성자 함수를 사용하여 데이터를 생성할 수 있음
    const str = new String('Hello');
    • 이 방식은 문자열을 객체로 감싸서 다양한 메서드와 프로퍼티를 사용할 수 있게 함
    • 그러나 생성자 함수를 사용하는 방식은 코드가 복잡해지고, 필요 이상으로 메모리를 사용할 수 있음

리터럴 표기법

  • 보다 편리하게 데이터를 생성하는 방법으로 리터럴 표기법이 있음
    const str = "Hello";
    • 리터럴 표기법은 간결하며 메모리를 효율적으로 사용함
    • 문자열 리터럴은 기본 데이터 타입으로 저장되며, 자바스크립트 엔진이 필요할 때 자동으로 객체처럼 취급하여 메서드와 프로퍼티를 사용할 수 있음

리터럴 표기법과 생성자 함수 비교

  • 리터럴 표기법으로 작성된 데이터가 프로토타입 객체를 사용할 수 있는 이유는 자바스크립트 엔진이 일시적으로 인스턴스 객체처럼 래핑(wrapping)하기 때문임

    const str = new String('Hello');
    console.dir(str);
    
    const str2 = 'Hello';
    console.dir(str2);
    • 문자열 리터럴 str2는 기본 타입으로 저장되지만, 메서드를 호출할 때 임시로 객체로 변환됨
    • 이 과정에서 문자열 리터럴도 생성자 함수로 생성된 객체와 동일한 메서드와 프로퍼티를 사용할 수 있음
  1. 리터럴 표기법

    const str = "Hello";
    • 문자열을 기본 타입으로 생성
    • 메모리를 효율적으로 사용
    • 자주 사용되는 방식으로, 코드가 간결함
    • 객체의 메서드와 프로퍼티를 사용할 수 있음 (자바스크립트 엔진이 자동으로 객체처럼 취급)
  2. 생성자 함수

    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)

    • 자바스크립트는 피연산자가 서로 다른 타입일 때 자동으로 타입을 변환하는 능력이 있음

    • 이 과정은 "암묵적 타입 변환" 또는 "타입 강제 변환"이라고 부름

      1. 객체에서 기본 값으로의 변환:

        • 자바스크립트는 객체를 기본 타입 값으로 변환할 때, 객체의 valueOf 메서드나 toString 메서드를 호출함
        • Number 객체의 경우, valueOf 메서드가 기본 숫자 값을 반환함
      2. 산술 연산:

        • 산술 연산 (+, -, *, /)을 수행할 때, 자바스크립트는 피연산자를 숫자로 변환하려고 시도함
      3. 비교 연산:

        • == 연산자는 두 값을 비교할 때 타입을 강제로 변환함. numObject는 객체이지만, 비교를 위해 valueOf 메서드를 호출하여 기본 값인 10을 얻음
        • 따라서 10 == 10이 되어 true를 반환함

참고 자료

객체를 원시형으로 변환하기

참고: === 연산자는 타입을 변환하지 않고 비교하기 때문에, numLiteral === numObjectfalse가 됨

console.log(numLiteral === numObject); // false

❓생성자 함수로 만든 문자열은 메모리에 어떻게 저장되는가?

❗다음 그림과 같이 리터럴 표기법으로 만든 문자열은 스택에 저장되고, 생성자 함수로 만든 문자열은 힙에 저장됨

  1. 기본 타입(Primitive Types):

    • 숫자, 문자열, 불리언 등 기본 타입은 일반적으로 스택(Stack)에 저장됨. 이 값들은 불변(immutable)하며, 실제 값이 스택에 저장됨
  2. 객체 타입(Object Types):

    • 객체, 배열, 함수 등 복합 타입은 힙(Heap)에 저장됨. 힙은 동적으로 할당된 메모리 블록들을 저장하는 데 사용됨
  3. 문자열의 경우:

    • 리터럴 표기법으로 생성된 문자열은 기본 타입으로 저장됨. 기본 타입은 스택에 저장됨
    • 생성자 함수를 사용하여 생성된 문자열은 String 객체로, 힙에 저장됨. String 객체는 래퍼 객체로, 문자열 값을 래핑하고 있으며, 추가 메서드와 프로퍼티를 제공함

예시 코드

let strLiteral = "Hello";            // 기본 타입 문자열, 스택에 저장
let strObject = new String("Hello"); // String 객체, 힙에 저장
  • strLiteral은 기본 타입 문자열로, 스택에 값이 저장됨
  • strObjectString 객체로, 힙에 객체가 저장되며, 스택에는 힙에 있는 객체를 가리키는 참조(reference)가 저장됨

참고 자료

Memory Management in JavaScript
Memory management


❓자바스크립트에서 실행컨텍스트가 생성되는 call stack과 원시값이 저장되는 메모리 상의 stack은 다른 것인가?

❗자바스크립트 엔진은 원시 타입 값을 콜 스택에, 참조 타입 값을 힙에 저장하며, 콜 스택은 원시 타입 값과 함수 호출의 실행 컨텍스트를 저장하는 곳임

  1. 전역 스코프는 스택에서 "전역 프레임"에 보관됨
  2. 모든 함수 호출은 프레임 블록으로 스택 메모리에 추가됨
  3. 반환 값과 인자를 포함한 모든 지역 변수들은 스택에서 함수 프레임 블록 안에 저장
  4. int와 string과 같은 모든 원시 타입 값은 스택에 바로 저장됨. 이는 전역 스코프에서도 적용됨
  5. Employee와 Function과 같은 객체 타입의 값은 힙에서 생성되고 스택 포인터를 사용해 힙에서 스택을 참조함. 전역 스코프에도 적용됨
  6. 현재 함수에서 호출된 함수들은 스택의 최상단에 추가됨(실행 컨텍스트 추가)
  7. 함수 프레임이 반환, 즉 함수가 종료될 때 스택에서 제거됨
  • 결론
    자바스크립트 엔진은 원시 타입 값을 콜 스택에, 참조 타입 값을 힙에 저장하며, 콜 스택은 원시 타입 값과 함수 호출의 실행 컨텍스트를 저장하는 곳임. 이 두 스택 메모리 영역은 같은 개념이며, 힙은 동적 데이터가 저장되는 별도의 메모리 영역임

참고 자료


14. 배열 내장 객체 (Array Built-in Objects)

파괴적 메서드 (Mutative Methods)

  • 인스턴스 메서드를 호출했을 때, 원본 데이터가 변경되는 메서드

    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: 배열의 요소를 정렬

비파괴적 메서드 (Non-mutative Methods)

  • 이 메서드를 호출했을 때, 원본 데이터가 변경되지 않는 메서드

    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: 조건에 맞는 첫 번째 요소의 인덱스를 반환

16. Math 객체

  • Math 객체는 다양한 수학적 계산을 쉽게 수행할 수 있게 해주는 메서드들을 포함하고 있으며, 인스턴스를 생성할 필요 없이 직접 호출하여 사용할 수 있음
    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));  // 8
    • Math.random(): 0과 1 사이의 난수를 반환
    • Math.max(): 전달된 인수 중 최대값을 반환
    • Math.min(): 전달된 인수 중 최소값을 반환
    • Math.pow(): 제곱 값을 반환

자바스크립트의 동기와 비동기

자바스크립트는 싱글 스레드 언어로 한 번에 하나의 작업만 처리할 수 있음을 의미하며 동기적으로 실행됨

  • 동기 (Synchronous): 코드가 순차적으로 실행되며, 코드의 순서가 보장됨
  • 비동기 (Asynchronous): 코드가 순차적으로 실행되지 않으며, 코드의 순서가 보장되지 않음

동기 실행의 문제점

  • 동기 실행에서는 모든 코드가 순서대로 실행되기 때문에, 실행 시간이 오래 걸리는 작업이 있을 경우 해당 작업이 완료될 때까지 자바스크립트 엔진은 다른 작업을 수행할 수 없음

비동기 실행의 필요성

  • 비동기 실행을 통해 자바스크립트는 실행 시간이 오래 걸리는 작업을 백그라운드에서 처리할 수 있음
  • 이는 자바스크립트 엔진이 긴 작업을 처리하는 동안에도 다른 작업을 계속 수행할 수 있게 함
  • 예를 들어, 네트워크 요청을 보내고 응답을 기다리는 동안 UI가 멈추지 않고 계속 반응할 수 있음

결론

  • 비동기 실행은 자바스크립트의 싱글 스레드 특성으로 인해 발생할 수 있는 블로킹 문제를 해결함
  • 실행 시간이 오래 걸리는 작업을 비동기로 처리함으로써, 사용자 경험을 향상시키고 애플리케이션의 성능을 최적화할 수 있음
  • 비동기 실행을 통해 자바스크립트는 동시에 여러 작업을 효율적으로 처리할 수 있음

❓싱글스레드와 병렬 실행(비동기)의 차이

❓자바스크립트 엔진은 비동기로 처리할지 말지를 어떻게 결정하지?


17. 콜백 함수

다른 함수의 매개변수로 전달되어 그 함수가 실행되는 동안 특정 시점에 호출되는 함수

  • 콜백 함수는 동기적일 수도 있고 비동기적일 수도 있음

동기 콜백 함수

동기 콜백 함수는 즉시 실행되는 콜백 함수를 의미함

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 객체를 제공함

18. Promise Then

Promise는 비동기 작업을 처리할 수 있도록 도와주는 자바스크립트 내장 객체이며 다음과 같은 세 가지 상태를 가짐:

  • pending: 비동기 처리가 아직 수행되지 않은 상태
  • fulfilled: 비동기 처리가 성공적으로 완료된 상태
  • rejected: 비동기 처리가 실패한 상태

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'));
  1. Promise 객체가 생성됨
  2. 콜백 함수가 즉시 실행되어 console.log('doing something...')이 출력됨
  3. 1초 후 setTimeout의 콜백이 실행되어 resolve('success')가 호출됨
  4. resolve가 호출되면 Promise 객체의 상태가 pending에서 fulfilled로 변경됨
  5. then 메서드 내부의 콜백 함수가 실행되어 console.log(value)에서 'success'가 출력됨
  6. catch 메서드는 호출되지 않으며, finally 메서드가 실행되어 console.log('finally')가 출력됨

결과 받는 방법

Promise 객체는 비동기 작업의 결과를 처리하기 위해 then, catch, finally 메서드를 제공함

then

  • then 메서드는 Promisefulfilled 상태일 때 호출됨
  • 첫 번째 매개변수로 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

  • catch 메서드는 Promiserejected 상태일 때 호출됨
  • 콜백 함수는 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

  • finally 메서드는 Promise가 완료되면 무조건 호출됨
  • 성공 또는 실패 여부와 상관없이 항상 실행됨

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);
  });

19. async await

async

  • 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 함수 내에서 발생한 예외는 자동으로 Promise.reject로 처리되므로, catch 메서드를 사용하여 예외를 처리할 수 있음

async 키워드를 사용하여 동일한 기능을 구현한 코드:

const getStarIcon = async () => '⭐'; // 무조건 resolve()
getStarIcon().then((star) => console.log(star));

여기서 getStarIcon 함수는 async 키워드를 사용하여 자동으로 Promise 객체를 반환하며, 반환된 값은 resolve됨. 따라서 동일하게 then 메서드를 사용하여 값을 출력할 수 있음

await

  • await 키워드는 async 함수 내부에서만 사용할 수 있음
  • awaitPromise가 처리될 때까지 기다린 다음, Promise가 처리되면 resolve된 값을 반환함

잘못된 예시

const getStarIcon = async () => setTimeout(() => '⭐', 1000); // 값이 즉시 반환되지 않음
  • aysycPromise를 반환할 뿐 비동기 함수를 기다려 주지는 않음
  • 따라서 setTimeout함수를 기다려 주지 않음
  • setTimeout함수는 1초 후에 '⭐'을 반환하지만 async가 기다려 주지 않아서 undefined를 즉시 반환함
  • 따라서 우리가 원하는 '⭐'을 반환 받으려면 Promise가 처리될 때까지 기다려 주는 await가 필요함
  • 또, setTimeoutPromise로 감싸야 함

올바른 예시

비동기 처리를 위해 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 함수가 반환한 Promiseresolve될 때까지 1초 동안 기다림
  • 1초 후, delay 함수의 Promise가 resolve되면 getStarIcon 함수는 을 반환함
  • getStarIcon().then((star) => console.log(star));getStarIcon 함수가 반환한 Promiseresolve될 때, 즉 이 반환될 때 then 메서드가 실행되어 star를 출력함

또 다른 콜백 지옥 예시

asyncawait를 사용하지 않은 경우의 콜백 지옥:

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();
  • 위 코드는 여러 비동기 작업이 중첩되어 가독성이 떨어짐

asyncawait를 사용하여 콜백 지옥을 해결한 예시:

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();
  • 위 코드는 asyncawait를 사용하여 비동기 작업을 순차적으로 실행하면서도 코드의 가독성을 높임

비동기 작업 최적화

순차 실행

  • 순차 실행은 작업을 하나씩 순서대로 처리함
  • 아래 코드는 각 작업이 완료된 후 다음 작업을 실행함
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();
  • 위 코드에서는 각 작업이 완료된 후 다음 작업이 시작됨
  • 따라서 모든 작업이 완료되려면 총 4초가 소요됨

병렬 실행

  • 병렬 실행은 여러 작업을 동시에 처리하여 실행 시간을 단축할 수 있음
  • 다음은 3가지 병렬 실행 방법임

1. 코드 수정하기

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초 후에 모든 작업이 완료됨
  • 총 실행 시간이 2초로 단축됨

2. Promise.all

  • 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가 실패하면 모든 Promise가 실패한 것으로 간주되어 전체 작업이 중단됨

3. Promise.allSettled

  • 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도 포함되므로, 모든 결과를 확인할 수 있음
profile
아이디어와 구현을 좋아합니다!

0개의 댓글