[Frontend 기술 면접 대비] JavaScript에서 동기와 비동기

Jo HangJoon·2022년 7월 12일
1

[Frontend 기술 면접 대비] 시리즈는 Frontend 개발자로 취업하기 위해 내 프로젝트 경험과 지식들을 정리한 내용이다.


질문: 동기와 비동기의 차이는? JavaScript의 비동기 처리 방식은?


1. 동기 & 비동기

동기 Synchronous

요청과 그 결과가 동시에 일어난다. 요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 한다.

  • 순서에 맞춰 진행된다.
  • 설계가 매우 간단하다.
  • 여러 가지 요청을 동시에 처리할 수 없고 대기해야 한다.

비동기 Asynchronous

요청과 결과가 동시에 일어나지 않는다. 요청에 따른 응답을 즉시 처리하지 않아도, 대기 시간동안 다른 요청에 대한 처리가 가능하다.

  • 동기 방식보다 속도가 느리고 복잡하다.
  • 자원을 효율적으로 사용할 수 있다.

그림 출처


2. JavaScript 비동기 콜백

JavaScript의 엔진은 다음 두 가지 요소로 구성되어 있다.

  • Memory Heap(메모리 힙)
  • Call Stack(호출 스택)

Call Stack

JavaScript는 Single Thread 언어이므로 단일 호출 스택이 있다. 즉, 한 번에 하나의 일만 처리할 수 있다. 스택의 사이즈를 초과했을 경우, Stack Overflow 에러가 난다.

  • 작업(함수)을 실행하면 스택의 맨 위에 해당 작업(함수)이 추가(push)된다.
  • 작업(함수)을 반환하면 스택에 쌓여있던 작업(함수)는 제거(pop)된다.

Call Stack에 저장되는 각 항목을 실행 맥락(Execution Context)라 한다.

Call Back

하나의 작업이 완료될 때까지 기다려야하는 문제점을 해결하기 위해서 비동기 콜백(Asynchronous Callback)을 사용한다. 일이 끝나면 실행시킬 콜백 함수들은 바로 Call Stack에 push될 필요가 없다. 이를 위해 JavaScript 실행환경(runtime)은 태스크 큐(Task Queue)를 가지고 있다.

  • Task Queue(태스크 큐): 처리할 메시지 목록과 실행할 콜백 함수들의 목록

JavaScript 비동기 콜백

그림 출처

비동기 콜백 과정

  1. DOM 이벤트, HTTP 요청, setTimeOut() 등과 같은 비동기 함수는 web API를 호출한다.
  2. Web API는 콜백 함수를 Event Queue에 넣는다.
  3. Task Queue는 대기하다가 Call Stack이 비는 시점에 Event Loop를 돌려 Call Stack에 콜백 함수를 넣는다.

3. JavaScript의 비동기 처리

JavaScript는 기본적으로 비동기적으로 작업을 처리하는 비동기 처리 특성을 가지고 있다.

Ajax

Ajax(Asynchronous JavaScript And XML)란 서버와 통신하기 위해 XMLHttpRequest 객체를 사용하는 것을 말한다. Ajax는 비동기성 특징을 가지므로 페이지 전체를 새로고침하지 않아도 수행된다.

function getData() {
	var tableData;
    // ajax 통신
	$.get('https://domain.com/products/1', function(response) {
		tableData = response;
	});
	return tableData;
}

console.log(getData());

> 실행 결과

undefined

getData()를 호출하여 출력하면 통신으로 받아온 데이터가 tableData에 저장되어 출력되지만 undefined가 출력된다. 이는 데이터를 요청하고 응답받을 때까지 기다려주지 않고 return tableData가 실행됐기 때문이다. 즉, 비동기 처리가 발생했다.

setTimeout

setTimeout()은 Web API의 한 종류로, 코드를 바로 실행하지 않고 지정한 시간만큼 기다렸다가 로직을 실행한다.

// #1
console.log('Hello');
// #2
setTimeout(function() {
	console.log('Bye');
}, 3000);
// #3
console.log('Hello Again');

> 실행 결과

Hello
Hello Again
Bye

setTimeout() 역시 비동기 처리가 되므로 바로 다음 코드인 console.log('Hello Again');으로 넘어갔다. 이후 3초가 지나 Bye가 출력된다.


하지만 개발을 하다보면 비동기적 방식이 아닌 동기적 방식으로 작업을 처리해야할 때가 있다. 이를 해결하기 위해 다음 방법들을 사용한다.

콜백 함수

Callback 함수는 다른 함수의 파라미터로 넘기는 함수를 말한다. 콜백 함수가 끝나고 콜백 함수를 받은 함수가 실행되기 때문에 동기식으로 동작된다.

function findUserAndCallBack(id, cb) {
  const user = {
    id: id,
    name: "User" + id,
    email: id + "@test.com",
  };
  cb(user);
}

findUserAndCallBack(1, function (user) {
  console.log("user:", user);
});

> 실행 결과

user: {id: 1, name: "User1", email: "1@test.com"}

findUserAndCallBack 함수는 cb로 콜백함수를 할당받았기 때문에 cb(user)가 실행될 때, 동기적으로 실행된다.

콜백함수로 동기적으로 처리 하기 위해서는 함수의 결과값을 리턴받으려 하지말고, 결과값을 통해 처리할 로직을 콜백 함수로 넘기는 방식으로 코딩한다.

콜백함수를 연속해서 사용하면 콜백 지옥(Callback Hell) 문제가 생긴다. 이러한 코드 구조는 가독성도 떨어지고 로직을 변경하기도 어렵다.

$.get('url', function(response) {
	parseValue(response, function(id) {
		auth(id, function(result) {
			display(result, function(text) {
				console.log(text);
			});
		});
	});
});

Promiseasync/await를 이용해 이런 문제를 해결할 수 있다.

Promise

Promise는 비동기 작업의 최종 완료와 그 결과값을 나타내는 객체이다. Promise는 다음 중 하나의 상태를 가진다.

  • 대기(Pending): 이행하지 않거나 거부되지 않은 초기 상태
  • 이행(Fulfilled): 연산이 성공적으로 종료됨
  • 거부(Rejected): 연산이 실패함

Promise 사용

  • new Promise()를 사용해 Promise 객체를 생성하고 콜백 함수를 선언할 수 있다. 이때 인자는 resolvereject를 사용한다.
  • resolve는 결과가 성공인 Promise 객체를 반환하고, reject는 결과가 실패인 Promise 객체를 반환한다.
  • 결과가 성공인 Promise 객체는 then을 사용해 처리하고, 결과가 실패인 Promise 객체는 catch를 사용해 처리한다.
let myFirstPromise = new Promise((resolve, reject) => {
  setTimeout(function(){
    // 성공 시 resolve 사용
    resolve(`Success!`);
  }, 3000);
});


myFirstPromise
// 성공 시 then 사용하여 결과 처리
.then((successMessage) => {
  console.log(`Yay! ` + successMessage);
});
// 실패 시 catch 사용하여 결과 처리
.catch((reason) => {
  console.log('여기서 거부된 프로미스( ' + reason + ' )를 처리하세요.');
});

> 실행 결과

# 성공 시
Yay! Success!

# 실패 시
여기서 거부된 프로미스( Error: Fail )를 처리하세요.

다음 예시처럼 Promise를 사용해 콜백 지옥을 해결할 수 있는 장점이 있다.

function a() {
  return new Promise({
    // ...
  });
}

function b() {
  return new Promise({
    // ...
  });
}

function c() {
  return new Promise({
    // ...
  });
}

myFirstPromise()
.then(a)
.then(b)
.then(c);

async/await

Promise 역시 여전히 콜백함수를 사용하기 때문에 가독성이 좋지 않은 문제점이 있다. 이를 해결하기 위해 async/await을 사용한다. 함수 앞에 async를 붙이면 해당 함수는 비동기 함수(async function)가 되고 반환되는 값은 Promise 객체가 된다. awaitthen과 유사한 기능을 한다. await이 붙은 메서드가 종료될 때까지 비동기 함수는 실행을 중지한다. 비동기 함수는 동기식 코드를 짜듯이 비동기 코드를 짤 수 있다는 장점이 있다.

async function hello() {
  return 'Hello';
}

async function callHello() {
  try {
    const r = await hello();
    console.log('성공: ' + r);
  } catch (e) {
    console.log('실패: ' + e.message);
   }
}

callHello();

> 실행 결과

성공: Hellotext

비동기 함수에서는 try/catch를 이용하여 예외 처리를 할 수 있다.


4. 경험 정리

JavaScript 언어를 배우기 전에는 Python만 사용해봤기 때문에, 그 당시에는 동기/비동기 동작에 대해 걱정할 필요가 없었다. JavaScript로 frontend 개발 프로젝트를 하면서, 데이터를 받아오기 전에 페이지가 렌더링되어 빈 데이터가 출력되는 일이 굉장히 빈번히 있었다. 때문에 동기/비동기와 Promise 객체, async/await을 끊임없이 공부하고 사용했는데, 완벽히 정리하기 어려운 개념이었다. 비동기 처리방식을 모두 사용해본 결과 비동기 함수를 사용하는 방식이 가장 코드를 수월하게 작성할 수 있었다.


함께 보면 좋은 글

0개의 댓글