JS 비동기

gang_shik·2025년 3월 19일
0

JavaScript

목록 보기
19/23

비동기와 동기

  • Synchronous는 동기라고 하고 순차적으로 흐르는 케이스임, Asynchronous의 경우 비동기로 순차적인게 아닌, 실행 순서와 실행 Flow가 동시에 즉 병렬적으로 흐르는 케이스임
  • JS는 비동기 처리를 하는 경우가 많음
  • 동기는 결국, 코드가 그대로 쭉 실행되는 것을 동기라고 해석하고 볼 수 있음(어느정도 순서가 있게, 위에서 아래로 실행된다고 생각하는 등)
  • 하지만 그러지 않은 경우가 많은데 그럴 때 비동기의 개념이 들어감, 이 때 쓰레드를 알아야함
  • 쓰레드는 프로그램의 실행 단위라고도 함, JS는 싱글 스레드 언어임, 그래서 비동기로 실행되는 경우가 생기는 것임
// 아래 예시 자체가 비동기라고 볼 수 있음, 사용자가 버튼을 클릭할 때마다 함수를 실행시키는데, 그 클릭 시점은 알 수 없기 때문임
const btn = document.querySelector('button');

btn.addEventListener('click', () => {
  alert('You clikced me!');

  let pElem = document.createElement('p');
  pElem.textContent =
    'This is a newly-added paragraph.';
  document.body.appendChild(pElem);
});
  • 위의 케이스의 경우도 클릭시 alert이 뜨고 그 다음에 확인을 눌러야지 p element를 만들고, text content를 만든 뒤, document.body에 추가를 함
  • 이는 아무리 비동기 실행이더라도 싱글 스레드 언어이기 때문에 한 번에 한 작업만 하기 때문임, 한 작업만 하나의 main thread에서 작업을 할 수 있음
  • 아래의 경우에도, 코드가 순서대로 출력이 되는게 아닌 시작, 종료, SetTimeout으로 나옴
console.log('시작 ===');

setTimeout(() => {
    console.log('Set Timeout');
}, 1000);

console.log('종료 ===');

// 시작 ===, 종료 ===, Set Timeout 순으로 출력됨
  • 이처럼 싱글 스레드 언어이기에 그런식의 출력이 됨

Callback

  • 앞서 쓴, setTimeout은 비동기로 실행되고 싱글 쓰레드, 그리고 이벤트 루프로 처리되서 순서대로 실행이 되지 않는 것임
  • callback 패턴은 함수의 사용권을 위임하는 것임, 아래 예시처럼 filter를 흉내내면 callback으로 아래와 같이 실행되고 처리된다고 이해할 수 있음
const isFilter = function (ele, index, array) {
  console.log(ele);
};

[(1, 2, 3)].filter(isFilter);

// callback이 쓰인다면 위의 로직이 아래와 같이 돌아갈 것이라 예상(isFilter의 함수가 callback으로 계속 실행되서 되는 것)
Array.prototype.filter = function (callback) {
  const array = this;

  for (let i = 0; i < array.length; i++) {
    callback(array[i], i, array);
  }
};

// 아래의 경우도 마찬가지임
Element.addEventListener('click', (e) => console.log(e));

// callback으로 위의 메소드가 쓰이게 될 것임, 그리고 아래와 같이 동작이 되게 될 것임
Element.addEventListener = function (eventType, callback) {
  if (eventType === 'click') {
    const clickEventObject = {
      target: {},
    };
    callback(clickEventObject);
  }
};
  • 그래서 callback이 함수의 사용권을 위임한다고 하는 것임, 이에 대해서 setTimeOut을 callback을 활용해서 실행해볼 수 있음
console.log('1');

function setTimeoutWithCallback(callbackFunc) {
    setTimeout(() => {
        console.log('2');
        callbackFunc()
    }, 1000);
}

setTimeoutWithCallback(() => console.log('3'))
  • 위의 로직의 경우 callback을 활용해서 순서대로 그 값이 나올 수 있게됨, callback에 console.log('3')을 넘겨서 위임하기에 순서대로 실행된 것
  • 마치 아래와 같은 로직으로 실행이 된 것일 것임
setTimeout(function() {
    console.log('2')

    setTimeout(function () {
      console.log('3')
    }, 1000)
}, 1000)
  • 넘긴 함수만큼 알아서 순차적으로 실행된 것임, 순차적으로 비동기를 실행시킨 것임. 하지만 너무 과하게 쓰면 callback 지옥에 빠질 수 있음
  • 알아서 위임된 형태로 콜백 패턴으로 넘겨서 실행한다는 것임, 그렇게 비동기 처리를 하는 것

promise란? & promise resolve & promise reject

  • 객체이면서 비동기 작업을 도와주고, 실행 종료 후 완료 & 실패와 그 결과값을 알려줌
  • pending(대기) 상태, fulfilled(이행) 상태, rejected(거부) 상태로 초기 상태와 성공 & 실패 상태가 나뉘어지고, 이에 대해서 then 메서드로 이어짐
  • 점 연산자로 then 체이닝으로 promise를 사용함, catch의 경우 실패했을 때 처리를 함
  • 성공은 then 체이닝으로 실패는 catch 체이닝으로 이어짐
  • 이 과정이 계속 반복되고 비동기 연산을 할 수 있음
const promiseResolve = Promise.resolve('성공')
const promiseReject = Promise.reject('실패')

// 성공하고 resolve가 다음으로 체이닝된대로 넘어감
promiseResolve
    .then((resolve) => {
        console.log('1회' + resolve); // 1회 성공
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('2회' + resolve); // 2회 성공
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('3회' + resolve); // 3회 성공
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('4회' + resolve); // 4회 성공
        return promiseResolve;
    })
  • reject의 경우도 마찬가지임(catch 사용 후 안정적으로 resolve도 같이 사용가능)
// 2회 성공까지만 나오고 밑의 코드는 실행되지 않음
promiseResolve
    .then((resolve) => {
        console.log('1회' + resolve); // 1회 성공
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('2회' + resolve); // 2회 성공
        return promiseReject;
    })
    .then((resolve) => {
        console.log('3회' + resolve); 
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('4회' + resolve);
        return promiseResolve;
    })

// reject를 만나고 정상적으로 catch 후, 4회 성공으로 넘어감
promiseResolve
    .then((resolve) => {
        console.log('1회' + resolve); // 1회 성공
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('2회' + resolve); // 2회 성공
        return promiseReject;
    })
    .catch((reject) => {
        console.log('3회 실패' + reject); // 3회 실패실패
        return promiseResolve;
    })
    .then((resolve) => {
        console.log('4회' + resolve); // 4회 성공
        return promiseResolve;
    })

new promise

  • 앞서 setTimeout을 써서 한 것에 대해서 아래와 같이 Promise를 사용해서 할 수 있음, 이 때 new 키워드 활용
const one = Promise.resolve('1');
const two = (delay) =>
  new Promise((resolve) =>
    setTimeout(() => {
      resolve('2');
    }, delay),
  );
const three = Promise.resolve('3');

one
  .then((oneRes) => {
    console.log(oneRes);

    return two(3000);
  })
  .then((twoRes) => {
    console.log(twoRes);

    return two;
  })
  .then((threeRes) => {
    console.log(threeRes);
  }).finally(() => console.log('END'));
  • 그러면 위의 경우 1이 나온 뒤 3초 뒤 2, 3, END가 나타남
  • 이처럼 비동기를 명시적으로 then 체이닝으로 쓸 수 있음
const starbucks = function(coffeeName) {
  return new Promise((resolve, reject) => {
    if (coffeeName === '아메리카노') {
      resolve('아메리카노 한잔입니다');
    } else {
      reject('아메리카노는 없습니다.');
    }
  };
};

// 인자를 넘겨서 처리해야함, 값에 따라서 알아서 체이닝이 잡혀서 처리됨(명시적으로 쓸 수 있음)
starbucks('아메리카노')
  .then((res) => console.log(res))
  .catch((rej) => console.log(rej))
  .finally(() => console.log('감사합니다'));

async & await

  • async는 함수에 선언할 수 있는 일종의 키워드임 prefix로 쓰고 그 내부에는 await을 쓸 수 있음
  • async - await의 경우, 마치 promise resolve에서 then 체이닝과 비슷하게 쓸 수 있음
  • 앞서 만든 starbucks 예시를 async & await으로 바꿔볼 수 있음
const starbucks = function(coffeeName) {
  return new Promise((resolve, reject) => {
    if (coffeeName === '아메리카노') {
      resolve('아메리카노 한잔입니다.');
    } else {
      reject('아메리카노는 없습니다.');
    }
  };
};

// 인자를 넘겨서 처리해야함, 값에 따라서 알아서 체이닝이 잡혀서 처리됨(명시적으로 쓸 수 있음)
starbucks('아메리카노')
  .then((res) => console.log(res))
  .catch((rej) => console.log(rej))
  .finally(() => console.log('감사합니다'));

// async & await 사용
async function americano() {
  const result = await starbucks('아메리카노');

  return result;
}

americano(); // 아메리카노 한잔입니다.
  • 이를 동시에 then, catch, finally를 만들기 위해서, 아래와 같이 쓸 수 있음
async function americano(someDrink) {
  try {
    const result = await starbucks(someDrink);
    return result;
  } catch (error) {
    console.log(error);
  } finally {
    console.log('감사합니다');
  }
}

americano('아메리'); // 아메리카노는 없습니다. 감사합니다.
americano('아메리카노'); // 감사합니다. 아메리카노 한잔입니다.
  • async를 사용하면 반드시 await을 붙여줘야함, 그래서 무조건 좋은 것은 아님
  • 하지만 장점은 then 체이닝으로 스코프를 유지하는 게 아닌 함수 내부에서 자유롭게 then, catch, finally를 쓸 수 있음
  • 상황에 맞게 찾아서 사용하는게 좋음
profile
측정할 수 없으면 관리할 수 없고, 관리할 수 없으면 개선시킬 수도 없다

0개의 댓글