티스토리 뷰

비동기 방식이란 논 블로킹 형태로 동작하는 방식이라는 것을 의미한다.

그럼 논 블로킹 방식이란 무엇인가?

이 논 블로킹 방식은 콜백함수를 반환하는 코드 방식을 의미한다.

이에 대해 구체적으로 설명해보고자 한다.

 

비동기 방식은 메인 쓰레드가 있으며 실제 코드를 실행하는 역할을 한다.

잠깐 용어를 짚고 넘어가자!

프로세스란 단순히 실행중인 프로그램을 의미하며 쓰레드는 이 프로세스 내에서 실제로 작업을 수행하는 주체이다.

 

다시 비동기 방식의 얘기로 돌아오자!

이 메인 쓰레드가 실제 코드를 실행할 때 실행시간이 좀 오래 걸린다고 판단하면 다른 쓰레드에게 '방금 내가 맡은 코드가 실행시간이 좀 오래 걸릴 거 같은데 너가 대신 실행해줘!'라고 하며 작업을 넘기고 다른 코드를 실행시키는 작업을 한다.

그러면 메인 쓰레드에게 작업을 받은 쓰레드는 해당 작업이 완료되면 다시 콜백함수를 메인 쓰레드에게 돌려준다.

이때 해당 작업을 전달받은 쓰레드를 워커 쓰레드라고 한다.

 

콜백함수를 전달받은 메인 쓰레드는 다른 코드를 실행시키는 작업을 처리한 이후에 해당 콜백함수를 실행시킨다.

그리고 또 실제 코드를 실행하는 과정에서 실행시간이 오래 걸릴 것 같으면 또 내부 쓰레드에게 작업을 맡긴다.

이와 같은 과정을 비동기 방식이라고 한다.

 

그럼 이 비동기 방식의 단점은 무엇인가 생각해볼 필요가 있다.

바로 코드의 실행 순서를 예측하기 힘들기 때문에 코드를 실행할 때마다 다른 순서로 출력된다는 것이다.

코드 실행 순서를 예측하기 힘든 이유는 메인 쓰레드가 아닌 다른 쓰레드가 언제 메인 쓰레드에게 받은 작업을 완료할지 모르며 설령 작업을 완료 후 메인쓰레드에게 콜백함수를 전달해도 메인쓰레드가 하던 다른 작업이 처리가 된 후에 해당 콜백함수가 실행되기 때문에 언제 콜백함수가 실행될지 예측하기 힘들다는 것이다.

 

즉 비동기 형태로 작성한 코드는 순서를 차례대로 보장받지 못하기 때문에 비동기 방식의 단점을 해결할 수 있어야 한다.

 

1. 콜백함수로 해결하는 방법이 있다.

비동기 함수인 setTimeout이 포함되어 있는 함수의 실행결과를 순서대로 출력되도록 콜백함수를 통해 코드를 작성하였다.

 

//함수의 인자에는 함수가 들어온다고 가정!

function install1(finished){
  setTimeout(() => finished('설치1 완료'), 200);	
}
function install2(finished){
  setTimeout(() => finished('설치2 완료'), 300);
}
function install3(finished){
  setTimeout(() => finished('설치3 완료'), 400);
}
function install4(finished){
  setTimeout(() => finished('설치4 완료'), 500);
}

install1(function(notification1){
  console.log('0.2초 후: '+notification1)
  install2(function(notification2){
    console.log('0.3초 후: '+notification2)
    install3(function(notification3){
    	console.log('0.4초 후: '+notification3)
    	install4(function(notification4){
          console.log('0.5초 후: '+notification4)	
    	})	
    })	
  })	
})

 

이 코드는 비동기 함수의 실행 결과를 순서대로 출력하여 비동기의 단점을 해결하였다.

하지만 이 코드를 보면 뭔가 보기 불편하다.

왜냐하면 여러개의 콜백함수가 점점 안쪽으로 들어가서 화살표 모양이 되었으며 보기 힘들기 때문이다.

이러한 형태를 콜백 지옥이라고 표현한다.

그럼 코드의 가독성이 떨어지는 문제까지 해결해 줄 수 있는 방법은 없을까?

다음 방법에서 비동기 방식을 해결하는 방식에 대해 알아보자.

 

2. promise패턴으로 해결하는 방법이 있다.

promise 패턴은 원래 응답을 지연시켜서 함수가 동시에 실행되는 것을 제어하기 위해서 고안된 것이었지만 오늘날에는 비동기의 단점을 해결하기 위해서 널리 사용되고 있다.

promise 패턴을 사용하면 마치 비동기 방식의 코드가 순서대로 동작하는 것처럼 구현할 수 있다.

이 promise 패턴을 구현하기 위해서는 promise객체라는 것을 만들어 사용한다.

그럼 promise객체는 어떻게 만들까?

 

요즘에는 fetchAPI처럼 promise객체를 반환하여 바로 사용할 수 있지만 과거의 코드의 경우 promise객체를 반환하지 않기 때문에 아래와 같이 직접 만들어줘야 한다.

 

const err = false

const PROMISE = () => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if(err) reject('에러 발생');
   resolve('실행1');
  },200)
 })
}
console.log(PROMISE())

 

위 코드의 실행결과는 다음과 같다.

 

Promise {<pending>}
undefined

 

우선 코드를 차근차근 알아보기 전에 promise객체의 상태를 먼저 짚고 넘어가고자 한다.

위의 결과에서 Promise {< >}은 현재 promise 객체의 상태를 나타낸다.

여기서 pending은 promise가 아직 완료되지 않은 상태라는 것을 의미한다.

다시 말하자면 resolve()와 reject()가 아직 호출되지 않아서 완료되지 않았다는 의미이다.

 

위와 같이 new Promise()를 통해 promise객체를 생성할 때 인자 값인 resolve와 reject를 갖고 있는 비동기 성질을 갖고 있는 콜백 함수를 넣는다.

만약 promise객체를 생성할 때 넣은 비동기 성질을 갖는 콜백함수가 정상적으로 실행된다면 resolve()를 호출한다.

그와 반대로 에러가 발생하면 reject()가 호출된다.

 

다시 resolve()에 대한 얘기로 돌아오자!

콜백함수가 정상적으로 실행되면 resolve()함수가 실행된다고 하였다.

그때 내부 함수가 실행되는데 그 이후의 코드들이 비동기 함수이며 이 함수들이 마치 동기처럼 차례대로 실행되는 효과를 적용하고자 한다면 then()을 사용하면 된다.

then을 통해 비동기로 동작하는 함수들을 연결해서 마치 동기 함수인 것처럼 표현할 수 있다.

이 then()에는 resolve로 반환한 값이 콜백함수의 첫번째 인자인 result에 들어온다.

또한 then()에서 return을 사용해서 다음 then을 호출할 수 있다.

이때 promise객체를 return하면 첫번째 then()과 같은 형태로 동작한다.

 

const err = false

const PROMISE = () => {
 return new Promise((resolve, reject) => {
  setTimeout(() => {
   if(err) reject('에러 발생');
   resolve('실행1');
  },200)
 })
}

PROMISE()
  .then(result => {
    setTimeout(() => {
     console.log(result)
     return console.log('실행2');
    },200);
  })
  .then(() => {
    setTimeout(() => {
     return console.log('실행3');
    },200);
  })
  .then(() => {
    setTimeout(() => {
      console.log('실행4');
    },200);
  })
  .catch(err => {console.log(err)})

 

실행결과는 다음과 같다.

 

실행1
실행2
실행3
실행4

 

위처럼 비동기 함수의 실행결과가 순서대로 출력된 것을 확인할 수 있다.

 

그럼 reject()는 무엇인가?

위에서 언급했듯이 promise객체를 사용할 때 에러가 발생할 수 있다.

이때 reject()가 호출되며 내부함수가 실행 후 then()으로 가지 않고 체이닝된 catch()를 실행한다.

위 함수에서 에러가 발생하면 reject()함수가 실행되고 catch()에는 reject로 반환한 값이 콜백함수의 첫번째 인자인 err에 들어온다.

에러 상황이라 가정했을 때 실행 결과는 다음과 같다.

 

에러 발생

 

하지만 다음과 같은 코드에서 에러가 발생되면 코드의 실행결과는 어떻게 될까?

 

const err = true

const PROMISE = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(err) reject('에러 발생');
      resolve('실행1');
    },200)
  })
}

PROMISE()
  .then(result => {
    setTimeout(() => {
      console.log(result)
      return console.log('실행2');
    },200);
  })
  .catch(err => {console.log(err)})
  .then(() => {
    setTimeout(() => {
      console.log('실행3');
    },200);
  })
  .then(() => {
    setTimeout(() => {
      console.log('실행4');
    },200);
  })

 

결과는 다음과 같다.

 

에러 발생
실행3
실행4

 

예상했던 것처럼 then을 건너띄고 가장 가까운 catch()가 호출되었다.

그리고 catch() 아래에 있는 then()이 계속 호출되었다.

즉 catch()로 체이닝된 부분부터 끊기지 않고 코드가 이어져 실행되는 것이다.

이러한 형태로 체이닝을 유지하는 특징을 활용하여 아래처럼 각 promise객체에서 reject()를 감지하기 위해서 catch()를 체이닝하는 것은 좀 복잡하다.

 

const err = true

const PROMISE = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(err) reject('에러 발생');
      resolve('실행1');
    },200)
  })
}

PROMISE()
  .then(result => {
    setTimeout(() => {
      return console.log(result);
    },200);
  })
  .catch(err => {
    console.log(err,1);
    return PROMISE()
  })
  .then(result => {
    setTimeout(() => {
      return result;
    },200);
  })
  .catch(err => {
    console.log(err,2);
    return PROMISE();
  })
  .then(result => {
    setTimeout(() => {
      return result;
    },200);
  })
  .catch(err => {
    console.log(err,3);
  })

 

이럴 때는 then의 두번째 인자를 사용하면 된다.

then은 체이닝된 promise객체에서 resolve가 호출되면 첫번째 인자가 호출되며 reject가 호출되었을 경우 두번째 인자가 호출된다.

코드로 살펴보자!

 

const err = true

const PROMISE = () => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(err) reject('에러 발생');
      resolve('실행1');
    },200)
  })
}

PROMISE()
  .then(result => {
    setTimeout(() => {
      return console.log(result);
    },200);
  }, err => {
    console.log(err,1);
    return PROMISE()
  })
  .then(result => {
    setTimeout(() => {
      return result;
    },200);
  }, err => {
    console.log(err,2);
    return PROMISE()
  })
  .then(result => {
    setTimeout(() => {
      return result;
    },200);
  }, err => {
    console.log(err,3);
  })

 

위 코드와 같이 then의 두번째 인자로 에러 처리를 한다면 이전 체인의 promise객체가 reject()를 호출했을 경우에만 호출된다는 것이다.

각 promise의 reject여부를 확인하려면 위와 같이 then의 두번째 인자를 사용하고 promise객체를 단위로 reject를 파악하려면 catch를 사용하는 것이 코드면에서 깔끔하다.

즉, 각 상황에 맞게 코드를 선택해서 써야 한다는 것이다.

 

한편 promise 패턴 중 Promise.all()이라는 함수가 있다.

이것은 promise객체를 갖고 있는 함수가 여러개 있고 이 함수들이 순서에 상관없이 병렬적으로 실행시키고 실행이 모드 완료된 후 특정 코드를 실행시키고 싶을 경우 사용하며 아래의 예시코드를 보면 사용방법을 알 수 있다.

 

const err = false

const PROMISE = (count) => {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if(err) reject('에러 발생');
      resolve(`실행${count}`);
    },200)
  })
}

Promise.all([PROMISE(1),PROMISE(2),PROMISE(3)])
	.then(result => {console.log(result);})
	.catch(err => {console.log(err);})

 

Promise.all()은 then으로 처리된 결과를 배열 형태로 반환하며 다음과 같은 실행결과가 나온다.

 

["실행1", "실행2", "실행3"]

 

반응형

'Languages > JS' 카테고리의 다른 글

이벤트 루프(event loop)  (0) 2021.05.14
비동기 2편(async, await)  (0) 2021.05.13
클로저(closure)  (0) 2021.05.13
호이스팅(hoisting)  (0) 2021.05.13
const와 let  (0) 2021.05.13
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2025/05   »
1 2 3
4 5 6 7 8 9 10
11 12 13 14 15 16 17
18 19 20 21 22 23 24
25 26 27 28 29 30 31
글 보관함
반응형