왜 비동기적 프로그래밍을 해야하는가?

김현재·2021년 7월 7일
26
post-thumbnail

비동기 비동기 비동기 자꾸 언급되는데 그래서 왜 비동기 프로그래밍을 해야되는 걸까?
그리고 어떻게 하는 걸까?

two_jay님의 블로그 포스트아이스홍시님의 블로그 포스트를 보며 비동기 프로그래밍에 대하여 다시 review하는 시간을 가져보았다🤗
(하기 내용은 상기 두 분의 블로그 포스트를 많은 부분 참고하여 작성한 내용으로 참고 부탁드립니다😊)

자바스크립트는 싱글 스레드 언어이다

즉, 자바스크립트는 한번에 한 가지 작업밖에 수행하지 못한다! 그래서, 자바스크립트로 코드를 구동하면 무언가 순차적으로 일어나게 된다.

쉬운 예로, 편의점에서 물건을 살 때 계산 줄을 서는 것을 생각하면 쉽다. 먼저 온 순서대로 한사람 한사람씩 계산되는 과정이 바로 자바스크립트가 작업을 수행하는 방식이다.

여기서, 어떤 사람이 물건을 엄청 많이 샀다던가, 계산 중에 다른 물건을 집어오는! 행태를 부린다면, 뒤에 서있는 고객들은 하염없이 그 문제의 사람이 계산을 마칠 때까지 기다리는 수밖에 없다.

위의 상황이 바로 싱글 스레드 언어가 가지는 치명적인 단점이다.

많이 무거운 작업을 수행중일 때는 그 뒤의 어떤 작업도 진행되지 않기에, 화면 로딩, 통신 연결 등의 비효율을 높이고, 사용성 또한 급격히 떨어진다. 그리고 오늘날같이 엄청나게 많은 이미지와, 엄청나게 많은 데이터를 갖고 있는 웹페이지를 실행시킬 때 이러한 단점은 더욱 부각된다.

💡참고)
동기적으로 무거운 작업이 수행되어 이후의 작업들이 실행되지 않는 것을 Blocking 이라고 합니다.
예시는 아래 링크 참고!
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Concepts

코드가 현대 웹페이지에서 동기적으로만 일을 하게 된다면, 아마 페이지 최상단에 있는 것부터 하나씩 또 하나씩 로딩이 될 것이며, 설상가상 시작하면 중간에 물릴 수도 없으니, 페이지를 로딩하는 중간에 다른 명령을 수행할 수도 없을 것이다.

만약 유튜브 한 페이지가 오브젝트 하나씩하나씩 따로 로딩해서 동영상이 돌아가는데 꽤 오랜 시간이 걸리고, 그 시간 동안 다른 건 아무것도 클릭하지 못한다면? (생각만해도 아찔해진다..)

거기에 대한 해답이 바로 '비동기 프로그래밍'이다. 비동기적으로 코드를 실행하면 더 유동적으로, 더 효율적으로 많은 일을 할 수 있다!

💡참고)
동기적 일처리 방식 : 순차적으로 일을 스스로 끝내 나가는 방식 (한줄 한줄 쳐낸다)
비동기적 일처리 방식 : 해야 할 일을 위임하고 기다리는 방식 (동시다발적으로 업무를 수행하고 완료되는대로 loading되는 방식)

결론

다시 정리하면, 엄청난 용량의 정보를 갖고있는 현대 웹페이지를 보다 유동적으로, 효율적으로 움직이게 하기 위해서는 비동기적 프로그래밍이 필요하다는 것이다.



자바스크립트로 비동기 프로그래밍 하기

자바스크립트는 싱글 스레드 언어지만, 위와 같이 현대 웹환경에 적응하기 위해, 비동기적 프로그래밍을 구현할 수 있도록 다양한 방법을 제공하고 있다. 이를 하나하나씩 살펴보자.

Callback 함수

callback함수는 어떠한 메소드 내에서 호출되는(callback되는)함수로, 이를 이용하여 우리는 비동기 프로그래밍을 구현할 수 있다.

callback함수를 사용한 대표적인 예로 setTimeout 을 통해 callback을 익혀보자.

setTimeout(() => {console.log('setTimeout 작동!')}, 3000)

출처 : two_jay.log

setTimeout은 일정 시간(ms)가 지나면 callback함수를 실행시키는 메소드이다.

setTimeout 을 설정해두면, 해당 함수에서 지정한 시간이 지날 때까지 다른 작업을 먼저 진행하며, 지정 시간이 되면 setTimeout 내 콜백 함수를 실행하여 비동기를 구현한다.

위의 예를 보면, setTimeout 이 실행된 이후, 3초동안 다른 작업을 수행하다가, 3초가 지나면 callback 함수에 해당하는 console.log('setTimeout 작동!') 이 실행된다.

💡참고)
callback함수의 작동 원리가 궁굼하다면
어쨌든 이벤트 루프는 무엇입니까? | Philip Roberts | JSConf EU
이 유튜브를 확인해보자! - 한국어 자막 지원

이렇게 setTimeout 등을 사용해 자바스크립트엔진이 유동적으로 일하도록 할 수 있지만, callback에는 치명적인 단점이 있는데, 우리는 그것을 Callback 지옥이라고 부르기로 했다.

Callback 지옥

Callback을 사용하여 비동기적 작업 처리를 구현해야되는데, 그를 위해서 callback이 한 3~4개가 필요하다면, 아래 예시와 같은 코드가 나올 것이다.

비동기적으로 실행!! (function 콜백() {
  비동기적으로 실행2 (function 또다른 콜백(){
    비동기적으로 실행3 (function 또또다른 콜백(){
    	비동기적으로 실행4 (function 또또또다른 콜백(){
        	console.log(`차라리 날 죽여줘 ㅠ_ㅠ`)
          		비동기적으로 실행.....
        })
    })
  })
})

출처 : two_jay.log

이렇게 미친듯이 중첩된 callback 함수를 콜백 지옥(callback hell)이라고 부르는데, 그 이유는 가독성이 매우 떨어지기에, callback지옥 내부에서 어떤 에러가 발생했을 때 찾아내기가 정말 어렵다!

(그런 코드들을 구경하고 싶다면 http://callbackhell.com 여기로,,)

callback이 매력적임에도 불구하고 callback지옥이라는 크리티컬한 단점이 있기에, 이를 보완하기 위해서 promise라는 오브젝트가 등장하게 된다.

Promise

자바스크립트에서 비동기 task를 다룰 수 있게 도와주는 객체로, 비동기적 작업을 하는 함수의 return type으로 사용 된다.

함수 안의 작업이 성공적으로 동작했을 때 동작하는 'resolve' callback과 함수가 동작이 실패해서 에러가 발생했을 때 동작하는 'reject' callback을 인자로 받는 함수를 실행한다.

// 기본적으로 PROMISE는 인터페이스와 같으므로 new 생성자를 이용합니다
// Promise는 resolve와 reject에 해당하는 callback 2개를 받는
// 함수를 인자로 받습니다.
let promise = new Promise(function(resolve, reject) {});

//기본예제1
let myFirstPromise = new Promise((resolve, reject) => {  
  setTimeout(function(){
    resolve("Success!"); // resolve callback만 구현한 예제
  }, 250);
});

출처 : MDN

Promise는 처음에는 함수가 실행되기를 쭉 기다리고 있는 Pending 상태였다가, 함수가 잘 작동이 되면 resolve 가 되면서 resolve callback을 실행시키며 프로미스를 종료하고, 함수가 작동하다가 에러가 나면 rejected 상태가 되면서 reject callback을 실행한다.

기존 callback 사용시와 차이가 있다면, 기존에는 resolve, reject 각각의 상황에 대응하는 함수를 직접만들어 대입을 했어야 했지만, Promise의 경우 dafault로 resolve, reject callback method가 내장되있어 보다 편리하고 심플하게 사용할 수 있다는 장점이 있다.

// callback을 사용한 예제
function do_something_async(if_success,if_fail) {
	// 좀 많이 무거운 작업...
  	// 상황에따라 callback를 실행
}

function if_success(){
	// 어쩌구 저쩌구
}
function if_fail(){
	// 어쩌구 저쩌구
}

// Promise를 사용하여 보다 간단하게 구현할 수 있다
function do_something_async() {
	return new Promise(function (resolve, reject){
    	// 좀 많이 무거운 작업...
    }
  if(success){
    	resolve();
    } else {
    	reject();
    }
}

출처 : two_jay.log

.then(), .catch()

Promise를 사용하면 함께 사용할 수 있는 두가지 무적의 메소드 .then().catch() 가 있다.

Promise 사용 시 이 두가지를 가지고 callback 지옥없이! 연속적으로 비동기적인 동작을 하는 코드를 작성이 가능하다!

.then() 의 경우, 앞의 데이터를 받아온 다음에(then) 그 값으로 무언가를 실현한다는 것으로, chaining을 통하여 callback을 구현한 것으로 볼 수 있다.

.catch() 의 경우, 앞의 데이터 중 error만을 받아와(catch해) 그 값으로 무언가를 실현하는 것으로, error 상황에 특정한 callback을 구현한다.

our_work.then((...) => {
  console.log(...)
  return callback_on_success1()
})
	.then((......) => { //callback_on_success1()의 return 값을 가지고 진행
  console.log(......)
  return callback_on_success2()
})
	.catch(error => {
  console.log(error)
  return reject()
})

출처 : two_jay.log

⚠️ 주의사항

Promise를 .then으로 체이닝해서 연속적으로 실행 가능하다는 점을 보면 알듯이, Promise는 Promise를 반환한다 (.then과 .catch를 포함한 Promise 관련 메소드들도 포함됨).

⇒ 이로 인하여 위험한 상황이 발생하는데, Promise를 얹고 얹으면서 반환값이 중간에서 꼬여버린다면, 콜백지옥은 코드를 스윽 읽어라도 볼 수 있지만, Promise는 일일히 반환값을 디버거로 뜯어봐야한다..!

중간에 다른 값이 필요할 때에도 callback과 마찬가지로 코드를 읽기 어렵도록 하여 가독성을 해친다.

→ 결국) Promise의 then과 catch가 쌓이면 가독성이 떨어지며 디버깅도 어려워진다.

const function work_with_promise(){
promise.then(data => {
	....
    //난 지금 여기에 다른 프로미스를 가져오고 싶은걸?
    return another_promise(data).then(data1 => 
   	....
    //앞의 두개의 결과값을 받는 새로운 프로미스를 또 써먹고 싶은걸? (점점 꼬여간다앗~~?!)
	return new_promise(data1, data2)
    })
	// -_-;;;;;;;;;
    .catch....
  })
}

출처 : two_jay.log

async / await

이전의 callback과 Promise보다 비동기 작업의 실행을 좀 더 유연하고 '일반함수'적으로 실행하도록 하기위해 제안된 객체가 바로 "async"이다.

비동기 작업을 수행하지만 겉보기에는 '일반함수' 같아보이기에 사용하기 더 편하다.

async function 함수명() {
  await 비동기_처리_메서드_명();
  // 그 뒤에 다른 것을 뚜닥뚜닥하면 됩니다.
}

출처 : two_jay.log

사용 방식은 간단히 해당 함수의 선언자 function 앞에다가 async를 붙여주면 된다(선언).

그리고 비동기작업이 필요한 작업 앞에다가 await를 붙여준다. 이때 await 은 자바스크립트가 promise가 실행되어 그 결과값을 받아 올 때까지 기다린다.

async/await의 매력은 다수의 비동기작업을 처리할 때 나타난다.

Promise.all과 같이 복잡한 로직을 생각하지 않고도 아래처럼 다수의 비동기작업의 결과들을 이용한 작업을 심플하게 수행할 수 있기 때문이다.

// 3곳의 url에서 정보를 가져와서 json변환 뒤 객체를 만들어 리턴을 해주는 함수
// data - news - weather를 가져와서 해당 정보를 객체화한다.
async function get_resources(){
  //마찬가지로 셋 다 서버에서 완전히 데이터를 받아와야 밑에 제대로 뜨겠죠?
	//즉, await이 걸려있는 fetch함수가 끝날때까지 대기를 하다가, fetch함수의 실행결과를 받아오면 then을 실행합니다
  let data_json = await fetch(data_url...).then(response => response.json())
  let news_json = await fetch(news_url...).then(response => response.json())
  let weather_json = await fetch(weather_url...).then(response => response.json())

    return { //  async에서 return이후의 값은 모두 resolved promise로 인식된다
    	data : data_json;
      	news : news_json;
      	weather : weather_json;
    }
  // 임의로 설정한 예제입니다.
  // 디테일은 데이터 저장 양식과 response에 따라 달라질 수 있습니다.
}

출처 : two_jay.log

이렇듯 async로 "이 함수에는 비동기적으로 실행되는 작업이 포함되어 있습니다"라고 선언하고, 비동기적 실행이 필요한 곳에만 await처리를 함으로서 일반적인 함수 실행/선언 환경처럼 편리하게 비동기 프로그래밍이 가능하다.



Summary

  1. 동기 함수 : 순서대로 진행
  2. 비동기 함수 : 순서대로 진행되지 않고, 기다려야하는 일이 있으면 기다리는 동안 다른일을 처리한다
  3. 콜백 : 어떤 이벤트가 실행되고나서 호출되는 함수(비동기함수의 일이 끝나고 호출되는함수), 인자값으로 들어가는 함수
  4. promise : 자바스크립트에서 비동기 task를 다룰 수 있게 도와주는 객체
  5. async/await : Promise보다 비동기 작업의 실행을 좀 더 유연하고 '일반함수'적으로 실행하도록 해주는 함수



참고자료

profile
쉽게만 살아가면 재미없어 빙고!

3개의 댓글

comment-user-thumbnail
2021년 7월 7일

어려운 개념들을 쉽게 설명해주시는 것 같아 많이 도움이 됩니다!! 구독 버튼이 없는게 아쉽습니다🤣

답글 달기
comment-user-thumbnail
2021년 7월 7일

비동기적 프로그래밍을 해야하는 이유와 방법이 일목요연하게 정리되어 있네요! 감사합니다!

답글 달기
comment-user-thumbnail
2021년 7월 16일

감사합니다 !

답글 달기