동기, 비동기 돌아보기

ChangHyeon Bae·2022년 11월 30일
0

JavaScript

목록 보기
1/8
post-thumbnail

동기, 비동기? 자바스크립트로 프로젝트를 진행하면서 들어왔던 중요한 핵심 개념이라는건 알고 있다. 그런데 내가 이 개념을 활용해서 효율적으로 코드를 작성하고 있는걸까?

동기와 비동기라는것이 무엇인지 자세하게 알기전에 밑에 코드의 결과값에 대해 말해보자.

👀 예시코드를 보며 결과 값 생각해보기

console.log('1');
setTimeout(()=>{
	console.log('2');
}, 5000);
console.log('3');

위 코드의 결과값은 뭐가 나올까? 자바스크립트를 통해 프로젝트를 한번이라도 진행을 해본 경험이 있다면
결과값이 1, 3, 2 이 나오는것을 알 수 있다.

그러면 아래 코드는 어떤 결과값이 나올까?

console.log('1');
setTimeout(()=>{
	console.log('2');
}, 0);
console.log('3');

위 코드의 결과값은 첫번째로 작성했던 코드와 똑같은 결과값이 나온다. 물론 실제로 이렇게 setTimeout()을 0으로 설정하는 일은 거의 없다. 그래서 지금까지 위와 같은 코드를 본적이 없을 것이다.

하지만 위 코드와 아래코드가 왜 같은 결과 값이 나오는지에 대해 비동기라는 개념에 대해 한번 더 생각해 볼 수 있다.

🙋‍동기, 비동기가 뭐에요?

동기에 대한 개념을 살펴보면 먼저 영어로 synchronous라고 한다. 동시에 일어나다 라는 의미를 가지고 있는데 이는 요청을 하면 (바로) 응답을 받는다.라는 의미이다. 그래서 동기는 요청을 하면 시간이 얼마나 걸리든 요청한 자리에서 결과 값을 반환 해주어야한다.

그럼 비동기는 뭘까? 비동기는 동기와 반대라고 생각하면 쉽게 알 수 있다. 비동기는 영어로 asynchronous 라고 한다. 동시에 일어나지 않는다. 라는 의미인데 비동기는 동기와는 다르게 요청과 결과가 동시에 일어나지 않으며, 요청에 대한 결과값을 바로 처리하는 것이 아니고 당장 응답하지 않아도 된다.

개념에 대해서는 이해한거 같다. 동기는 그자리에서 결과 값을 반환하고, 비동기는 그 자리에서 바로 처리하지 않고 당장 응답하지 않아도 된다.

위에 정도로만 이해하고 밑에 두가지 상황 예시를 통해 동기와 비동기에 대해 확실하게하고 넘어가자!

⭐️ 상황 예시

  • A 커플이 있다.
  • 오랜만에 기념일을 맞이하여 맛집에 가려고 한다.
  • 하지만 가게 앞에 많은 팀이 입구에 서 있기 때문에 기다려야 한다.
  • 꼼짝없이 1시간정도 줄을 서서 기다린 후에 들어갈 수 있었다.
  • B 커플이 있다.
  • 이 커플도 A커플과 마찬가지로 기념일을 맞이해 맛집에 가려고 한다.
  • 가게 내에 웨이팅 기계가 있어 간단하게 이름과 휴대전화 번호를 입력하고 대기 번호를 출력받았다.
  • 가게에 들어가기전까지 1시간정도 걸리지만 주위에 가고 싶었던 편집샵이 있어 가기로 한다.
  • 1시간 후, B 커플의 순서가 되어 가게에서 입장 가능하다는 연락을 받아 가게로 들어갔다.

위에 두 상황예시를 통해 어느 커플이 시간을 더 효율적으로 사용했다고 말할 수 있을까?

누가봐도 B커플이 웨이팅을 하면서, 다른 볼일을 보며 웨이팅을 했기 때문에 시간을 효율적으로 썼다고 할 수 있습니다.

그럼 위 상황을 동기랑 비동기 개념을 빚대어 말해보면 뭐가 동기이고 비동기라고 말할 수 있을까?
앞서 개념을 생각하면 A커플이 동기이고, B커플이 비동기적으로 처리되었다고 볼 수 있다.

그러면 효율적인 비동기만 사용하면 안될까? 라는 의문이 생길 수 있다.

위 상황 예시는 동기와 비동기의 처리방식을 설명하기 위해 실제 상황을 빚대어 설명한 것이기 때문에 저 상황만을 가지고 동기 방식은 별로고, 비동기적 방식이 최고다 라고 판단하기는 너무 섣부른 판단이 될 수 있다.

🤓 어떨 때 사용 하는거야?

동기 방식은 태스크를 순서대로 하나씩 처리하므로 실행 순서가 보장된다는 장점이 존재하면서도 단점으로 위에 상황 예시와 같이 꼼짝없이 그 결과값을 기다려야하는 블록현상이 발생할 수 있다.

블록현상이란? 다음 태스크를 실행하는데에 앞에 태스크 결과값을 기다리는 상황으로 인한 대기 발생이라 생각하면 된다. 블록 현상이 발생하면 사용자 UX가 최악이 되기 때문에 안좋은 현상으로 분류 된다.

비동기 방식은 현재 실행중인 태스크가 결과를 아직 결과를 반환하지 않아도 바로 다음 태스크를 실행 할 수 있고, 블록현상이 발생하지 않는 다는 장점이 있지만, 동기식에 비해 구현이 까다롭고, 태스크의 실행순서가 보장되지 않는 단점이 존재한다.

동기와 비동기 방식은 위에서 설명한것과 같이 각자 장, 단점이 분명하다, 그렇기 때문에 동기 방식은 순서가 보장되어야 하는 상황에서 사용하고, 비동기는 블록현상을 예방하고 순서가 상관없는 상황에서 적절히 두 방식을 섞어 사용하여 각 방식의 장, 단점을 상호보완 하며 사용하는게 베스트인거 같다!

🤔 그러면 비동기는 어떻게 처리가 이루어지는 걸까?

동기는 콜 스택에 순서대로 들어가면서 실행이 된다. 그런데 비동기는 어떻게 처리되는 걸까?

자바스크립트는 싱글 스레드라는건 공부를 하면서 가장 먼저 배우는 개념이다. 그런데 우리가 알고 있는 싱글 스레드는 하나이기 때문에 동시에 하나의 작업만 처리 할 수 있다. 그런데 자바스크립트를 통해 프로젝트를 하면서 동시에 여러개의 작업을 처리했던걸 기억할 수 있다. 스레드가 하나뿐인데 어떻게 그런작업이 가능했던 걸까?

먼저 자바스크립트의 구동환경을 한번 밑에 그림을 통해 살펴 보자 !

위에 그림을 통해 어떤 요소들로 구성되어있는지 알 수 있다. 그럼 각 요소들의 역할은 무엇일까?

  • JS Engine

    • JS엔진 안에는 Memery Heap 과 우리가 아는 Call Stack이 있다. 여기에는 어떤 것들이 처리되고 저장이 되는걸까?
    • MemeryHeap은 Object들이 모여있다. 참조, 타입(객체 등) 변수나 상수 메모리를 저장하는 영역 이다.
    • Call Stack에 작업 태스크가 추가되어 처리된다. 그리고 자바스크립트 엔진은 단 하나의 호출 스택을 사용하기 때문에 하나의 작업만 실행 할 수 있다.
  • WebAPIs

    • Web APIs에 비동기 방식으로 처리되는 이벤트를 넘겨 처리하고 있다.
  • Callback Queue

    • 비동기 방식으로 처리되어 나온 결과값을 차례대로 해당 큐에 쌓아 놓는다.
  • EventLoop

    • EventLoop는 JS엔진과 Callback Queue를 감시하여 큐에있는 결과값을 JS엔진으로 넘겨준다.
    • 감시라는 단어가 나왔는데 왜 이런말이 나온걸까? 그건 비동기 방식의 처리 부분에 대해 생각해보면 알 수 있다. JS엔진은 하나의 스레드로 처리하기 때문에 동기방식이나 다른 비동기 방식으로 처리되고 있는CallStack 중간에 새로운 이벤트를 끼워 넣을 수 없다. 그러기 때문에 JS엔진이 완전히 비워져있는 상태에서 넣어 주기 때문에 감시하는 역할을 가진 EventLoop가 필요하다.

    이제 위에 그림에 나와있는 각 요소들에 설명해봤다. 그런데 항상 그렇듯 개념 다음에 실습하는 과정을 꼭 필요하다.

처음에 설명했던 코드를 이용해 비동기처리과정을 한번 더 살펴보자

console.log('1');
setTimeout(()=>{
	console.log('2');
}, 0);
console.log('3');

먼저 순서대로 처리방식을 살펴보자

  1. console.log('1')은 콜스텍에 들어간 이후 바로 출력이 된다.
  2. setTimeout(); 은 WepAIPs에 넘어간다.
  3. 바로 console.log('3')이 실행 되고 출력된다.
  4. WepAPIs에 넘어간 setTimeout()의 결과 값이 나온다.
  5. 나온 결과 값은 Callback Queue에 쌓인다.
  6. EventLoop는 CallStack 내부가 비워져있는지 확인한다.
  7. Callback Queue에서 결과값을 CallStack에 넣고 값을 처리, 출력한다.

앞에서 코드를 봤을 때랑 동기와 비동기의 처리과정을 알고난 후에 이렇게 처리되는 과정을 보니 느낌이 또 다르다. 뭔가 조금 깊이 들어간거 같긴하지만 비동기가 어떤 방식으로 처리되는지 한번 살펴보니 찝찝했던 부분이 다소 사라진다.

그럼 이렇게 동기와 비동기 개념과 차이점에 대해 알아보고, 비동기 동작 방식도 알아봤다!

📚 비동기 처리하는 방식은 뭐가 있을까?

그러면 그 비동기를 만드는 방법에는 뭐가 있을까 ?

대표적으로 3가지 방법이 존재한다.

  1. 콜백 함수
  2. Promise
  3. async / await

3가지 비동기 처리 방식을 천천히 한번 뜯어보자

먼저 콜백함수를 어떻게 쓰는지 코드로 한번 확인해보자

📝 콜백 함수

	function foo1(init, callback){
      const result = init + 1;
      callback(result);
    }

	function foo2(init, callback){
      const result = init + 2;
      callback(result);
    }

	function foo3(init, callback){
      const result = init + 3;
      callback(result);
    }

	function main() {
      foo1(0, result => {
        foo2(result, result1 => {
          foo3(result1, result2 =>{
            console.log(`result = ${result2});
          });
        });
      });

	main();

3개의 함수 foo1, foo2, foo3은 의존적인 관계로 콜백 내부에서 콜백을 호출하기 때문에 깊게 중첩된 main() 함수가 생긴다. 이게 우리 익히 알고 있는 콜백 지옥(callback hell) 이다.

여기서 우리가 흔히 알고 있는 콜백 지옥짤을 볼 수 있다.
(위짤은 보기만해도 아찔 하다...)

콜백 지옥같은 경우 이렇게 중첩해서 나타나는데 오류처리도 매우 어려워질 뿐더러, 코드를 읽는 가독성이 매우 떨어진다. 그래서 ES6 이후로는 콜백 방식을 지양하고 Promise를 사용한다.

그럼 여기서 Promise의 코드는 어떨까?

위에 콜백함수에서 보여줬던 코드를 Promise로 바꿔보자

📝 Promise

function foo1(init) {
  return init + 1;
}

function foo2(init) {
  return init + 2;
}

function foo3(init) {
  return init + 3;
}

const promise = new Promise((resolve, reject) => {
  resolve(0);
});

function main() {
  promise
    .then((result) => {
      return new Promise((resolve, reject) => {
        resolve(foo1(result));
      });
    })
    .then((result) => {
      return new Promise((resolve, reject) => {
        resolve(foo2(result));
      });
    })
    .then((result) => {
      return new Promise((resolve, reject) => {
        resolve(foo3(result));
      });
    })
    .then((result) => console.log(result));
}

main();

Promise를 사용하면 위에 보여줬던 콜백지옥 짤과는 거리가 멀어지게 된다.
하지만 뭔가 느낌이 콜백을 썻을때보다 코드의 양이 늘어보인다.
그건 아마 리턴 값에 중복된 코드가 있어서 그런거 같다.

✏️ Promise 연결하기 (체이닝)

return 키워드를 사용하면 Promise 객체로 반환되기 때문에 다시 Promise 객체를 생성해줄 필요 없이 바로 return을 한다.

function foo1(init) {
  return init + 1;
}

function foo2(init) {
  return init + 2;
}

function foo3(init) {
  return init + 3;
}

const promise = new Promise((resolve, reject) => {
  resolve(0);
});

function main() {
  promise
    .then((result) => {
      return foo1(result);
    })
    .then((result) => {
      return foo2(result);
    })
    .then((result) => {
      return foo3(result);
    })
    .then((result) => console.log(result));
}

main();

이제야 콜백함수를 사용했을 때와 다르다는게 확실히 더 와닿는다.
이외에도 예외처리에서도 장점이 존재하는데 이번 게시물에서는 따로 다루지 않겠다.
왜냐하면 콜백함수와 Promise 각각 같은 코드를 구현했을 때의 모습을 보여주는 것이기 때문에 예외처리라는 장점이 추가적으로 있다고 알고 넘어가자!

우리는 콜백함수에서 콜백 지옥을 맛보고 Promise로 천국을 봤다. 하지만 2017년도에 나온
async / await이 나왔다. 이 방식이 앞서 나온 두가지 방식보다 어떤 장점을 가지고 있을까?

📝 async/await

  • 앞에 두 방식보다 상대적으로 코드가 간결해지고, 가독성이 높아진다.
  • 응답데이터로 들어오는 변수 ( 관례쩍으로 많이 사용되는 data, response 등을) 제거할 수 있다.
  • try/catch 구문을 통해 에러 헨들링을 할 수 있다.

이렇게 장점에 대해 말해봤는데 항상 말보다는 코드를 만들어서 보고 파악해보자!


function foo1(init) {
  return init + 1;
}

function foo2(init) {
  return init + 2;
}

function foo3(init) {
  return init + 3;
}

async function main() {
  try{
 	const response = await foo1(0);
  	const response2 = await foo2(response);
  	const response3 = await foo3(response2);
  	console.log(response3);
  }catch(err){
  	console.log(err);
  }
}

main();

앞서 Promise 에서는 생성한 Promise객체를 연속적으로 호출하여 사용했다.
하지만 위 코드를 통해 Promise를 사용했을 때보다 코드가 간결해진것을 알 수 있다.
그리고 try/catch를 통해 에러 헨들링도 가능한 모습을 보여주고 있다.

😇 느낀점

전에는 동기와 비동기에 대해 그냥 개념만 이해하고, 나와있는 문법을 쓰기만 할뿐 이해하려 하지 않고 그냥 코드를 볼때도 복사/ 붙여넣기만 했다. 하지만 이렇게 동기와 비동기의 개념, 그리고 비동기가 처리되는 방식,
그리고 비동기를 만드는 방식3가지를 정리하면서 코드를 앞으로 어떻게 작성해야 효율적으로 작성할 수 있는지에 대해 더 알 수 있었던거 같다.

🔗 참고

https://hongchangsub.com/javascript-asyncandsync/
https://developer.mozilla.org/ko/docs/Learn/JavaScript/Asynchronous/Introducing
https://velog.io/@dtimes2/%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%9D%98-%EC%9D%B4%ED%95%B4
https://velog.io/@cheo/TIL-20210804
https://velog.io/@thsoon/JS-%EB%B9%84%EB%8F%99%EA%B8%B0%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%EB%90%98%EC%96%B4%EC%9E%88%EB%8A%94%EA%B0%80
https://sangminem.tistory.com/284
https://medium.com/@la.place/async-await%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EA%B5%AC%ED%98%84%ED%95%98%EB%8A%94%EA%B0%80-fa08a3157647

정확하지 않은 정보가 있을 수 있습니다. 댓글을 통해 피드백 주시면 적극적으로 반영하겠습니다🥲

profile
모든 결과는 내가 하기 나름이다 🔥

0개의 댓글