[JS] 콜백함수와 비동기

yoon Y·2022년 7월 3일
0

JS 학습 내용 정리

목록 보기
6/7

콜백함수와 비동기


콜백함수란?

콜백함수는 다른 코드(함수 또는 메서드)에게 인자로 넘겨지므로써 그 제어권도 함께 위임한 함수.
콜백함수를 위임받은 코드는 자체적인 내부 로직에 의해 콜백함수를 적절한 시점에 실행한다.

콜백함수의 this 바인딩

  • 객체의 메서드를 콜백함수로 넘기면 실행 시에 메서드가 아닌 함수로 동작한다.(실행시키는 것이 아닌 전달만 시키는 것이기 때문)
  • 그렇기에 따로 this를 바인딩하지 않으면 전역객체(디폴트)를 가리키게 된다.
  • 제어권을 가진 코드 내부에서 call함수를 이용해 this를 명시적으로 바인딩할 수 있다.
  • 제어권 코드 밖에서 콜백함수를 전달하는 시점에 this를 바인딩하고 싶으면 bind(thisArg)함수를 사용해야한다
  • 아니면 화살표 함수를 사용해서(감싸서) 선언시점의 상위 스코프의 this를 참조하도록 할 수 있다.
  • setTimeout(obj1.func.bind(obj1), 1000)

비동기 제어

동기적인 코드 - 현재 실행 중인 코드가 완료된 후에야 다음 코드를 실행하는 방법
비동기적인 코드 - 현재 실행 중인 코드의 완료 여부와 무관하게 즉시 다음 코드로 넘어가는 것

비동기 제어(처리)란 비동기 코드 완료 이후에 실행되어야할 코드들을 지정하는 것.

  • 비동기 로직이 끝난 후에 처리되어야 할 코드의 실행을 보장받는 게 목적이다.
  • 콜백함수, promise, async/awiat를 사용해 구현할 수 있다.
  • 콜백함수는 자신의 실행 순서를 보장받기 때문에 이를 이용해 비동기 처리를 할 수 있다.

webApi를 이용한 비동기 처리

  • js는 자체로 비동기 실행을 할 수 없으므로 webApi를 사용해야한다.
  • setTimeout: n초가 지날 때까지 기다림
  • addEventListner: 조건이 만족될 때까지 기다림
  • XMLHttpRequest: 서버 요청이 완료될 때까지 기다림
  • 위 함수들은 실행되면 조건이 만족될 때까지 기다리며(실행되고 있음) 다른 코드들을 실행하다가
    조건이 만족되면 전달 받은 콜백함수가 있다면 이를 실행시킨다.
  • 해당 함수의 로직이 끝나기 전에도 다른 코드들을 실행시킨다는 점에서 비동기적인 코드라고 할 수 있다.

연속적으로 비동기 코드 실행 및 처리하기

콜백 함수 이용 (콜백 기반 비동기 프로그래밍)

비동기 프로그래밍의 일반적인 접근법이다.
무언가를 비동기적으로 수행하는 함수는 함수 내 동작이 모두 처리되 후 실행되어야 하는 함수가 들어갈 콜백을 인수로 반드시 제공해야한다.

콜백 지옥
비동기 동작을 하는 함수를 중첩적으로 호출하면서 화살표 함수 모양의 콜백 안에 콜백을 넣는 것.
연속으로 실행할 비동기 코드가 많을 경우 콜백함수를 이용 시 콜백 지옥이 발생할 수 있다.
콜백지옥이 일어나는 이유는 비동기 코드 실행과 처리를 연속적으로 해야하기 때문이다.

콜백 지옥 해결 방법

  1. 콜백함수를 화살표함수가 아닌 기명함수로 작성해서 사용하기
    가독성을 완화시킬 수 있지만 일회성인 함수를 따로 선언하고 메모리에 저장하는 것이 썩 효율적이진 않고
    가독성이 크게 좋아지진 않는다
  2. Promise와 async/await 사용하기 콜백함수를 대체해 연속 비동기 처리를 간편하고 가독성있게 구현할 수 있도록 만들어진 것이다.

Promise

1. 제작 코드(프로미스 생성자로 전달되는 콜백함수인 excutor)

시간이 걸리는 비동기 동작을 실행함.
executor라는 콜백함수 안에 작성되어 프로미스 객체 생성 시 인자로 넘겨진다.

executor

  • 프로미스 생성 시 자동으로 실행됨
  • resolve, reject 라는 두 개의 콜백을 인자로 받는데 둘 중 하나를 반드시 호출해야한다. resolve(value) - 앞 코드가 성공적으로 완료되었을 경우 그 결과를 인자로 하여 호출
    reject(error) - 앞 코드 실행 중 에러 발생 시 에러객체를 인자로 하여 호출
  • 처리가 끝나면 성공 여부에 따라 resolvereject 를 호출한다

executor는 promise객체의 상태를 변화시킨다.

state
처음엔 pending(보류) 였다가 resolve호출 시 fulfilled, reject호출 시 rejected로 변경

result

처음엔 undefined였다가 resolve호출 시 value로, reject호출 시 error 로 변경됨
프로미스 객체가 처음 생성된 후에 ****executor의 실행 결과에 따라 상태와 결과가 변하는 것

executor는 보통 시간이 걸리는 일을 수행한다.
일이 끝나면 resolve나 reject함수를 호출하는데, 이때 프로미스 객체의 상태가 변화한다.
이행(resolve) 혹은 거부(reject) 상태의 프로미스는 처리된 프로미스 라고 부르고,
반대의 프로미스는 대기상태(pending)의 프로미스라고 한다.

한 번 변경된 상태는 더 이상 변하지 않는다.
처리가 끝난 프로미스에 resolve와 refect를 호출하면 무시된다.
executor함수 내부의 코드에 꼭 비동기 로직만 들어갈 수 있는 건 아니다.
resolve와 reject를 즉시 호출할 수도 있다. (프로미스 객체는 즉시 이행 상태가 됨)

2. 소비 코드(then, catch, finally의 콜백함수)

  • 제작 코드의 결과를 기다렸다가 이를 소비함
  • 프로미스 생성자의 인스턴스의 then, catch, finally메서드의 콜백함수(핸들러) 안에 작성됨
  • 소비함수는 then, catch, finally메서드를 사용해 등록(구독)된다.
  • 프로미스 객체의 상태가 executor실행 결과에 따라 성공이나 실패로 바뀌면 결과 값(result)이 핸들러 함수에 전달되어 코드가 실행된다.
  • 프라미스가 대기 상태일 때, .then/catch/finally 핸들러는 프라미스가 처리되길 기다리지만
    프라미스가 이미 처리상태라면 핸들러가 즉각 실행된다.
     let promise = new Promise(resolve => resolve("완료!"));
       promise.then(alert); // 완료! (바로 출력됨) 하지만 밑의 다른 코드 이후에..

then

첫번째 인자

  • 프로미스가 성공 상태일 때 실행될 함수를 받는다.
  • result파라미터를 넣어줘야하는데 성공 결과가 들어온다

두번쨰 인자

  • 프로미스가 실패 상태일 때 실행될 함수를 받는다.
  • error파라미터를 넣어줘야하는데 에러 객체가 들어온다.

성공 경우만 다루고 싶다면 첫번째인수만, 실패 경우만 다루고 싶다면 첫번째 인수를 null로 하고 2번째 인수만 넣어주면 됨

// 성공 경우
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve("done!"), 1000);
});

// resolve 함수는 .then의 첫 번째 함수(인수)를 실행합니다.
promise.then(
  result => alert(result), // 1초 후 "done!"을 출력
  error => alert(error) // 실행되지 않음
);

// 실패 경우
let promise = new Promise(function(resolve, reject) {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// reject 함수는 .then의 두 번째 함수를 실행합니다.
promise.then(
  result => alert(result), // 실행되지 않음
  error => alert(error) // 1초 후 "Error: 에러 발생!"를 출력
);

catch

  • 실패 경우를 다루는 메서드
  • 문법이 간결하다는 점만 빼고 .then(null,f) 과 같다.
let promise = new Promise((resolve, reject) => {
  setTimeout(() => reject(new Error("에러 발생!")), 1000);
});

// .catch(f)는 promise.then(null, f)과 동일하게 작동합니다
promise.catch(alert); // 1초 뒤 "Error: 에러 발생!" 출력

finally

  • 실패든 성공이든 상관없이 실행된다.
  • 로딩 인디케이터(loading indicator)를 멈추는 경우같이, 결과가 어떻든 마무리가 필요할 때 유용하다.

프라미스 체이닝

프로미스 객체의 (소비코드를 처리하는) .then.catch,  .finally 메소드를 체이닝 형태로 이어서 사용하는 것.

.then.catch,  .finally 의 핸들러에서 반환을 하게되면 프라미스를 반환한다.
나머지 체인은 반환된 프라미스가 처리될 때까지 대기한다.
처리가 완료되면 프라미스의 result (값 또는 에러)가 다음 체인으로 전달된다.

값 반환하기

어떤 값을 반환하면 그 값을 성공 결과로 가진 (이행된)promise객체가 반환된다.

이후의 핸들러는 결과 값을 즉시 전달 받아 실행되게 된다.

let promise = new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise.then(function(result) {
  alert(result); // 1
  return result * 2;
});

promise반환하기

promise를 반환하거나 생성과 동시에 반환할 수도있다.

이 경우 이어지는 핸들러는 프라미스가 처리될 때까지 기다리다가 처리가 완료되면 그 결과를 받는다.

new Promise(function(resolve, reject) {
  setTimeout(() => resolve(1), 1000);

}).then(function(result) {
  alert(result); // 1
  return new Promise((resolve, reject) => { // (*)
    setTimeout(() => resolve(result * 2), 1000);
  }); 

}).then(function(result) {
  alert(result); // 2초 후에 2가 찍힌다.
})

fetch의 경우

응답을 받아오는 비동기로직 + 응답 값을 다운받아 변환하는 비동기 로직

fetch('/article/promise-chaining/user.json') 
  .then(response => response.json()) 
  .then(user => alert(user.name)); 

// 서버가 응답하면 응답값을 받아온다.  
// 하지만 사용할 body데이터는 전부 다운로드되지 않은 상태다.
// json()함수로 데이터를 다운로드 받고, 받은 데이터를 js객체로 변환시킨다.
// 변환된 js객체를 결과값으로 하는 이행된 프로미스를 반환한다.

Promise와 에러 핸들링

프라미스가 거부되면 제어 흐름이 제일 가까운 rejection핸들러로 넘어가기 때문에 프라미스 체인을 사용하면
에러를 쉽게 처리할 수 있다.

.catch는 첫번째 핸들러일 필요가 없고 하나 혹은 여러 개의 .then 뒤에 올 수 있다.
위의 어떤 핸들러에서 오류가 발생하든 가장 가까운 catch에서 잡는다.

executor함수 내에서의 암시적 try...catch

  • reject함수를 실행시키지 않고 throw error를 하더라도 catch함수 핸들러에 에러 객체가 전달된다.
new Promise((resolve, reject) => {
  throw new Error("에러 발생!");
}).catch(alert); // Error: 에러 발생!

핸들러 내에서의 암시적 try...catch

  • .then 핸들러 안에서 throw를 사용해 에러를 던지면, 거부된 프라미스를 반환하게 되어 제어 흐름이 가장 가까운 에러 핸들러로 넘어간다.
  • throw문이 만든 에러뿐만 아니라 핸들러 위쪽에서 발생한 비정상 에러 또한 잡는다.
new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  throw new Error("에러 발생!"); // 프라미스가 거부됨
}).catch(alert); // Error: 에러 발생!

new Promise((resolve, reject) => {
  resolve("ok");
}).then((result) => {
  blabla(); // 존재하지 않는 함수
}).catch(alert); // ReferenceError: blabla is not defined
  • 에러 다시 던지기
    • .catch 핸들러 안에서  에러가 성공적으로 처리되면 가장 가까운 곳에 있는 .then
      핸들러로 제어 흐름이 넘어가 실행이 이어진다.
    • 에러가 처리되지 못하여 다시 throw를 던지면 제어 흐름이 가장 가까운 곳에 있는 에러 핸들러로
      넘어간다.
  • 끝까지 처리되지 못한 에러는 전역에러가 생성되는데 브라우저 환경에선 unhandledrejection
    이벤트로 잡을 수 있다.

마이크로태스크 큐

.then/catch/finally 의 핸들러는 비동기적으로 실행된다.
비동기로 작동하는 코드가 없고 즉시 resolve가 이행되더라도 나머지 코드를 먼저 실행시킨다.

let promise = Promise.resolve(); // 이행 상태의 프로미스 객체를 즉시 만드는 방법

promise.then(() => alert("프라미스 성공!"));

alert("코드 종료"); // 이 얼럿 창이 가장 먼저 나타납니다.

마이크로태스크 큐란?

비동기 작업을 처리를 위한 js런타임 환경 안에 있는 큐

  • 프라미스가 준비되었을 때 이 프라미스의 .then/catch/finally 핸들러가 큐에 들어간다.
  • 핸들러들은 여전히 실행되지 않고 대기하다가 콜스택이 비워졌을 때 자바스크립트 엔진이 큐에서 핸들러 함수들을 차례로 꺼내 실행한다.
  • 마이크로태스크 큐가 다 비워질 때까지 FIFO로 전부 꺼내져 실행된다.
  • 프라미스 핸들러는 항상 내부 큐를 통과하게 된다.
  • js엔진은 마이크로태스크 큐가 빈 이후에 처리되지 못한 에러가 있다면 unhandledrejection이벤트를 트리거 한다.

async/await

프라미스를 좀 더 편하게 사용할 수 있는 문법적 설탕.

읽고, 쓰기 쉬운 비동기 코드를 작성할 수 있다.

async함수

  • function 앞에 async를 붙이면 해당 함수는 항상 프라미스를 반환한다.
  • 프라미스가 아닌 값을 반환하더라도, 그 값을 result로 하는 이행된 프라미스가 반환되도록 한다.
  • await를 사용할 수 있다.
async function f() {
  return 1;
}

f().then(alert); // 1

await

  • await는 async 함수 안에서만 동작한다.
  • await 함수는 promise객체에만 붙을 수 있다.
  • await 키워드를 만나면 async 함수 내부 코드 실행을 중지하고,
    프라미스가 처리될 때까지 기다린 후 결과를 반환한다.
  • 프로미스가 처리되는 동안 async 함수 내부 코드 이외의 다른 코드들이 실행된다.
  • 프로미스 처리가 완료되면 async 함수 내부 코드가 실행된다.

에러 핸들링

프라미스가 정상적으로 이행되면 await promise는 프라미스 객체의 result에 저장된 값을 반환하지만
프라미스가 거부되면 throw문을 작성한 것처럼 에러가 던져진다.

try...catch문을 이용해 에러를 핸들링 할 수 있다.

async function f() {

  try {
    let response = await fetch('http://유효하지-않은-url'); //처리가 완료되면 result를 반환한다
    let user = await response.json();
  } catch(err) {
    // fetch와 response.json에서 발행한 에러 모두를 여기서 잡는다.
    alert(err);
  }
}

f();

참고 링크

profile
#프론트엔드

0개의 댓글