프로미스와 async & await/ 클로저

·2023년 4월 20일
1

study

목록 보기
59/81
post-thumbnail

프로미스 (Promise)와 async/await에 대해 설명해보세요.

Promise

+추가 참고

Promise 객체는 비동기 작업이 맞이할 미래의 완료 또는 실패와 그 결과 값을 나타냅니다.

비동기 작업을 처리할 때 콜백 함수를 사용했었다. 계속 콜백 함수를 사용하지 않고 Promise를 사용하게 된 이유는 콜백 지옥(Callback Hell) 때문이다.

콜백 함수

콜백 함수란, 다른 함수에 인자로 전달되는 함수이며, 외부 함수 내에서 일종의 루틴 또는 동작을 실행하기 위해 호출되는 함수를 뜻한다. 즉, 코드를 통해 명시적으로 호출하는 함수가 아니라, 어떤 이벤트가 발생했을때, 혹은 특정 시점에 도달했을 때에 시스템에서 호출하는 함수이다.

초기의 자바스크립트는 코드의 복잡도가 높지 않아서 콜백 함수를 중첩하는 일이 드물었지만, 현재는 규모가 커졌기 때문에 위 사진처럼 비동기 작업이 많아질 수록 콜백 지옥, Pyramid of Doom에 빠지게 되는데, 이러한 프로그래밍은 가독성이 몹시 떨어지고 추후에 코드를 수정할 일이 생길 경우, 이를 몹시 어렵게 한다.

그래서 ES6(ES2015)에서 추가된 개념이 바로 이 Promise인 것이다. Promise는 바로 이 코드의 깊이가 깊어지는 현상을 방지할 수 있다.

Promise 생성

// Promise 객체의 생성
const promise = new Promise((resolve, reject) => {
  // 비동기 작업을 수행한다.

  if (/* 비동기 작업 수행 성공 */) {
    resolve('result');
  }
  else { /* 비동기 작업 수행 실패 */
    reject('failure reason');
  }
});

Promise는 Promise 생성자 함수를 new 연산자와 함께 호출하여 생성한다. 이때, 비동기 처리를 수행할 콜백 함수를 인수로 전달받게 되는데, 이 콜백 함수는 resolve와 reject 함수를 인수로 전달받는다. 여기서 비동기 처리가 성공하게 되면 resolve 함수를 호출(pending => fulfilled)하고, 실패하면 reject 함수를 호출(pending => rejected)하게 되는 것이다.

프로미스는 다음과 같이 현재 비동기 처리가 어떻게 진행되고 있는지에 대한 상태(state) 정보를 갖는다.

프로미스의 상태 정보의미상태 변경 조건
pending비동기 처리가 아직 수행되지 않은 상태프로미스가 생성된 직후 기본 상태
fulfilled(settled)비동기 처리가 수행된 상태(성공)resolve 함수 호출
rejected(settled)비동기 처리가 수행된 상태(실패)reject 함수 호출

프로미스의 후속 처리 메서드

위처럼 프로미스의 비동기 처리 상태가 변화하면, 예를 들어 fulfilled 상태가 되면 프로미스의 처리 결과를 가지고 무언가를 해야 하고, rejected 상태가 되면 에러 처리를 해야 한다. 이를 위해 프로미스는 후속 메서드 then, catch, finally를 제공한다. 프로미스의 비동기 처리 상태가 변화하면 후속 처리 메서드에 인수로 전달한 콜백 함수가 선택적으로 호출된다.

Promise.prototype.then

then 메서드는 두 개 또는 한 개의 콜백 함수를 인수로 전달받는다.
then 메서드는 언제나 프로미스를 반환한다.

// fulfilled
new Promise(resolve => resolve('fulfilled'))
	.then(v => console.log(v), e => console.error(e)); // fulfilled

// rejected
new Promise((_, reject) => reject(new Error('rejected')))
	.then(v => console.log(v), e => console.error(e)); // Error: rejected
  • 첫 번째 콜백 함수는 프로미스가 fulfilled 상태가 되면 호출한다. 이때 콜백 함수는 프로미스의 비동기 처리 결과를 인수로 전달받는다.

  • 두 번째 콜백 함수는 프로미스가 rejected 상태가 되면 호출된다. 이때 콜백 함수는 프로미스의 에러를 인수로 전달받는다.

Promise.prototype.catch

catch 메서드는 한 개의 콜백 함수를 인수로 전달받고, 프로미스가 rejected 상태인 경우에만 호출된다.
catch 메서드도 언제나 프로미스를 반환한다.

// rejected
new Promise((_, reject) => reject(new Error('rejected')))
	.catch(e => console.log(e)); // Error: rejected

Promise.prototype.finally

finally 메서드는 한 개의 콜백 함수를 인수로 전달받고, 프로미스의 성공 또는 실패와 관계없이 무조건 한 번 호출된다. 그렇기 때문에 프로미스의 상태와 상관없이 공통적으로 수행해야 할 처리 내용이 있을 때 유용하게 사용된다.
finally 메서드도 언제나 프로미스를 반환한다.

new Promise(() => {})
	.finally(() => console.log('finally')); // finally

async/await

+추가 참고

async/await는 ES8(ES2017)에서 추가된 비동기 프로그래밍을 쉽게 처리하기 위한 기능이다.

async/await은 Promise를 기반으로 동작하지만, 프로미스의 then/catch/finally 후속 처리 메서드에 콜백 함수를 전달해서 비동기 처리 결과를 후속 처리할 필요 없이 함수의 실행을 일시 중단하고 비동기 작업이 완료되기를 기다린 후 실행을 재개하는 형식으로 마치 동기 처리처럼 Promise를 사용할 수 있다. 말 그대로 Promise의 상위 호환인 격이다.

async 함수

async 함수는 async 키워드를 사용해 정의하며 언제나 프로미스를 반환한다.

  • async 함수가 명시적으로 프로미스를 반환하지 않더라도 async 함수는 암묵적으로 반환값을 resolve 하는 프로미스를 반환한다.

  • async 함수에서는 await를 사용할 수 있습니다. await는 async 함수에서만 사용 가능합니다. 일반 함수에서 await를 사용하게 되면 syntax error가 발생됩니다.

await 키워드

await 키워드는 프로미스가 settled 상태가 될 때까지 대기하다가 settled 상태가 되면 프로미스가 resolve한 처리 결과를 반환한다. await 키워드는 반드시 프로미스 앞에서 사용해야 한다.

에러 처리

async 함수를 호출하고 Promise.prototype.catch 후속 처리 메서드를 사용해 에러를 캐치할 수도 있다.
하지만, async / await에서 에러 처리는 try ... catch 문을 사용할 수 있다.

콜백 함수를 인수로 전달받는 비동기 함수와는 달리 프로미스를 반환하는 비동기 함수는 명시적으로 호출할 수 있기 때문에 호출자가 명확하다.

const fetch = require('node-fetch');

const foo = async () => {
  try {
    const wrongUrl = 'https://wrong.url';
    
    const response = await fetch(wrongUrl);
    const data = await response.json();
    console.log(data);
  } catch (err) {
    console.log(err); // TypeError: Failed to fetch
  }
};

foo();

클로저 (Closure)란 무엇인가요?

클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여 자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.

  • 자바스크립트의 렉시컬 환경은 외부 렉시컬 환경을 가리키는 outer가 존재한다.
  • 자바스크립트는 렉시컬 스코프를 따르므로 식별자가 현재 스코프에 존재하지 않으면 선언된 위치를 기준으로 외부 환경에서 해당 변수를 찾는다.
  • 자바스크립트의 함수는 모두 클로저이다.
function a() {
  const x = 10;
  return function() {
    console.log(x)
  }
}

const b = a();

b(); // 10

위 코드에서 b라는 변수에 a의 반환값인 새로운 함수를 담았다.
그 후 호출했더니 x값이 없음에도 외부 변수였던 x를 기억하고 10울 반환한다.

클로저가 필요한(사용하는) 이유

1. 상태 유지
클로저가 가장 유용하게 사용되는 상황은 현재 상태를 기억하고 변경된 최신 상태를 유지하는 것이다.
클로저는 현재 상태를 기억하고 이 상태가 변경되어도 최신 상태를 유지해야 하는 상황에 매우 유용하다.

2. 전역변수를 줄일 수 있다.
자바스크립트에 클로저라는 기능이 없다면 상태를 유지하기 위해 전역 변수를 사용할 수 밖에 없다. 전역 변수는 언제든지 누구나 접근할 수 있고 변경할 수 있기 때문에 많은 부작용을 유발해 오류의 원인이 되므로 사용을 억제해야 한다.

// Counter 예제
const btn = document.querySelector('button')
btn.addEventListener('click',handleClick)

let count = 0
function handleCilck(){
  count++
  return count
}

//위와 같은 경우에 count를 전역변수로 사용해줘야 count가 증가를 해줄 수 있음
//이럴경우 클로져를 사용해서 해결할 수 있다.


// Counter Closure 예제
const btn = document.querySelector('button')
btn.addEventListener('click',handleClick())

function handleCilck(){
  let count = 0
  return function (){
    count++
    return count
  }
}
// 위와 같이 작성해 준다면 외부함수(handleClick)의 lexical environment를 참조하는 함수를
// btn의 콜백함수로 이용해 전역객체 없이 구현할 수 있다.

3. 비슷한 형태의 코드의 재사용률을 높일 수 있다.

4. 정보의 은닉

function Counter() {
  // 카운트를 유지하기 위한 자유 변수
  var counter = 0;

  // 클로저
  this.increase = function () {
    return ++counter;
  };

  // 클로저
  this.decrease = function () {
    return --counter;
  };
}

const counter = new Counter();

console.log(counter.increase()); // 1
console.log(counter.decrease()); // 0

생성자 함수 Counter의 변수 counter는 this에 바인딩된 프로퍼티가 아니라 변수다. counter가 this에 바인딩된 프로퍼티라면 생성자 함수 Counter가 생성한 인스턴스를 통해 외부에서 접근이 가능한 public 프로퍼티가 되지만 생성자 함수 Counter 내에서 선언된 변수 counter는 생성자 함수 Counter 외부에서 접근할 수 없다. 하지만 생성자 함수 Counter가 생성한 인스턴스의 메소드인 increase, decrease는 클로저이기 때문에 자신이 생성됐을 때의 렉시컬 환경인 생성자 함수 Counter의 변수 counter에 접근할 수 있다. 이러한 클로저의 특징을 사용해 클래스 기반 언어의 private 키워드를 흉내낼 수 있다.

단점

  1. 가독성이 떨어질 수 있다.
  2. 메모리 낭비
  • 내부에서 외부 변수를 참조하면서 가비지컬렉션이 일어나지 않게 됨.

가비지컬렉션
: 코드 실행시 객체가 생성되면 자동으로 힙 메모리를 할당하게 된다. 이것이 쓸모 없어졌을 때 자동으로 해제 시켜주는 기능.

  • 자바스크립트 엔진은 직간접적으로 참조되지 않은 객체들을 메모리에서 해제하여 메모리 공간을 확보하는 방식으로 동작한다.

  • 클로저는 개발자가 의도적으로 참조를 만들어 내는 것이지만, 그래도 메모리 낭비를 막을려면
    사용하지 않을 때 참조를 끊어준다면 메모리 사용을 해제할 수 있다.

    1. 쓰이지 않는 시점에 외부에서 null이나 undefined를 할당하는 방법
    2. 클로저 함수 내부에서 타이머함수나 이벤트리스너를 이용하여 더 이상 쓰이지 않을 경우를 대비해 null이나 undefined을 할당하는 방법
    3. ES2021에 새로 등장한 문법인 약한 참조를 활용
      약한 참조는 가비지 컬렉터가 돌 때, 참조를 유지하지 않겠다는 의미로 사용한다.
      (하지만 에측이 불가능한 단점이 있어 특별한 상항이 아니라면 약한 참조의 사용은 권장하지 않음.)

하지만 V8엔진 성능이 매우 좋아졌고, 가비지컬렉션 또한 덩달아 성능이 향상되면서 클로저의 메모리 누수가 심각하지 않다면 굳이 제거할 필요가 없다고 여겨진다.
오히려 불필요한 로직이 추가될 수도 있고, 의도적인 null/undefined 삽입이 유지보수 측면에서도 좋지 않을 것이기 때문

profile
개발자 꿈나무

0개의 댓글