개발자 첫걸음/The WEb Developer BootCamp 2022

Section 27: 비동기식JavaScript

프로아마추어 2022. 4. 29. 21:30

1. 콜 스택 (Call Stack)

  • 스택은 LIFO 데이터 구조로 되어있다.
  • JavaScript가 사용하는 메커니즘으로  여러 함수를 호출하는 스크립트에서 함수를 추적한다.
  • JavaScript는 콜 스택에 함수 호출을 추가한다. 값이 반환되면 콜 스택의 함수를 삭제하고 다시 코드를 해석한다.
const multiply = (x, y) => x * y;
const square = (x) => multiply(x, x);

const isRightTriangle = (a, b, c) => {
	// squre(a)는 multiply(a)의 값이 리턴 될 때까지 대기한다. -> multiply(a, a)
	// squre(b)는 square(a)의 값이 리턴 될 때까지 대기한다. -> multiply(b, b)
	// squre(c)는 square(b)의 값이 리턴 될 때까지 대기한다. -> multiply(c, c)
	
	// 모든 함수가 실행되고 return값을 연산하면 마지막으로 isRightTriangle 함수가 리턴되고 콜 스택이 모두 비어있게 된다. 
	return square(a) + square(b) === square(c)
};

2. WebAPI와 단일 메서드

  • JavaScript는 싱글쓰레드이다.
  • 한번에 한 줄의 코드만 실행가능하다.
console.log('Sending request to server');
setTimeout(() => {
	console.log('Here is your data from the server...');
	// 시간 처리는 브라우저가 담당한다.
}, 3000);
console.log('This is an end of file!');

// Sending request to server
// This is an end of file!
// Here is your data from the server...

JavaScript가 싱글쓰레드라면 왜 마지막 코드가 지연되고 순차적으로 코드가 실행되었어야 한다.
JavaScript가 아니라 브라우저가 일을 하고 있기 때문이다.
JavaScript는 WebAPI를 호출하여 브라우저에게 던져버린다. (setTimeout, request 등)
WebAPI가 작업을 마치면 callback queue를 거쳐 call stack에 추가되어 JavaScript가 처리한다.

 

3. Callback hell

무지개색을 1초 간격으로 보여주는 코드를 작성해보자. 

// 1초 간격으로 배경색 바꿔주기
// 지연시간을 계산해줘야해서 효율적인 코드라고 할 수 없다.

setTimeout(() => {
  document.body.style.backgroundColor = 'red';
}, 1000);

setTimeout(() => {
  document.body.style.backgroundColor = 'orange';
}, 2000);

setTimeout(() => {
  document.body.style.backgroundColor = 'yellow';
}, 3000);

함수의 중첩을 사용하여 위의 코드를 간소화해보자.

const delayColorChange = (newColor, delay, doNext) => {
  setTimeout(() => {
    document.body.style.backgroundColor = newColor;
    doNext && doNext();
  }, delay);
};

delayColorChange('red', 1000, () => {
  delayColorChange('orange', 1000 () => {
    delayColorChange('yellow', 1000);
  });
});

이렇게 함수가 다른 함수에 종속되어 실행되는 패턴은 상당히 많이 사용된다.
아래의 코드는 API로부터 영화를 검색해 DB에 넣는 코드라고 가정해보자.

searchMoviesAPI('놈놈놈', ()=> {
	saveToMyDB(movies, () => {
		// DB 작업 성공 시 실행코드
        }, () => {
            // DB 작업 실패 시 실행코드
        })
	}, () => {
		// API 통신 실패 시 실행코드
	})

이와 같이 하나 이상의 많은 콜백함수를 포함하고 있는 경우 콜백 지옥이라고 부른다.
코드가 복잡하게 중첩되어 있어 혼란스럽다. 

이를 해소할 수 있는 방법이 promise 객체를 사용하는 것이다.

 

4. Callback을 사용한 fakeRequest

  • promise객체를 알아보기 전에 실제 통신 요청을 흉내낸 함수로부터 응답을 어떻게 처리해야 하는지 알아보자.
  • 또한 promise객체가 필요한 이유를 콜백 지옥으로부터 다시 한번 살펴보자.
// 실제 요청을 흉내낸 코드
function fakeRequest(url, success, failure) {
  const delay = Math.floor(Math.random() * 4500) + 500;
  setTimeout(() => {
    if (delay > 4000) {
      failure('Connection time out');
    } else {
      success(`It's your fake data from ${url}`);
    }
  }, delay);
}

// API 첫번째 요청
fakeRequest(
  'test.com/page1',
  // API 첫번째 요청 성공 시
  response => {
    console.log('It works');
    console.log(response);
    // API 두번째 요청
    fakeRequest(
      'test.com/page2',
      // API 두번째 요청 성공 시
      response => {
        console.log('It works again!');
        console.log(response);
      },
      // API 두번째 요청 실패 시
      err => {
        console.log('error (2nd req)', err);
      }
    );
  },
  // API 첫번째 요청 실패 시
  err => {
    console.log('error', err);
  }
);

통신 요청 코드만 봐도 어질어질하다. 요점은 API 응답으로부터 성공 혹은 실패의 경우를 콜백함수로 처리한다는 것이다.
또한 promise객체를 사용하지 않은 무수한 콜백 함수의 요청은 코드에 혼란을 준다는 것을 알게 되었다.

 

5. Promise를 사용한 fakeRequest

promise는 어떤 연산이 최종적으로 완료 혹은 성공, 실패 했는지 알려주는 객체이다.

  • promise state 종류.
    • 대기 상태: pending
    • 성공: resolved
    • 실패: rejected
function fakeRequestPromise(url) {
  // promise는 resolve(응답 성공)와 reject(응답 실패) 두개의 콜백 함수를 가진다.
  return new Promise((resolve, reject) => {
    const delay = Math.floor(Math.random() * 4500) + 500;
    setTimeout(() => {
      if (delay > 4000) {
        reject('Connection time out');
      } else {
        resolve(`It's your fake data from ${url}`);
      }
    }, delay);
  });
}

// 첫번째 API 요청
fakeRequestPromise('yelp.com/api/coffee/page1')
// 첫번째 API 응답 성공
// resolve면 .then 이하 코드 실행
.then(() => {
  console.log('IT WORK! 요청(1)');
  // 두번째 API 요청
    fakeRequestPromise('yelp.com/api/coffee/page2')
    // 두번째 API 응답 성공
    .then(() => {
      console.log('IT WORK! 요청(2)');
    })
    // 두번째 API 응답 실패
    .catch(() => {
      console.log('ERROR! 요청(2)');
    })
  })
  // 첫번째 API 응답 실패
  // rejected면 .catch 이하의 코드 실행
  .catch(() => {
    console.log('ERROR! 요청(1)');
  });

그런데 promise 객체를 사용했음에도 이전의 직관성 떨어지는 코드와 별반 다를게 없어보인다. 
promise의 진가는 다음 시간에 알 수 있을 것이다!

 

6. Promise의 마법

  • promise chaining을 통해 이전의 코드보다 훨씬 간결한 코딩이 가능하다. 
fakeRequestPromise('yelp.com/api/coffee/page1')
  .then(data => {
    console.log('IT WORK! 요청(1)');
    console.log(data);
	// pormise객채를 return한다.
    return fakeRequestPromise('yelp.com/api/coffee/page2');
  })
  // return된 promise객체에 대한 resolve 
  .then(() => {
    console.log('IT WORK! 요청(2)');
    console.log(data);
  })
  // 단 하나의 catch로 chaining된 promise의 reject처리를 할 수 있다.
  .catch(err => {
    console.log('REQUEST FAILED!');
    console.log(err);
  });

 

7. 자신만의 Promise 만들기

promise 작성법

// resolve와 ,reject 콜백 함수를 만들어준다
new Promise((resolve, reject) => {
	reject();
})

예제1: promise 객체를 사용한 코드

const fakeRequest = (url) => {
	return new Promise((resolve, reject) => {
		setTimeout(()=>{
			resolve();
		},1000);
	})
}

// promise의 결과가 나오기 전까지는 pending 상태룰 유지한다.
fakeRequest('book.com')
	.then(()=> {
		console.log('DONE WITH REQUEST');
	})

예제2:  promise 객체를 배우기 전에 작성했던 delayColorChange 함수를 고쳐보자

const delayColorChange = (color, delay) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      document.body.style.backgroundColor = color;
      resolve();
    }, delay);
  });
};

delayColorChange('red', 1000) //
  .then(() => delayColorChange('orange', 1000))
  .then(() => delayColorChange('yellow', 1000))
  .then(() => delayColorChange('green', 1000))
  .then(() => delayColorChange('blue', 1000))
  .then(() => delayColorChange('navy', 1000))
  .then(() => delayColorChange('purple', 1000));

 

8. async 키워드

  • async 키워드 자체가 비동기 함수를 만들어준다.
  • async 함수는 promise를 반환한다.

예제1

const sing = async () => {
  // async 함수에 에러가 발생하면 rejected 상태가 된다.
  throw 'this is ERROR!';
  return 'LA LA LA LA';
};

sing()
  .then(data => {
    console.log('PROMISE RESOLVED WITH:', data);
  })
  .catch(err => {
    console.log('PROMISE REJECTED');
    console.log(err);
  });

예제2

const login = async (username, password) => {
  // username이나 password가 누락되면
  if (!username || !password) throw 'Missing Credentials';
  // password가 맞으면
  if (password === 'corgifeetarecute') return 'WELCOME!';
  // password가 다르면
  throw 'Invalid Password';
};

login('hi', 'corgifeetarecute')
  .then(response => {
    console.log('LOGGED IN!');
    console.log(response);
  })
  .catch(err => {
    console.log('ERROR!');
    console.log(err);
  });


// PROMISE REJECTED
// this is ERROR!

 

9. await 키워드

  • await 키워드를 사용하면 지정 promise 객체의 실행 결과가 나올때까지 대기한다.
const delayColorChange = (color, delay) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      document.body.style.backgroundColor = color;
      resolve();
    }, delay);
  });
};

async function rainbow() {
  // await keyword를 통해 코드가 순차적으로 실행된다.
  await delayColorChange('red', 1000);
  await delayColorChange('orange', 1000);
  await delayColorChange('yellow', 1000);
  await delayColorChange('green', 1000);
  await delayColorChange('blue', 1000);
  await delayColorChange('navy', 1000);
  await delayColorChange('purple', 1000);
  return 'ALL DONE!';
}

rainbow().then(() => console.log('END OF RAINBOW!'));

 

10. 비동기 함수와 오류 처리하기

function fakeRequestPromise(url) {
  return new Promise((resolve, reject) => {
    const delay = Math.floor(Math.random() * 4500) + 500;
    setTimeout(() => {
      if (delay > 4000) {
        reject('Connection time out');
      } else {
        resolve(`It's your fake data from ${url}`);
      }
    }, delay);
  });
}

async function makeTwoRequests() {
  // reject 처리 필요가 있는 코드를 try문에 넣어준다.
  try {
    let data1 = await fakeRequestPromise('/page1');
    console.log(data1);
    let data2 = await fakeRequestPromise('/page2');
    console.log(data2);
    // catch문을 통해 async 함수의 reject를 제어한다.
  } catch (e) {
    console.log('CAUGHT AN ERROR');
    console.log('error is', e);
  }
}

 

 

'개발자 첫걸음 > The WEb Developer BootCamp 2022' 카테고리의 다른 글

Section 30: 터미널 완벽 정리  (0) 2022.05.02
Section 28: AJAX와 API  (0) 2022.04.30
Section 25: DOM Event  (0) 2022.04.28
Section 24: DOM이란?  (0) 2022.04.27
Section 23: JavaScript의 최신 기능들  (0) 2022.04.26