Javascript 비동기 처리

김승ㅈIT·2023년 2월 25일
post-thumbnail

데이터 처리 모델

데이터 처리 모델을 쉽게 표현하자면 데이터를 받는 방식이라고 할 수 있습니다.
이 방식에는 동기식 처리와 비동기식 처리 모델이 존재한다.

동기(synchronous)

동기는 데이터의 요청과 결과가 한 자리에서 동시에 일어나는것을 말한다.
요청을 하면 시간이 얼마나 걸리던지 요청한 자리에서 결과가 주어져야 한다.
사용자가 데이터를 서버에게 요청한다면 그 서버가 데이터 요청에 따른 응답을 사용자에게 다시 리턴해주기 전까지 사용자는 다른 활동을 할 수 없으며 기다려야함을 의미한다.

비동기(Asynchronous)

동기는 동시에 일어나지 않는다는 의미이다.
요청한 결과는 동시에 일어나지 않을거라는 약속이다.
서버에게 데이터를 요청한 후 요청에 따른 응답을 계속 기다리지 않고 다른 외부 활동을 수행하여도 되고 서버에게 다른 요청사항을 보내도 상관없다.

즉, 자바스크립트의 비동기 처리란 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 자바스크립트의 특성을 의미한다.

비동기 처리의 사례

1. jquery ajax

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

console.log(getData()); // undefined

$.get()이 ajax 통신을 하는 부분입니다. https://domain.com 에다가 HTTP GET요청을 날려 1번 상품(product) 정보를 요청하는 코드이다.
좀 더 쉽게 말하면 지정된 URL에 ‘데이터를 하나 보내주세요’ 라는 요청을 날리는 것과 같다.
그렇게 서버에서 받아온 데이터는 response 인자에 담깁니다. 그리고 tableData = response; 코드로 받아온 데이터를 tableData라는 변수에 저장합니다. 그럼 이제 이 getData()를 호출하면 어떻게 될까?

받아온 데이터가 뭐든 일단 뭔가 찍혀야할 것이다. 그런데 결과는 맨 아래에서 보시는 것처럼 undefined이다. 왜 그럴까?

그 이유는 $.get()로 데이터를 요청하고 받아올 때까지 기다려주지 않고 다음 코드인 return tableData;를 실행했기 때문이다. 따라서, getData()의 결과 값은 초기 값을 설정하지 않은 tableData의 값 undefined를 출력합니다.

이렇게 특정 로직의 실행이 끝날 때까지 기다려주지 않고 나머지 코드를 먼저 실행하는 것이 비동기 처리입니다. 자바스크립트에서 비동기 처리가 필요한 이유를 생각해보면, 화면에서 서버로 데이터를 요청했을 때 서버가 언제 그 요청에 대한 응답을 줄지도 모르는데 마냥 다른 코드를 실행하지 않고 기다릴 순 없기 때문이다.

2. setTimeout()

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

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

비동기 처리에 대한 이해가 없는 상태에서 위 코드를 보면 아마 다음과 같은 결과값이 나올 거라고 생각된다.

  • ‘Hello’ 출력
  • 3초 있다가 ‘Bye’ 출력
  • ‘Hello Again’ 출력

실제 결과 값은 아래와 같다.

  • ‘Hello’ 출력
  • ‘Hello Again’ 출력
  • 3초 있다가 ‘Bye’ 출력

setTimeout() 역시 비동기 방식으로 실행되기 때문에 3초를 기다렸다가 다음 코드를 수행하는 것이 아니라 일단 setTimeout()을 실행하고 나서 바로 다음 코드인 console.log('Hello Again');으로 넘어갔습니다. 따라서, ‘Hello’, ‘Hello Again’를 먼저 출력하고 3초가 지나면 ‘Bye’가 출력됩니다.

비동기 처리 방식의 문제점 해결방법

1. 콜백 함수

앞에서 살펴본 ajax 통신 코드를 콜백 함수로 개선해보겠습니다.

function getData(callbackFunc) {
	$.get('https://domain.com/products/1', function(response) {
		callbackFunc(response); 
      // 서버에서 받은 데이터 response를 callbackFunc() 함수에 넘겨줌
	});
}

getData(function(tableData) {
	console.log(tableData); // $.get()의 response 값이 tableData에 전달됨
});

이렇게 콜백 함수를 사용하면 특정 로직이 끝났을 때 원하는 동작을 실행시킬 수 있습니다.

콜백 지옥(callback hell)

콜백 지옥은 비동기 처리 로직을 위해 콜백 함수를 연속해서 사용할 때 발생하는 문제이다.

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

웹 서비스를 개발하다 보면 서버에서 데이터를 받아와 화면에 표시하기까지 인코딩, 사용자 인증 등을 처리해야 하는 경우가 있습니다. 만약 이 모든 과정을 비동기로 처리해야 한다고 하면 위와 같이 콜백 안에 콜백을 계속 무는 형식으로 코딩을 하게 됩니다. 이러한 코드 구조는 가독성도 떨어지고 로직을 변경하기도 어렵습니다. 이와 같은 코드 구조를 콜백 지옥이라고 합니다.

콜백 지옥을 해결하는 방법

일반적으로 콜백 지옥을 해결하는 방법에는 Promise나 Async를 사용하는 방법이 있습니다. 만약 코딩 패턴으로만 콜백 지옥을 해결하려면 아래와 같이 각 콜백 함수를 분리해주면 됩니다.

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

위 코드는 앞의 콜백 지옥 예시를 개선한 코드입니다. 중첩해서 선언했던 콜백 익명 함수를 각각의 함수로 구분하였습니다. 정리된 코드를 간단하게 살펴보겠습니다.

  1. 먼저 ajax 통신으로 받은 데이터를 parseValue() 메서드로 파싱 합니다.
  2. parseValueDone()에 파싱 한 결과값인 id가 전달되고 auth() 메서드가 실행됩니다.
  3. auth() 메서드로 인증을 거치고 나면 콜백 함수 authDone()이 실행됩니다. 인증 결과 값인 result로 display()를 호출
  4. 마지막으로 displayDone() 메서드가 수행되면서 text가 콘솔에 출력됩니다.

2. Promise

Promise는 자바스크립트 비동기 처리에 사용되는 객체이다.
자바스크립트의 비동기 처리‘특정 코드의 실행이 완료될 때까지 기다리지 않고 다음 코드를 먼저 수행하는 자바스크립트의 특성’을 의미합니다.

function getData(callback) {
  // new Promise() 추가
  return new Promise(function(resolve, reject) {
    $.get('url 주소/products/1', function(response) {
      // 데이터를 받으면 resolve() 호출
      resolve(response);
    });
  });
}

// getData()의 실행이 끝나면 호출되는 then()
getData().then(function(tableData) {
  // resolve()의 결과 값이 여기로 전달됨
  console.log(tableData); // $.get()의 reponse 값이 tableData에 전달됨
});

위에 언급했던 jquery의 ajax통신 API를 이용하여 지정된 url에서 1번 상품 데이터를 받아오는 코드를 Promise객체를 적용한 코드이다.
콜백 함수로 처리하던 구조에서 new Promise(), resolve(), then()와 같은 프로미스 API를 사용한 구조로 바뀌었다.

Promise의 3가지 states

프로미스를 사용할 때 알아야 하는 가장 기본적인 개념이 바로 프로미스의 상태(states)입니다. 여기서 말하는 상태란 프로미스의 처리 과정을 의미합니다. new Promise()로 프로미스를 생성하고 종료될 때까지 3가지 상태를 갖습니다.

  • Pending(대기) : 비동기 처리 로직이 아직 완료되지 않은 상태
  • Fulfilled(이행) : 비동기 처리가 완료되어 프로미스가 결과 값을 반환해준 상태
  • Rejected(실패) : 비동기 처리가 실패하거나 오류가 발생한 상태

Pending(대기)

먼저 아래와 같이 new Promise() 메서드를 호출하면 대기(Pending) 상태가 됩니다.

new Promise(function(resolve, reject) {
  // ...
});

new Promise() 메서드를 호출할 때 콜백 함수를 선언할 수 있고, 콜백 함수의 인자는 resolve, reject입니다.

Fulfilled(이행)

콜백 함수의 인자 resolve를 아래와 같이 실행하면 이행(Fulfilled) 상태가 됩니다.

new Promise(function(resolve, reject) {
 resolve();
});

그리고 이행 상태가 되면 아래와 같이 then()을 이용하여 처리 결과 값을 받을 수 있습니다.

function getData() {
 return new Promise(function(resolve, reject) {
   var data = 100;
   resolve(data);
 });
}

// resolve()의 결과 값 data를 resolvedData로 받음
getData().then(function(resolvedData) {
 console.log(resolvedData); // 100
});

Rejected(실패)

reject를 아래와 같이 호출하면 실패(Rejected) 상태가 됩니다.

new Promise(function(resolve, reject) {
  reject();
});

실패 상태가 되면 실패한 이유(실패 처리의 결과 값)를 catch()로 받을 수 있습니다.

function getData() {
  return new Promise(function(resolve, reject) {
    reject(new Error("Request is failed"));
  });
}

// reject()의 결과 값 Error를 err에 받음
getData().then().catch(function(err) {
  console.log(err); // Error: Request is failed
});

Promise 에러 처리 방법

1. then()의 두 번째 인자로 에러를 처리하는 방법

function getData() {
  return new Promise(function(resolve, reject) {
    reject('failed');
  });
}

// 1. then()의 두 번째 인자로 에러를 처리하는 코드
getData().then(function() {
  // ...
}, function(err) {
  console.log(err);
});

2. catch()를 이용하는 방법

function getData() {
  return new Promise(function(resolve, reject) {
    reject('failed');
  });
}

// 2. catch()로 에러를 처리하는 코드
getData().then().catch(function(err) {
  console.log(err);
});

Promise 에러 처리는 가급적 catch()사용이 효율적

// then()의 두 번째 인자로는 감지하지 못하는 오류
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result);
  throw new Error("Error in then()"); // Uncaught (in promise) Error: Error in then()
}, function(err) {
  console.log('then error : ', err);
});

getData() 함수의 프로미스에서 resolve() 메서드를 호출하여 정상적으로 로직을 처리했지만, then()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못합니다. 따라서 코드를 실행하면 아래와 같은 오류가 납니다.

'에러를 잡지 못했습니다(Uncaught Error)' 로그

하지만 똑같은 오류를 catch()로 처리하면 다른 결과가 나옵니다.

// catch()로 오류를 감지하는 코드
function getData() {
  return new Promise(function(resolve, reject) {
    resolve('hi');
  });
}

getData().then(function(result) {
  console.log(result); // hi
  throw new Error("Error in then()");
}).catch(function(err) {
  console.log('then error : ', err); // then error :  Error: Error in then()
})

발생한 에러를 성공적으로 콘솔에 출력한 모습업로드중..


3. async & await

async와 await는 자바스크립트의 비동기 처리 문법이다.
기존의 비동기 처리 방식인 콜백 함수와 promise의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있게 도와준다.

async & await 기본 문법

async function 함수명() {
  await 비동기_처리_메서드_명();
}

async & await 예제

function fetchItems() {
  return new Promise(function(resolve, reject) {
    var items = [1,2,3];
    resolve(items)
  });
}

async function logItems() {
  var resultItems = await fetchItems();
  console.log(resultItems); // [1,2,3]
}

fetchItems() 함수는 Promise객체를 반환하는 함수입니다.
fetchItems() 함수를 실행하면 Promise가 이행(Resolved)되며 결과 값은 items 배열이 됩니다.

그리고 logItems() 함수를 실행하면 fetchItems() 함수의 결과 값인 items 배열이 resultItems 변수에 담겨 콘솔에는 [1,2,3]이 출력된다.

await를 사용하지 않았다면 데이터를 받아온 시점에 콘솔을 출력할 수 있게 콜백 함수나 .then()등을 사용해야 했을 겁니다. 하지만 async/await 문법으로 인해 비동기에 대한 사고를 하지 않아도 되는 것이다.

async & await 예외 처리

async & await에서 예외를 처리하는 방법은 바로 try catch이다. Promise 에러 처리를 위해 .catch()를 사용했던 것처럼 async에서는 catch {} 를 사용하면 된다.

function fetchUser() {
  var url = 'https://jsonplaceholder.typicode.com/users/1'
  return fetch(url).then(function(response) {
    return response.json();
  });
}

function fetchTodo() {
  var url = 'https://jsonplaceholder.typicode.com/todos/1';
  return fetch(url).then(function(response) {
    return response.json();
  });
}

async function logTodoTitle() {
  try {
    var user = await fetchUser();
    if (user.id === 1) {
      var todo = await fetchTodo();
      console.log(todo.title); 
    }
  } catch (error) {
    console.log(error);
  }
}

위의 코드를 실행하다가 발생한 네트워크 통신 오류뿐만 아니라 간단한 타입 오류 등의 일반적인 오류까지도 catch로 잡아낼 수 있다. 발견된 에러는 error 객체에 담기기 때문에 에러의 유형에 맞게 에러 코드를 처리하면 된다.

참고 자료
https://velog.io/@slobber/%EB%8F%99%EA%B8%B0%EC%99%80-%EB%B9%84%EB%8F%99%EA%B8%B0%EC%9D%98-%EC%B0%A8%EC%9D%B4
https://www.youtube.com/watch?v=m0icCqHY39U&t=496s
https://joshua1988.github.io/web-development/javascript/javascript-asynchronous-operation/

0개의 댓글