Stevy's wavyLog 🌊

자바스크립트 비동기 처리 가이드

April 2021

내가 처음 겪은 비동기에 대한 경험

  내가 처음 자바스크립트를 배울 때의 일이다. 나는 생활코딩과 노마드코더의 튜토리얼로 가장 처음 자바스크립트를 배웠다. 데이터를 오픈 API로부터 AJAX 호출로 받아와서 리스트를 그리는 코드였다. Fetch API를 써서 AJAX 호출을 했는데 그때 제일 처음 비동기와 마주했다. 호출 후에 할당한 변수의 값이 undefined가 뜨는 걸 보고 약간의 충격을 받았었다. 선생님이 알려준 방법은 async awaitPromise를 써서 해결하는 것이었다.

기도

Promise는 너무 어렵게 생겼기 때문에 대충 다른 코드와 형태도 비슷하고 최신 스펙인 async await 를 써서 기도하는 마음으로 해결하고 넘어갔다. 왜 API 호출에서만 그렇게 쓰는지는 대충 네트워크 호출은 느려서 그렇다고만 이해를 했었다. 지금와서 조금 더 자바스크립트 생태계를 이해하게 된 지금에서 생각해보면 튜토리얼에서 다루기는 너무 큰 개념이기 때문에 깊은 설명 없이 지나 갔던 거 같다. 그 이후로 비동기를 조금 더 가까이 받아 들이기 까지는 2년 가까운 시간이 걸렸는데 왜 이렇게 오랜 시간이 걸렸는지와 그 과정 속에서 얻은 바 들을 적어 볼까 한다.

잘못된 방향의 비동기 대응

  보통 자바스크립트 초보자의 비동기 공부는 앞서 내가 쓴 Fetch API와 같이 Promise로 구현된 구현체들에 대한 대응을 위해서 훈련 된다. 비동기에 대한 개념 이해는 조금 나중에 실행하는 그런 느낌만 가지고 간다. 당장 구글에 비동기를 키워드로 검색을 해봐도 저런 느낌을 주는 짧은 블로그 글들이 대부분이기 때문에 자바스크립트 비동기처리 에 대한 이해를 확장 시키기 는 어려웠다. 특히나 비동기 상황 대응에 대한 훈련에 있어도 Promise는 생긴 것도 무섭게 생겨서 async await에 절대적으로 의존하며 상황에 맞지 않는데도 남발하고 Promise는 구닥다리 취급하면서 개발을 하게된다.

비동기는 왜 필요할까

  자바스크립트 생태계는 어렵다. 내가 배웠던 C 와 같은 컴파일이 필요한 언어와 다르게 자바스크립트는 많은 로직들이 런타임 중에 추가되고 실행되는 경우가 많다. 또 브라우저에서 싱글 스레드로 동작하기 때문에 적절한 테크닉을 이용한 동시성 프로그래밍이 이뤄진다. 사용자 경험을 높이기 위한 브라우저 환경에서 원활한 자바스크립트 실행이라는 목표를 이루기 위해서 비동기는 강력한 무기이다.

일반적으로, 프로그램의 코드는 순차적으로 진행됩니다. 한번에 한가지 사건만 발생하면서 말입니다. 만약 어떤 함수의 결과가 다른 함수에 영향을 받는다면, 그 함수는 다른 함수가 끝나고 값을 산출할 때까지 기다려야 합니다. 그리고 그 과정이 끝날 때 까지, 유저의 입장에서 보자면, 전체 프로그램이 모두 멈춘 것처럼 보입니다. - MDN

구체적으로 뭘 멈추고 어떻게 멈추는 걸까? 비동기를 제대로 알기위해 이해가 필요한 개념들을 써보고 또, 이해에 도움 됐었던 글들을 추천 해보겠다.

비동기를 이해 하기 위해 필요 했던 것들

Async, Sync, Block, Non blocking

  먼저 Async, Sync, Blocking, Non Blocking에 대한 이해가 필요하다. 일단 넷의 개념을 이해한 뒤 넷의 개념이 각각 섞여서 2 x 2 로 4가지 상황을 만들 수 있음 을 알아야한다. 코드가 제어권을 어떻게 전달하고 다시 받아오는지 또 결과 값은 어떻게 전달되는지 이해가 필요하다. 이를 이해한다면 내가 쓴 코드가 블로킹을 만들고 있는지 알 수 있고 블로킹을 적절하게 회피하는 지 알 수 있다. 동기적인 블로킹을 막기 위해 비동기 로직을 썼는데 최악의 경우 비동기를 쓴 블로킹이 될 수 도 있기 때문에 이를 판단할 수 있는 눈이 필요하다.

이벤트 루프에 대한 이해

  두번째로 이벤트 루프에 대한 이해가 필요하다. 브라우저에서 비동기 로직을 쓰기 위해서는 PromisesetTimeout, requestAnimationFrame 등 과 같은 콜백 함수 를 이용 해야한다. 이런 콜백 함수를 이용하면 이벤트 루프가 계속 돌아가면서 브라우저의 TaskQueue와 JS 엔진의 CallStack 이 정해진 시점에 따라 서로 상호 작용하며 비동기 로직이 일어난다. 이들 사이에는 우선 순위도 있다. 내가 쓴 비동기 함수가 어떤 과정을 거쳐서 실행되는지 정확히 플로우를 그려보기 위해서는 이에 대한 이해 역시 필요하다.

브라우저 렌더에 대한 이해

  세번째로 브라우저 렌더에 대한 이해가 필요하다. 브라우저 렌더에 대한 이해가 있어야 렌더 프레임을 지키기 위해 비동기를 적절하게 사용하고 브라우저 에서 사용자 경험을 높이는 결과를 도출 할 수 있다. 브라우저는 이벤트 루프를 돌면서 콜스택에 쌓인 JS 실행 → 렌더 로직 을 반복한다. 이런 과정이 버벅임 없이 이뤄지려면 프레임과 동기화 되서 16ms( 60fps )라는 짧은 시간 안에 이뤄져야 한다( 수직 동기화 ). 비동기 로직을 이용한다면 이벤트 루프 중 정해진 시점에서 큐에서 콜스택으로 로직을 push 하게 되고 실행 시킨다. 이때 이 로직이 무겁게 블로킹을 하며 60fps 를 방해하며 동작한다면 브라우저는 렌더를 멈추고 사용자는 화면의 버벅임을 느끼게 된다. 작업을 더 작은 task로 쪼거나 브라우저가 제공하는 쓰레드를 하나 더 쓰는 Web Worker 등을 통해서 비동기 로직을 계속해서 이어간다면 60fps 를 지키고 적절한 순간에 큰 성능 저하 없이 화면을 그려낼 수 있다. 또 requestAnimationFrame을 적절하게 이용해서 수직동기화 까지 해야 최적으로 의도한 순간부터 그릴 수 있을 것이다.

비동기를 잘 다루기 위해 필요한 것들

Promise

  비동기를 다루기 위해서 능숙한 Promise 사용은 필수다. 대다수의 라이브러리와 API가 Promise를 이용해서 비동기 처리를 하기 때문에 정확한 Promise에 대한 연습 없이는 다음 단계의 비동기 처리를 정확히 한다고 보기 어렵다. async await 같은 최신 문법의 경우도 내부에 Promise를 이용한 로직이 있기 때문에 Promise를 알아야 async await 도 알 수 있다. 쉽지는 않지만 정확한 Promise 이해를 위해 직접 구현해 보는 것도 추천한다.

Iterable과 Generator

  iterablegenerator은 그 자체로 비동기 로직을 만들지는 못하지만 동시성 프로그래밍을 도와주는 유용한 도구이기 때문에 연습을 많이 해야한다. 비동기를 이용할 때 generator를 이용하면 함수를 역할 별로 좀더 나눌 수 있고 제어권 반환에 유용해진다. 이런 점은 논블로킹 로직을 원할 때 이점을 준다. async await 같은 경우에도 babel로 치환해보면 generator 로직으로 제어권을 반환했다가 Promise resolveawait 설정한 라인으로 돌아오도록 설계 되어있다.

async await

  async await를 이용하게 되면 구문 상 동기적인 코드와 크게 다르지 않게 보이는 효과가 있어서 코드 형태가 좀더 깔끔해 지는 장점이 있다. async await를 잘못 이해 하게되면 비동기를 그냥 동기적인 구문과 함께 플로우가 흘러가는 것으로 오해 할 수 있는데 구문만 그렇게 보일 뿐 내부는 비동기를 도와주는 Promise 와 논블로킹 프로그래밍을 도와주는 generator를 섞은 복잡한 형태의 구문을 단순하게 쓸 수 있게 도와줄 뿐이다. 이를 제대로 모르고 쓰면 동기적인 플로우와 함께 쓸수있는 매직인냥 async 선언을 남발하게 되는데 async 선언은 동기와 비동기를 섞는 매직이 아님을 유의 해야한다.

async generator

  크게 쓸 일 이 많이 없을 수 도 있는데 async generator 를 쓰면 순차적인 비동기 구문 이용 시 좀더 역할 별로 함수를 나눌 때 쓰일 수 있다. generator 에 단순히 async 를 합성해 놓은 느낌이라서 위의 도구들을 잘 이해한 개발자라면 크게 어려움 없이 사용할 수 있을 것이다.

for await of

  for await of 계속해서 next에서 Promise 반환이 되는 iterator를 좀 더 깔끔하게 다루기 위해 생겨났다. async generator와 마찬가지로 위의 도구들을 잘 이해한 개발자라면 크게 어려움 없이 사용할 수 있을 것이다.

  • 추천글 및 강의

복잡한 비동기 상황의 예

순차적으로 api 호출 원하는 경우

  여러 개의 api를 호출해야할 경우 Promise.all 이나 Promise.allSettled 를 쓰려고 하는 경우가 많겠지만 동시성 프로그래밍의 관점에서 봤을때 앞의 두 구문은 병렬적 프로그래밍에 가깝다. 순차적으로 Api를 호출하고 적용하고 순차 적용하기 위해서는 async await , for await of , async generator를 이용하면 능숙하게 해결 할 수 있다.

시각적 변화 작업 왔을때 렌더를 방해하는 애니메이션 과부하 연산

  어떤 시점에 브라우저 렌더링을 막는 블로킹을 일으키는 과부하 연산이 일어나면 역시 비동기 로직을 사용해서 해결할 수 있다. 렌더에 영향을 주는 연산이라면 setTimeout이 아니라 requestAnimationFrame 을 이용 해야 한다. setTimeout 을 이용하면 16ms 가운데서 로직을 시작해서 화면을 버벅이게 할수있다.

// 1000 단위로 쪼개서 프레임 시작마다 계산되는 함수

const MAX_LOOP = 1000;

const addAll = max => {
  const add = (i, result) => {
    let newResult = result;
    let finish = max - i > MAX_LOOP ? MAX_LOOP + i : max;
    while (i <= finish) {
      newResult += i;
      i++;
    }
    if (i < max) {
      requestAnimationFrame(() => {
        add(i, newResult);
      });
    } else {
      // 렌더 로직 실행
      console.log('newResult', newResult);
    }
  };
  requestAnimationFrame(() => add(0, 0));
};

addAll(100000);

마치는 글

  비동기는 단순한 Api 호출에서 부터 프레임 관리를 해서 사용자 경험을 높이는 부분까지 넓게 쓰이는 개념이다. 늘 공부하지만 헷갈리고 어렵다. 비동기 처리가 두려워 async await만 쓰고 도망가는 초보 자바스크립트 개발자가 있다면 이 글을 보고 조금 용기를 내서 비동기 처리를 정복 할 수 있는 실마리를 얻을 수 있었으면 좋겠다.

All rights reserved © Stevy Sung, 2024