프론트엔드의 비동기 란?

박병관·2022년 4월 30일
2

우아한Tech

목록 보기
17/17
post-thumbnail

순서
1. 프론트 엔드에서 비동기를 어떻게 처리해야 할까?
2. Promise와 Async Function은 어떻게 사용할까?

유튜브 [10분 테코톡] 📖 카일의 프론트엔드의 비동기

프론트 엔드에서 비동기를 어떻게 처리할까?

영상에서는 비동기를 가장 잘 나타낼 수 있는 단어를 간극(시간 사이의 틈)이라 한다
즉 현재시점과 나중의 시점에서 그 사이 어떤 틈이 존재할 수 있다는 얘기다

프론트 엔드에서 비동기 처리가 필요한 이유

프론트 엔드에서는 이 틈이 엄밀히 다루어져야 하는 이유를 생각해보면, 과거에 단방향의 정보 전달로만 하는 페이지에서 현재 사용자와 상호작용을 이끄는 앱으로 넘어온 흐름이다

그래서 사용자와 가장 밀접하게 닿아있는 프론트 엔드 영역에서는 주기적으로 발생하는 인터렉션을 처리, 연속적으로 변경되는 정보를 실시간으로 사용자에게 보여줄 수 있어야 했다

그러기 위해서는 모든 인터렉션을 들어온 그대로만 처리할 수는 없었다

타이머를 사용한 이벤트, 서버와의 네트워크 통신, 애니메이션
간극을 예측할 수 없는 여러 요인들이 생겨나면서 대기시간이 발생하게된다

이러한 반복적이고 긴 대기시간은 답답함을 느낀 유저의 이탈률은 빠르게 증가하고, 웹 이용률의 하락, 경제적 손실이 발생한다

그래서 프론트엔드에서 비동기 처리가 필요한 이유는, '무언가를 기다려야 하는건 유저가 아닌 브라우저의 역할'이 되기 때문이다

자바스크립트 개발자들은 꽤 오랜 시간 동안 비동기 처리를 위해 위와같은 콜백 함수를 사용해왔다
하지만 요즘은 단순히 이 방법으로만 비동기를 처리하지는 않는다

콜백 기반의 비동기 처리방식 한계(신뢰성)

영상에서의 제어의 역전 예시는 기업과 하청 업채에 비유했다
한 기업이 사내 프로젝트를 진행할 때 원자재를 하청 업체에 수주를 맡기는 방식을 채택(기업에서는 흐름에 집중할 수 있다)했을 때 , 여기서 기업은 하청 업체에 견적서를 통해 그 원자재로 수행해야할 기업의 다음 업무를 요청했다고 가정한다

이러한 상황에서 하청업체가 그 일을 부담하는 것을 약속한 상태이기 때문에 문제가 없어보이지만, 하청업체 내부의 변수로 중간 과정에 차질이 생겨도 기업은 처리 과정을 확실히 알 방법이 없다

만약 원하는 결과를 얻을 수 없어도, 그 사실을 알 수만 있다면 적절한 대비법을 알 수도 있었겠지만 불가능하다 = 신뢰할 수 없음

콜백 기반의 비동기 처리방식 대안점

위의 문제점을 해결하기 위해

  1. 기업이 하청업체에 원자재에 대한 요청만 견적서로 전달
  2. 그 수행결과를 가져와서
  3. 후순위 업무를 처리하는 방법
    이와 같은 방법을 생각할 수 있다

이런한 매커니즘이 Promise이다

프로미스에 대해 더 알아보면

  • ES6
  • 미래에 값을 반환할 수도 있는 함수를 캡슐화한 객체
  • 제어의 재역전?
  • 비동기 요청 수행에 대한 세 가지(성공, 실패, 대기)의 상태를 가지고 있다
  • 내부에서 비동기 요청이 끝나면 결과값을 연결된 콜백으로 보내줌

아래는 callBack을 사용했을 때의 코드이다

// callBack을 사용했을 때

function browserTasks(){
  // browerTask가 호출될 때 console.log(~)는 제어의 대상이 된다
  console.log('sync task');
  // 그에비해 비동기 요청과 함께 전달된 콜백은 
  // 외부라이브러리에 대한 의존성을 가지게 되어 제어권의 주체가 뒤바뀌게 된다
  asyncRequest(asyncTask);
}


function asyncRequest(callBackFn){
  ajax('url',function(data){
    callBackFn(data);
    // 이러한 비동기 요청,콜백 호출로 이어지는 흐름은 외부에서 관찰, 제어가 불가능하다
  })
}

function asyncTask(data){
  console.log(data)
}

아래는 promise를 사용해 변환한 코드이다

function request(){
  // promise를 만들 때 인자로 들어오는 함수를 
  // executor 함수 라고 하는데, 이 함수는 비동기 요청의 수행 결과에 따라
  // 넘겨줄 데이터를 콜백으로 받는다
  // !! 유의할 점은 execute함수는 Promise객체가 만들어지는 즉시 실행된다는 점이다
  // 그래서 버튼 클릭같은 반복적인 일에는 이와같은 방법이 적절하지 않다
  return new Promise(function(resolve, reject){
  ajax('url',function (data){
    if (data) {
      resolve(data);
    } else {
      reject('Error!');
    }
  });
 });
}

function asyncTask(){
  const promise = request();
  
  promise
    .then(function (data){
     // data를 이용한 작업수행
    })
    .catch(function(error){
     // error를 이용한 작업수행
    })
}

이 떄 프로미스의 장점과 고려할 점

장점

  • 제어권을 확보했기 때문에 콜백 방식에 신뢰할 수 없었던 여러 상황 대처
  • 체이닝을 통해 구조화된 콜백 작성 가능

고려할 점

  • Promise내의 연쇄적 흐름에 대한 예외처리가 어렵다
  • 단일 값 전달의 한계(여러 개의 값의 연관성이 부족해도 객체, 배열로 감싸야 한다)
  • 단순 콜백과 비교했을 때 성능 저하

콜백 기반의 비동기 처리방식 한계점(가독성)

콜백의 단점을 말할 때 가장 쉬운 예시인, 콜백지옥 이라 불리는 코드를 볼 수 있다

그러면 콜백 지옥은 단순히 코드가 길어서 읽기 힘든 것일까?

일정 부분을 차지하지만 오히려 비동기 방식 자체의 한계점이라 생각한다고 한다
카일님은 사람이 생각하는게 순차적이기 때문에 코드를 읽는 과정에서 중요한 전제라고 생각하고, 비동기 코드는 동작 방식의 특정상 직선적인 추론을 제시하지 못하기 때문에 "비동기 방식 자체의 한계점"이라 표현했다고 한다

function A(callback){
  	console.log('A');
  	setTimeout(() => callback(),0);
}

function B(){
  	console.log('B');
}

function C(){
  	console.log('C');
}

A(B);
C();

// 코드는 A - C - B 의 순서로 이루어 진다

이와같은 코드의 진행을 추론하기 위한 개발자의 디버깅 과정을 피로하게 한다

ES6 시기에 Promise의 구조화된 콜백으로 조금은 가독성을 챙길 수 있지만 비동기 코드 자체의 가독성 문제를 해결하진 못해서 개발자들은 동기코드처럼 보일 수 있도록 생각했다

Generator

Async Await이 등장하기 전, 이와같은 고민을 1차적으로 해결했던 방법은 Generator이다

Generator함수는 온전히 비동기 처리를 위해 나온 것은 아니지만, 비동기를 동기적으로 보이도록 하는데에 꽤 유용했다

Generator의 꼭 알아야 하는 특징은

  • * : gernerator함수를 작성하기 위한 규칙, function키워드 뒤나 식별자 앞에 선언
  • lterator : generator 호출로 반환된 객체, next()를 가지고 있다.
  • next() : generator 함수 안의 yield문으로 넘어가기 위한 메서드
  • yield : next()가 호출 되었을 때, 1.중간에 멈추고 2.데이터를 받는 지점
function *asyncTask(){
	const data = yield request();
  // 받아온 data를 이용한 일련의 작업 수행
}

function request(){
	ajax('url',function(data){
    	it.next(data);
    });
}

const it = asyncTask();
it.next();

generator를 이용하면 함수를 중간에 중단할 수 있다는 특징과, 함수의 중간 지점에서 값을 보낼 수 있따는 특징이 있어서 외부의 값을 기다렸다가, 받은 시점에서 함수가 실행되도록 만들었다

그래서 ES6개발자들은 generator와 promise를 같이 사용해서 비동기 코드의 가독성과 신뢰성을 향상시켰다

하지만 이 두 문법을 매번 섞어 써야하는 불편함이 있었고, 더 직관적이고 간결한 사용을 원하게 되었다

Async Function

  • ES2017에 등장
  • Syntatic Sugar(Promise에 generator를 더한)
  • 함수 내에서 await문을 만나면 함수의 실행을 일시 중지
  • await뒤에 있는 프로미스의 수행 결과 값을 받아 함수 재진행
async function asyncTask(){
  // await은 뒤에 있는 주체가 Promise일 때만 간극을 기다린다
  // 만약 함수 수행과정 중 에러가 난다면 해당 에러를 throw한 것과 동일한 동작 수행
	const data = await request();
  // 받아온 데이터를 이용한 일 수행
}

function request(){
	return new Promise(resolve => {
    	ajax('url',function(data){
        	resolve(data);
        })
    })
}

Async function의 특징을 보면 generator함수의 매커니즘과 닮아 있는 것을 확인할 수 있다
주석에 있는 await의 특징을 보면 async function을 사용하면 프로미스의 흐름을 관찰할 수 있기 때문에 가독성 + 폭넓은 예외처리가 가능해진 것을 확인할 수 있다

장점

  • await을 통해 반환 받은 것이 Promise의 수행된 값이기 때문에 외부에서 예외처리가 용이
  • 다른 방법에 비해 높은 가독성

단점

  • Promise에 대한 syntactic sugar이기 때문에 Promise에 대한 이해가 선행되어야 함
  • 하나의 함수 안에 다수의 Promise를 병렬적으로 처리할 수 없다
  • 경우에 따라 async키워드를 관련 함수마다 일일이 선언해야 할 수도 있다

결국 모든 상황에 적용 가능한 완벽한 비동기 처리는 없다

정리하며 더 궁금한 점, 느낀점 🙃

  • 현재 많이 사용되는 async function 를 사용하기 전 비동기처리 과정을 순차적으로 설명을 들은 느낌이라 현제 사용되는 async function 에 대해 이해하지 못했던 것('코드가 순차적으로 진행되는 것을 위한 거면 "동기"를 위한거 아닌가?'하는 고민 등) 들이 조금은 이해되는 것 같다

  • 비동기 처리를 많이 할 일이 없어서 그냥 async await 을 사용했었는데, 비동기 처리를 위해 발전해온 과정에서 장점만이 아닌 문제점도 같이 알 수 있어서 좋았다

profile
괴물신인

0개의 댓글