Frontend study 일지 #2 - 비동기 작업(event-loop) 편

이유미·2022년 10월 9일
0

frontend-study

목록 보기
2/5

< 비동기 작업에 대한 완벽 이해 >

앞서 목표였던 전역 상태 에러는 해결했지만 이후 같은 에러를 또 만들지 않기 위해서, javascript의 Promise, async-await같은 비동기 작업의 작동 순서와 이벤트 루프, 마찬가지로 비동기적으로 동작하는 react hooks에서 useState의 setter 함수도 완벽하게 이해하기 위한 마무리 포스팅이다.

< javascript 비동기 작업과 event-loop >

https://www.youtube.com/watch?v=8aGhZQkoFbQ&t=1595s => 이 영상이 진짜 쉽게 이벤트 루프와 렌더링까지 합쳐서 설명을 잘 해준다... 아래는 영상을 정리한 내용이다.

javascript app이 실행되는 바탕

  • v8 런타임(javascript 엔진! 크롬과 node.js에서 사용 중)

  • 브라우저는 web api & event-loop & task queue등을 가짐

  • web api가 뭐지? web api는 브라우저에서 자체 지원하는 api로 DOM 이벤트, ajax, setTimeout 같은 비동기 작업을 수행하는 api를 지원해줌

  • javascript의 memory heap? javascript 엔진 내부에는 call stack과 memory heap이 존재한다. memory heap에 대해서는 다음 포스팅에서 자세하게 다루겠다.

javascript는

싱글 스레드 = 싱글 콜 스택 = 한번에 하나씩만

call stack은

실행 순서를 기억하는 stack, 그러니까 그냥 실행할 함수들이 순서대로 쌓여있는 공간이라고 생각하면 된다.

일반적으로 call stack에서는 일어나는 것

함수 실행(=함수 호출) -> call stack에 함수를 넣음 -> 함수에서 리턴이 일어나면 -> call stack에서 해당 함수를 제거함

(실제 프로그램을 돌리면)
우선 코드 자체를 가리키는 main()을 call stack에 넣음 -> 코드를 쭉 읽어가며 함수를 정의함 -> 함수 호출 코드를 만나면 -> 호출한 함수를 call stack에 추가하고, 만약 이 함수에 콜백함수가 있으면 call stack에 쌓는 과정 반복 -> 함수 쌓기가 완료되면 쌓여있는 함수 코드 실행, 리턴을 만나면 함수가 콜 스택에서 제거되는 과정 반복 -> (if) console.log() 같은 경우 call stack에 쌓이고,실행되고,바로 제거됨 -> (if) 함수에 리턴문이 없어도 함수 마지막(})에 도달하면 암묵적으로 리턴하기 때문에 call stack에서 제거됨 -> (if) 함수가 자기자신을 콜백할 경우 무한대로 계속 call stack에 함수가 쌓이기 때문에 브라우저는 에러를 발생시키고 강제로 중지함 -> main()도 제거됨

에러가 발생해서 콘솔에 찍히면 에러 문구 밑에 쭉 나열된게 바로 call stack이다.

블로킹 코드

= 느린 코드
정확한 정의는 없다

블로킹? 네트워크 요청(http request get, post …), 이미지 로딩 같은 느린 작업이 (실행에 시간이 오래 걸려)call stack에 남아있는 것

=> 이것이 문제가 되는 이유는 브라우저에서 작업이 이루어지기 때문이다. 블로킹 현상 동안은 화면이 멈추고 이동,클릭도 안되고 아무것도 못하게 된다.

비동기 작업

이런 블로킹 현상을 없애기 위해 존재함

우리가 예시로 javascipt 메서드들을 사용할 때 블로킹 현상을 접하지 못하는 것도 거의 다 비동기적으로 동작하기 때문이다.

그렇다면 비동기적 콜백함수들은 call stack에서 어떻게 쌓일까?
기존 순서에서 마지막에 main()이 제거된 후 -> 콜백함수가 call stack에 갑자기 나타나서 쌓임 -> 실행 후 제거 됨

콜백함수가 갑자기 어디서 나왔는가?

이벤트 루프와 concurrency가 도움을 주었다.

(setTimeout 함수 사용시)
setTimeout이 call stack에 쌓이고 제거됨 -> 동시에 web api가 정해진 시간만큼 타이머를 작동시킴(콜백함수도 같이 전달됨) -> web api는 call stack에 갑자기 끼어들수 없다!!! 타이머가 종료해도 call stack에 갑자기 콜백 함수를 다시 쌓을 수가 없다는 뜻 -> 여기에서 task queue가 등장! 아까 타이머 종료 후호출된 콜백 함수가 task queue에서 대기를 타게됨 -> 이제 event-loop 순서?단계?에 도달한다. event-loop가 하는 일은 call stack과 task queue를 주시하는 것이다 -> call stack이 비어있으면 event-loop는 task queue의 첫번째 콜백함수를 call stack에 넣어줌 -> 콜백 함수가 실행되고 제거됨 -> 끝!
_setTimeout 0초를 사용하는 이유! 실행 순서를 조작하기 위해서

setTimeout의 의미는 '해당 시간 후에 실행하겠다'가 아니라 '최소 해당시간 동안 이후에 실행하겠다'가 된다!

렌더링

브라우저는 16.6ms마다 리렌더링을 하는데, 렌더링 작업이 render queue에서 대기하다가 call stack이 비워지면(함수 실행이 끝나면) 렌더링이 일어난다.

이것은 곧 call stack이 비워져있지 않으면 렌더링을 하지 않는다는 뜻이다 = call stack이 블로킹 되어 있으면 리렌더링이 이루어지지 않기 때문에 화면이 그대로 멈춤.
=> 비동기 작업을 해줌으로써 중간 중간에 call stack이 비워지는 타이밍이 생겨서 동기적 작업을 할 때 보다는 더 자주 렌더링이 일어나게 되고, 그에 따라 사용자는 동시에 다른 작업들도 할 수 있게 되는 것이다.

DOM이벤트 핸들러 같은 경우

web api에서 계속 대기하면서 이벤트가 발생할때마다 콜백 함수를 task queue에 전달 -> call stack이 비어있으면 전달되어 실행되는 방식이다.

< javascript Promise, async-await >

Promise

비동기 작업을 다루는 방식 중 하나. 작업의 진행 상태와 값을 지닌 객체이다.


const myPromise = new Promise(()=>{
  //...비동기 작업 코드...
  //성공시 
  resolve("success!");
  //실패시
  reject(new Error("error!"));
});

myPromise
  .then(value=>console.log(value)); //성공시 resolve()라는 콜백 함수를 호출하고
  .catch(error=>console.log(error)); //실패시 reject()라는 콜백 함수를 호출

async

Promise의 간편한 사용이 가능하다. new Promise()로 객체를 생성하지 않아도 Promise 객체를 반환한다.

await

async가 붙은 함수 내에서만 사용 가능하다. await 키워드는 비동기 작업 코드 앞에 붙이면 되며, 이 비동기 작업이 끝날 때까지 기다린다.

< react hooks에서 useState의 setter 함수 >

가끔 useState로 state를 업데이트 할 때, 의도한대로 되지 않는 경우가 있는데 이것이 바로 setter 함수가 비동기적으로 실행되기 때문이다.(자주 까먹는 부분이니 강조!!)

useState의 사용법은

const [count, setCount] = React.useState<number>(0);

과 같은데, 여기에서 count = class에서의 getter, setCount = class에서의 setter 가 된다.

react에서 리렌더링이 일어나는 조건 중 하나가 state가 업데이트 될 때인데, state를 업데이트하려면 setter를 사용해서 변경한다. -> setter가 과하게 호출되어 state가 업데이트 될 때마다 계속 리렌더링이 이루어지면 브라우저 입장에서 비효율적이다. -> 이것을 없애기 위해서 setter가 여러번 호출되어도 모두 취합해서 한번에 처리한 후 한번의 리렌더링이 일어나도록 만들었다.

그래서 위 예시에서 setCount(count+1) 즉, count값이 1씩 증가하는 setter를 100번 호출해도 count 값이 100이 되는 것이 아니라 1이 된다.

이 말은 useState에서 동일한 setter를 100번 호출하면 call stack에 100개가 다 쌓이지만 실제 state의 업데이트는 마지막 한번만 이루어진다는 뜻이 된다.

그렇다면 어떻게 의도한대로 setCount()를 호출할 때마다 count값이 1씩 증가할 수 있을까?
=> setter의 인자로 값이 아니라 콜백 함수를 전달 하는 것이다. => setCount((prevState)=>prevState+1)

setter에서 (prevState)=>prevState+1를 호출하게 되면 이 콜백함수가 task queue에 setter를 호출한만큼 쌓여서 대기하다가, call stack이 비워지면 순서대로 하나씩 전달되면서 state를 계속해서 업데이트 하게 된다.

< 전역 상태 관리 라이브러리에서의 비동기 작업 >

  • redux-saga
    따로 다른 포스팅에서 redux에 대해 자세하게 정리할 예정
  • recoil
    recoil에서의 비동기 작업의 경우 state(atom)을 getter로 가져다가 파생 데이터를 만들 수 있는 selector()를 사용하면 된다. recoil 공식 문서에 다양한 예시가 잘 나와있다.
    https://recoiljs.org/docs/guides/asynchronous-data-queries
profile
신입 프론트엔드 개발자 구직중

0개의 댓글