티스토리 뷰

자바스크립트의 싱글 스레드 작업 수행 방식(싱글 스레드? 콜스택이 1개라고 생각하시면 됩니다.)

스레드? 코드를 한줄씩 실행시켜주는 일꾼이라고 생각하면 된다.
  • JS는 코드가 작성된 순서대로 작업을 처리함
  • 이전 작업이 진행 중 일때는 다음 작업을 수행하지 않고 기다림
  • 먼저 작성된 코드를 먼저 다 실행하고 나서 뒤에 작성된 코드를 실행함

-> 동기 방식의 처리 

일을 하고 있을 때 다른 작업을 할 수 없는 방식을 "블로킹 방식"이라고도 합니다.

 

동기처리 방식의 문제점

  • 하나의 작업이 너무 오래 걸리게 되면, 해당 작업이 완료될 때까지 모든 작업이 올 스탑되기 때문에 전반적인 흐름이 느려집니다. -> 멀티 쓰레드로 해결하면되지만 JS는 싱글 스레드라서 "비동기 작업"으로 해결합니다.

싱글 스레드 방식을 이용하면서, 동기적인 작업의 단점을 극복하기 위해 여러 개의 작업을 동시에 실행시킵니다.

즉, 먼저 작성된 코드의 결과를 기다리지 않고 다음 코드를 바로 실행하는 비동기 작업을 합니다. 

스레드가 다른 일을 동시에 처리할 때 막지 않는 방식을 "논 블로킹 방식"이라고도 합니다. 

 

비동기 작업의 패턴은 대표적으로 callback 패턴이 있습니다.

function taskA(a, b, cb) {
  setTimeout(() => {
    //지역 상수 res
    const res = a + b;
    cb(res);
  }, 3000);
}
taskA(3, 5, (res) => {
  console.log(`A task result is ${res}`); // A task result is 8
});
console.log('동기 끝');

function taskB(a, b, cb) {
  setTimeout(() => {
    const res = a * b;
    cb(res);
  }, 2000);
}

taskB(3, 4, (res) => {
  console.log(`B task result is ${res}`);
});

function taskC(a, cb) {
  setTimeout(() => {
    const res = a ** 2;
    cb(res);
  }, 1000);
}
taskC(2, (res) => {
  console.log(`C task result is ${res}`);
});

//콜백 헬
taskA(4, 5, (a_res) => {
  console.log('a_res', a_res);
  taskB(a_res, (b_res) => {
    console.log('b_res',b_res);
    taskC(b_res, (c_res) => {
      console.log('c_res',c_res);
    });
  });
});

결과를 보면 동기 끝이 먼저 출력되고 그 다음 익명함수로 전달한 콜백함수의 결과값으로 8이 3초 뒤에 출력됩니다. 

콜백함수는 파라미터로 전달된 함수를 말합니다. 

 

JS 엔진은 Heap(메모리 할당) 과 Call Stack(코드실행) 으로 이루어져 있습니다. 

Call Stack에는 우리가 실행시키는 함수가 들어있는데 LIFO(Last In First Out)구조입니다. Stack은 가장 나중에 들어온 아이가 가장 먼저 나가게 되는 자료구조입니다. 실행이 종료된 함수는 Call stack에서 제거됩니다. main context 는 call stack의 가장 맨 아래에 쌓이는 아이인데 이 아이가 나가게 되면 프로그램이 종료된 것입니다. 

 

콜백 헬 예시를 보면, 콜백을 통해 받아온 비동거 처리 값을 다음 콜백의 인자로 넘겨주고 있는데 이렇게 작성하게 되면 코드의 가독성이 많이 떨어지게 됩니다. (콜백 헬은 비동기 처리를 할 때 비동기 처리 결과값을 사용하기 위해서 콜백이 계속해서 깊어지는 현상을 말합니다.) -> 이러한 콜백 헬을 빠져나오기 위해 나온 것이 JS의 Promise입니다. 

callback 패턴의 단점을 보완하기 위해 Promise객체가 나왔으나 Promise역시 then 지옥에 빠질 수 있어 ES8부터 async await 문법을 사용하여 비동기 코드를 동기처럼 보이도록 직관적인 코드를 작성할 수 있습니다. 

 

Promise란 자바스크립트의 비동기 작업을 돕는 객체를 말합니다. 

Promise를 사용하면 "비동기 처리의 결과값을 핸들링하는 코드를 비동기 함수로부터 분리"할 수 있습니다! 

-> 간단히 말하면 콜백을 줄지어서 쓸 필요가 없어진다는 뜻(콜백헬 탈출!) 빠르게 직관적으로 비동기 처리 코드를 작성할 수 있습니다. 

 

비동기 작업은 3가지 상태를 갖는데 Pending(대기), Fulfilled(성공), Rejected(실패) 가 있습니다.

  • pending -> fulfilled 되는 과정을 resolve(해결)되었다 라고 하고
  • pending -> rejected 되는 과정을 reject(거부)되었다 라고 합니다.  
  • 작업이 완료되었다면 비동기 작업이 resolve되었다와 같은 말로 이해하시면 됩니다. 
🛠비동기 작업은 한번 성공하거나 실패하면 그걸로 작업이 끝납니다. 

 

// 콜백함수 ver

function isPositive(number, resolve, reject) {
  setTimeout(() => {
    if (typeof number === 'number') {
      //성공? -> resolve
      resolve(number >= 0 ? '양수' : '음수');
    } else {
      //실패 -> reject
      reject('주어진 값이 숫자가 아닙니다.');
    }
  }, 2000);
}

isPositive(
  10,
  (res) => {
    console.log('성공적으로 실행됨 ', res);
  },
  (err) => {
    console.log('실패됨 ', err);
  },
); //2초 뒤, 성공적으로 실행됨 양수 라고 나옵니다.

// ===============================================================

//😎 Promise ver

// 인자로 받았던 콜백함수는 res, rej를 사용하기때문에 빼줘도 된다.
function taskA(a, b) {
  return new Promise((resolve, rej) => {
    setTimeout(() => {
      const res = a + b;
      resolve(res); //콜백함수를 res로 바꿔준다
    }, 3000);
  });
}

function taskB(a) {
  return new Promise((resolve, rej) => {
    setTimeout(() => {
      const res = a * 2;
      resolve(res);
    }, 1000);
  });
}

function taskC(a) {
  return new Promise((resolve, rej) => {
    setTimeout(() => {
      const res = a * -1;
      resolve(res);
    }, 2000);
  });
}

// 콜백식으로 처리 
// taskA(5, 1).then((a_res) => {
//   console.log('a_res', a_res);
//   taskB(a_res).then((b_res) => {
//     console.log('b_res', b_res);
//     taskC(b_res).then((c_res) => {
//       console.log('c_res', c_res);
//     });
//   });
// });

taskA(5, 1)
  .then((a_res) => {
    console.log('a_res', a_res);
    return taskB(a_res);
  })
  .then((b_res) => {
    console.log('b_res', b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log('c_res', c_res);
  });

위의 작업에서 비동기 작업 도중에 끊어서 다른 작업도 아래와 같이 수행할 수 있습니다. 

Promise객체를 사용하면 비동기 처리를 호출하는 코드와 결과를 처리하는 코드를 분리시킬 수 있기 때문입니다. 

const bPromiseResult = taskA(5, 1).then((a_res) => {
  console.log('a_res', a_res);
  return taskB(a_res);
});

// 다른 작업 와라랄ㄹ라라 

bPromiseResult
  .then((b_res) => {
    console.log('b_res', b_res);
    return taskC(b_res);
  })
  .then((c_res) => {
    console.log('c_res', c_res);
  });

 

+ axios랑 같이 사용하기 

- axios는 Promise 기반의 HTTP 클라이언트입니다. 이 클라이언트를 통해 네트워크 요청을 하면 프로미스 객체를 받환받습니다.

//axios는 써드파티 라이브러리 이므로 따로 설치가 필요합니다. 


const p1 = axios.get("서버주소1");
const p2 = axios.get("서버주소2");
const p3 = axios.get("서버주소3");
const p4 = axios.get("서버주소4");
const p5 = axios.get("서버주소5");

Promise.all([p1,p2,p3,p4,p5]).then((result)=>{});

 

🐱‍💻여러 개의 비동기 처리를 한번에 수행할 때에는 Promise.allSettled() 를 쓰자

 

Promise.all([]) 메서드의 단점은 해당 배열에 들어있는 프로미스 결과 값 중 하나라도 실패하면 나머지 성공한 아이들도 날라간다는 점이다. catch를 통해 에러를 받아낼 수 있지만 어디서 오류가 났는 지 알 수 없습니다.

이를 보완하기 위해서 나온 메서드가 바로 Promise.allSettled([]) 입니다. 후속처리 메서드 then에서 성공한 비동기 처리와 실패한 처리를 확인할 수 있습니다. 그렇기 때문에 then()의 콜백에서 필터링을 통해 실패한 비동기 처리만 재요청해주면 됩니다. 

결론적으로 Promise.all([]) 은 사용하지 않는 것이 좋습니다.

Promise.allSettled([p1,p2,p3,p4,p5]).then((result)=> {
//실패한 비동기 처리만 필터링해서 다시 시도
}).catch((err)=> {});
마지막으로 catch() 메서드에서 핸들링되는 에러들은 Promise.allSettled() 에서 발생한 에러만을 처리하는 것이 아니라 Promise.allSettled().then((result)=> { //어쩌구 로직 }) then절까지의 에러를 캐치한다. 이 점을 유의해야 합니다.

 

👾 결론적으로 Promise 패턴의 장점은 비동기 처리를 호출하는 코드와 결과를 처리하는 코드를 분리시킬 수 있다는 점 같습니다. 기존 콜백 패턴은 코드가 깊어지면 코드 가독성의 문제도 있지만 비동기 처리 결과값을 원하는 때에 받아와서 사용하는 것이 아니라 수행되는 즉시 사용해야 하기 때문입니다. 물론 기존의 콜백 패턴의 단점을 보완하여 보다 직관적인 비동기 코드 작성이 가능합니다.
댓글