[part5] JavaScript 비동기

누리·2023년 8월 16일
0

Interview

목록 보기
10/13

💡 물어보는 이유

  1. 단일 스레드 환경 : JavaScript는 기본적으로 단일 스레드 (single-threaded) 언어입니다.
    즉, 한 번에 하나의 작업만 처리할 수 있습니다. 이러한 환경에서 비동기 프로그래밍은 여러 작업을 효율적으로 처리하거나,
    비용이 많이 드는 작업을 백그라운드에서 실행하여 사용자 인터페이스가 멈추지 않게 하는 데 필요합니다.
  2. Web API 와의 상호작용 : 웹 어플리케이션에서는 네트워크 요청, 타이머, 이벤트 핸들러 등과 같은 비동기적인 동작이 일반적입니다.
    이러한 작업들은 JavaScript의 비동기 모델을 이용하여 처리됩니다.
  3. 콜백, 프로미스, async/await : JavaScript에는 비동기 코드를 작성하기 위한 여러 방법이 있습니다. 이에대한 이해는 코드의 동작을 정확히 이해하고 예측할 수 있도록 돕습니다.
  4. 성능과 사용자 경험 : 비동기 프로그래밍은 어플리케이션의 성능을 향상(TTI, TTV)시키고, 사용자 경험을 개선 하는 데 중요한 역할을 합니다.
    잘 작성된 비동기 코드는 웹 어플리케이션을 더 반응적으로 만들어 주고, 느린 네트워크 연결이나 고비용의 연산이 어플리케이션의 반응성을 저하시키는 것을 방지할 수 있습니다.

비동기 프로그래밍에 대한 깊은 이해는 JavaScript 개발자에게 중요한 역량 중 하나입니다. 이는 코드를 더 효과적으로 작성하고, 복잡한 문제를 해결하며, 성능과 사용자 경험을 개선하는 데 필요한 기술입니다.

들어가기 전

Promise 핵심

  • rule1. Promise를 쓴다는 말은 곧 Promise 객체를 return 하는 것과 같은 말이다.
const myPromise = new Promise((resolve, reject) => resolve(리졸브 해야하는 콜백함수))
  • rule2. then에 전달되는 함수가 받는 parameter는 resolve에 넣은 value가 전달된다.
const url = 'http://api.coinpaprika.com/v1/coins';
fetch(url); // Promise { <pending> } 프로미스 객체 리턴
fetch(url)
  .then((res) => res.json()) // 프로미스 객체를 json()으로 사용할 수 있게 함
  .then((data) => 
        console.log(data) // {데이터 객체}
); 
//즉 then의 res에는 fetch(url)의 리턴값인 프로미스 객체를 파라미터로 받는다
  • rule3. Promise를 리턴하는 함수를 실행하게 되면 리턴값은 당연히 Promise 객체가 즉시 리턴된다.
    이 Promise 객체가 pending, fulfilled, rejected 되는 것은 그 다음 문제이다.
  • rule4. New Promise()를 하게 되면 즉시 Promise에 전달 된 executor 함수가 실행된다.
  • rule5. 마찬가지로, Promise를 리턴하는 함수를 실행하게 되면 바로 Promise에 전달 된 executor 함수가 즉시 실행된다.
  • rule6. Promise chaining을 하게 되면 then()에서 return한 값을 그 다음 then()에 전달되는 함수(executor)의 parameter로 들어 가게 된다.
  • rule7. Promise chaining을 할 때 then은 value를 바로 return 해도 되고, Promise를 return 해도 된다.
    value를 리턴 할 경우엔 즉시 그 다음 then에 전달된 function이 즉시 실행되며,
    Promise를 return 할 경우 해당 Promise가 resolve 되면 그 다음 then 이 실행된다.
  • rule8. 동시에 여러 Promise를 실행 시킬 때는 Promise.all()을 자주 사용한다.

Async/Await 핵심

  • rule1. async function을 만들면 javascript/typescript 내부적으로는 자동으로 Promise 객체를 리턴하게 된다. 즉, 자동으로 Promise를 return 해주게 된다.
const myAsync = async () => {
  return 3;
}
console.log(myAsync()); // Promise { 3 }
console.log(myAsync().then(console.log)); // Promise { <pending> } // then으로 인해 3을 리턴하게 된다

const url = 'https://api.coinpaprika.com/v1/coins';
const url2 = 'https://api/coinpaprika.com/v1/coins/btc-bitcoin';
// 2개 이상의 url에 fetch 해야할 때
// 1. fetch 사용
// fetch(url1).then().then();
// fetch(url2).then().then(); 

//2. async await 사용 (즉시 실행함수)
(async() => {
  // 불편한 통신
  // const res1 = await fetch(url1);
  // const res2 = await fetch(url2); // 문제점 : await가 걸려있어서 res1의 data가 fetch 될때까지 res2가 실행되지 않음
  
  // 2개 url의 fetch를 동시에 할 수 있음
  const [res1, res2] = await Promise.all([fetch(url1).then((res) => res.json()), fetch(url2).then((res) => res.json())]); 
  
})()
  • rule2. async function을 await 없이 실행하더라도 Promise를 return 한다. await가 없기 때문에 기다림 없이 Promise를 즉시 리턴받게 된다. (프로미스 룰 4,5와 동일)
  • rule3. 위 rule2를 사용하는 사람도 있겠지만 웬만하면 Promise.all() 을 이용해서 배열화 시킨다.

👯‍♀️ 등장인물

  1. 콜백 함수(Callbacks): 콜백 함수는 특정 작업이 완료된 후에 실행될 함수입니다. 이는 JavaScript에서 가장 오래된 비동기 프로그래밍 방식 중 하나입니다. 비동기 작업이 완료되면 콜백 함수가 호출되어 결과를 처리합니다. 그러나 콜백 함수의 중첩 사용은 코드가 복잡해지고 가독성이 떨어지는 콜백 지옥(callback hell)을 초래할 수 있습니다.
  2. 프로미스(Promises): 프로미스는 비동기 작업의 최종 완료(또는 실패) 및 그 결과 값을 나타내는 객체입니다. 프로미스는 성공적으로 완료되었을 때(resolve), 혹은 어떤 이유로 실패했을 때(reject) 호출되는 두 개의 콜백 함수를 가집니다. 프로미스는 콜백 지옥 문제를 해결하며, 체이닝(chainable)이 가능하여 여러 비동기 작업을 순차적으로 처리하는 것이 가능합니다.
  3. async/await: async/await는 ES2017에서 도입된 비동기 코드를 동기적인 방식으로 작성할 수 있게 해주는 구문입니다. async 함수는 항상 프로미스를 반환하며, await 키워드는 프로미스가 settle될 때까지 기다리는 역할을 합니다. 이를 사용하면 비동기 코드를 마치 동기 코드처럼 읽고 작성할 수 있어 가독성이 높아집니다.
  4. Event Loop: JavaScript의 비동기 동작을 이해하기 위해서는 이벤트 루프에 대한 이해가 필수적입니다. 이벤트 루프는 호출 스택이 비어 있을 때마다 태스크 큐에서 태스크를 가져와 호출 스택에 넣는 역할을 합니다. 이를 통해 JavaScript는 단일 스레드 언어임에도 불구하고 비동기적인 동작을 지원합니다.
  5. Microtask and Task: JavaScript에서는 이벤트 루프에서 처리되는 태스크를 마이크로태스크와 매크로태스크로 분류합니다. 프로미스의 then 메서드에서 반환되는 작업은 마이크로태스크로 분류되며, setTimeout 등의 타이머 함수는 매크로태스크로 분류됩니다. 이벤트 루프에서는 마이크로태스크가 매크로태스크보다 우선적

기초 CS 지식 (쓰레드)

Thread란? 일 처리를 하는 하나의 Line이다.

동기 VS 비동기

왜 비동기가 필요한가??

서버와의 통신(네트워크 작업)이 가장 큰 요인이다.
비동기로 처리를 하게 되면 기다리지 않아도 된다!!
처음 들어온 작업을 다른 쓰레드에서 하도록 시킨다. 이 때 Pending이라는 상태값으로 Promise를 바로 리턴을 시키고, 그 작업이 끝나길 기다리지 않고 다음 작업을 진행한다.

예시 : 배달의 민족 주문시 발생

  • 접수 대기 중을 띄워주며 접수를 받았을 때 즉시 Promise를 던져준다. ex) 예상 배달시간 xx분 (pending)
  • 접수를 받지 않았을 때 reject를 던져준다. ex) 배달 주문이 취소되었습니다. (reject)
  • 모든 프로세스가 완료 되었을 때 내가 시킨 음식을 resolve 해준다. (resolve/fulfilled)

자주 실수하는 내용 : 밥이 아직 안왔는데 밥을 먹는다 라는 명령을 시킬 때가 있음

const getMeal = async () => {
  return axios.get('https://배달의민족주문').then(res => res.data)
}
const eatMeal = (meal) => {
  // do something
}

// 실수 
eatMeal(getMeal()) // <Promise>를 meal에 던지고 있다 (밥이 아직 안오는데 먹으라 함)

// 정답
getMeal().then(meal => eatMeal(meal));
getMeal().then(eatMeal) // 보내는 인자와 받는 인자가 같을 때는 생략이 가능

그렇다면 동기(Sync)는 무엇인가?

작업을 다른 쓰레드에서 하도록 시킨 후, 그 작업이 끝나기를 기다렸다가 다음 작업을 진행한다.

Serializing 처리 vs Concurrent 처리

  1. 직렬처리(Serial) : 다른 한개의 Thread에서 순서가 중요한 작업을 처리할 때 사용
    then chaining 사용하는 것 : 직렬성 처리라고 볼 수 있다
  1. 동시처리(Concurrent) : 다른 여러개의 Thread에서 각자 독립적이지만 유사한 여러개의 작업을 처리할 때 사용
    fetch 메소드를 사용하는것 : 동시성 처리라고 볼 수 있다

분산 처리 시 동시처리가 더 좋아 보이는데 왜 직렬처리가 필요한가?
작업에 순서가 필요할 수 도 있기 때문이다

비동기와 동시는 같은 말인가?

  • 비동기 : 작업을 보내는 Main Thread가 주체이고
  • 동시처리 : 작업을 받는 Thread가 주체이다.

엄연히 다른 Thread임

메인 스레드가 하위 스레드들을 관리하는 역할을 할 수 있다.

JavaScript 비동기란

전체적 흐름 : 비동기로 작업 처리 요청을 하면 > Callback(이거 했을때 이거 해줘) > 너무 길어지면 보기가 힘들어짐 > Promise(데이터 준다고 약속할게, 성공/실패 가능)가 생김 > asnyc (return 값으로 Promise 줄게) > await (async function 내에서 이거 될 때 까지 내 밑에서 기다려! 하고 싶을때)

Callback

  • 정의 :: 함수에 파라미터로 들어가는 함수
  • 용도 :: 할 때 ! ➡️ 이거 해라 같이 순차적으로 실행하고 싶을 때 사용
getUser('a01043340999@gmail.com', function(user) {
    getPosts(user.id, function(posts) {
        getComments(posts[0].id, function(comments) {
            console.log(comments);
        }, function(error) {
            console.error("Error:", error);
        });
    }, function(error) {
        console.error("Error:", error);
    });
}, function(error) {
    console.error("Error:", error);
});

Promise

Callback이 너무 심하게 들어가는 현상을 방지하기 위해 나온 Promise

  • State :: process 실행 중(pending) or 성공(fulfilled) or 실패(reject)
  • Producer, Consumer :: 정보 제공자와 사용자

0. state

  • fulfilled
    Promise가 resolved 됐을 때 이다. 잘 처리 되었고, Promise 안에서 에러가 발생하지 않았을 때 이다.
  • reject
    Promise 장비를 정지합니다.
  • pending
    Promise가 아직 대기 중 입니다.

    pending => fullfilled || rejected

1. Promise 만들기

const Promise = new Promise()
새로운 Promise가 만들어 질 때에는 우리가 생성한 executor 함수가 바로 실행된다.

// fetch는 어차피 Promise를 반환하기 때문에 따로 Promise를 만들어 줄 필요가 없기 때문에 
// 불필요한 코드지만 예시를 위해 사용했습니다.
const promise = new Promise((resolve, reject) => {
  // 뭔가 헤비한 일들 (데이터를 가지고 오거나, 큰 데이터를 읽어올 때)
  // resolve 안에 우리가 비동기적으로 해야 하는 일을 넣어준다.
  resolve(fetch('https://api.coinpaprika.com/v1/coins').then(res => res.json()));
});

2. Promise 사용하기

promise.then((data) => console.log(data))

3. 기타 에러 핸들링

  • Producer 쪽에서는 에러가 발생 했을 때 해주고 싶은 처리를 reject에 적어주고
  • .catch를 이용하여 reject된 정보를 가져온다.
    .finally는 resolve / reject 상관 없이 실행된다.
const promise = new Promise((resolve, reject) => {
  // 뭔가 헤비한 일들 (데이터를 가지고 오거나, 큰 데이터를 읽어올 때)
  resolve(
    fetch('https://api.coinpaprika.com/v1/coins').then((res) =>
      res.json()
    )
  );
  reject(new Error('no network'));
});

promise
  .then((data) => console.log(data.slice(0, 100)))
  .catch((err) => console.log(err))
  .finally(() => console.log('아 시원하다 끝났다')); //reject resolve 결과와 상관없음

4. Promise Chaining

then으로 계속 직렬처리 방식으로 처리됨

fetchCoins
  .then((data) => data.slice(0, 100))
  .then((data) => data.map((item) => item.name))
  .then(
    (names) =>
      new Promise((resolve, reject) => {
        resolve(names.slice(0, 30));
      })
  )
  .then((data) => console.log(data));

.then 에서는 값을 바로 전달해도 되고 새로운 promise를 전달해도 된다

5. 오류 처리

const getHen = () =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve('🐓'), 500);
  });

const getEgg = (hen) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${hen} => 🥚`), 500);
  });

const cook = (egg) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(`${egg} => 🍳`), 800);
  });

getHen()
  .then(getEgg)  // .then(hen => getEgg(hen))
  .then(cook)    // .then(egg => cook(egg))
  .then(console.log); // .then(result => console.log(result))
                      // 이런식으로 하나의 인자가 들어가고 함수가 하나의 인자를 받으면 callback 함수 데려오기만 해도 됌
// 🐓 => 🥚 => 🍳

getHen() //
  .then(getEgg)
  .catch((err) => '🧐')
  .then(cook)
  .then(console.log)
  .catch(err => '에러임다~~');

// 에러가 났을 경우 다음과 같이 핸들링 ::  🧐 => 🍳

JavaScript Environment에선 어떤식으로 처리되고 있을까?

Async Await

💡 Promise Chain을 더 간결하고 동기적으로 실행되는 것 처럼 보이게 만들어 주고 싶을 때 사용
상황에 맞게 Promise와 적절히 섞어 사용하는 것이 좋다.

1. async

동기적으로 만들때 자주하는 실수

const fetchCoins = () => {
  return fetch('https://api.coinpaprika.com/v1/coins').then((res) => res.json());
};

const data = fetchCoins();
console.log(data);  // undefined

이 때 네트워크 패널에서는 데이터가 페칭 되어있다.

해결방법 : 함수 앞에 async를 붙히자

const fetchCoins = async () => {
  return fetch('https://api.coinpaprika.com/v1/coins').then((res) => res.json());
}
const data = fetchCoins();
console.log(data); // Promise 객체 반환

async를 함수 앞에 붙혀주면 자동으로 해당 함수는 Promise의 resolve를 뱉어내는 함수가 된다.

2. await

async가 붙은 함수 안에서만 쓸 수 있다.

const fetchCoins = async () => {
  const data = await fetch('https://api.coinpaprika.com/v1/coins').then((res) =>
    res.json()
  );

  return data;
};

fetchCoins().then(console.log); // async 함수는 Promise를 리턴한다 (.then 처리 필요)

await의 강점 : Promise 안에서 콜백지옥을 이쁘게 처리 해준다

// await 를 쓰지 않고 Promise Chaining했을 경우
// 물론 Promise.all 을 써도 좋음 (사용 방법에 따른 차이)
const getAll = () => {
  return fetchCoins().then((coins) => {
    return fetchMarketOverview().then((global) => { coins, global });
  });
};

getAll().then(console.log);

// await 를 사용 한 경우
const getAll = async () => {
  const coins = await fetchCoins();
  const overview = await fetchMarketOverview();
  return { coins, overview };
};

getAll().then(console.log);

async / await 함수 플로우

  1. 먼저 Before function이 콘솔창에 출력되고 myFunc()가 콜스택에 쌓인다. myFunc()내의 In function!이 이때 출력된다.

  2. myFunc()은 async 함수이므로 await 를 만나고 queue로 빠지게 된다. (queue는 콜스택이 비어있을때만 들어온다)

  3. queue는 콜스택이 비어있을때만 콜스택으로 들어오기 때문에 myFunc()바깥의 console.log('After function')을 콜스택에 먼저 올리고 빠져 나간다.

  4. 콜스택이 완벽히 비어있으므로 queue에 있던 myFunc()을 콜스택으로 올린다. one()을 받고 console.log(res)로 빠져나간다.

문제점

1. await 병렬처리

await를 사용하면 순차적으로 진행할 때 비효율적일 수 있다.
위의 예시에서 coins 의 정보를 받아오는 것과 overview 정보를 받아오는 것이 직렬적으로 처리 되고 있고, 둘은 서로 동시성 으로 처리 해도 되는 것이기 때문에 비효율적이다.
2개의 url 직렬처리가 되어 있는 코드 >> 순서에 상관없이 독립적으로 일어나는 일들은 동시성으로 처리하면 좋아 보임

  • Promise를 바로 만들어서 동시성으로 fetch 되게 하고 그 내용을 await 하여 병렬처리
    그런데 쓰다 보니 너무 지저분
const fetchCoins = async () => {
  return fetch(baseUrl).then((res) => res.json());
}
const fetchMarketOverview = async () => {
  return fetch(overviewUrl).then((res) => res.json());
}

const getAll = async () => {
  const coinsPromise = fetchCoins();
  const overviewPromise = fetchMarketOverview();
  const coins = await coinsPromise;
  const overview = await overviewPromise;
  return {coins, overview}
}

2. useful Promise APIs

  • Promise.all([비동기작업1, 비동기작업2, ...]) 사용
    위의 코드보다 훨씬 짧아진 것을 볼 수 있다.
const getCoins = fetch(baseUrl).then(res => res.json()).then((data) => data.slice(0,3));
const getOverview = fetch(overviewUrl).then(res => res.json());

const getAll = async () => {
  const [coins, overview] = await Promise.all([getCoins, getOverview ]); // 구조분해 할당
  console.log(coins);
}
  • Promise.race
const pickOnlyOne = () => {
  return Promise.race([fetchCoins(), fetchMarketOverview()]).then(
    (data) => data
  );
};
profile
프론트엔드 개발자

0개의 댓글