[JS] 비동기 처리 모든 것 : 콜백지옥을 해결하는 방법

AREUM·2023년 5월 1일
1

Javascript이론

목록 보기
10/10
post-thumbnail

비동기 처리에 관련되서 코드 구현하는 부분을 공부했던 내용을 정리해서 적어보려고 한다.
공부하다 보니 해당 내용은 아니지만, 관련된 내용도 적게 되다 보니 그냥 각자 나눠서 올릴까 ? 생각도 했지만, 관련 내용을 한번에 모아서 보는 것이 좋을거 같다고 생각이 들어서 한번에 모아서 올려본다.

❗️비동기 코드 구현에 필요한 조금의 지식❗️

🌟잠깐의 알아야할 내용🌟

HTTP에는
1. Body
2. Header
3. Cookie

가 있고 이 것을 REST API 규칙에 맞게 요청한다.

  • Get
    - 받아오기만 하면 될때 url로 요청만 하면 될때
    ( 서버에서 어떤 데이터를 가져와서 보여줌 )

  • Post
    - 내 정보를 서버에 게시를 해놓고 그거에 맞는 정보를 받아올 때 ( 포스트잇 )
    - 내가 원하는 정보, 컨텐츠
    - 그 컨텐츠를 담는 공간이 바디이다.
    ( 서버로 데이터를 보냄 )

  • Put
    - 원래있던 데이터를 새로운 데이터로 대체하고 싶을 때 사용. (수정할때 사용하게 된다. )
    - ex) 게시글의 내용을 수정할 때 사용된다. ( 전체를 대체 )
    ( 데이터베이스 내부 내용 갱신 )

  • patch
    - 데이터의 정보를 부분적으로 교체해야할 때 사용된다. ( 부분대체 )

  • Delete
    - 삭제하고 싶을때 사용된다.
    ( 데이터베이스 내부 내용 삭제 )

이 메소드를 사용하기 위해서 같이 보내야할 정보들.

  • 어떤 메소드를 사용할 것인지
  • url 주소
  • data ( 선택적 )
  • params ( 선택적 )

    req : 요청 오브젝트
    res : 응답 오브젝트

비동기 처리의 문제점

비동기 처리란 ?
: 특정 코드의 연산이 끝날 때까지 코드의 실행을 멈추지 않고 다음 코드를 먼저 실행하는 것.

비동기처리를 하다보면 한 가지 주의해야하는 점이 있다.
그것은 바로 콜백지옥( callback ) 이다.

  • 비동기 코드에서 나타나는 주의해야할 점.
    - 콜백지옥
    - this를 사용한 콜백 함수

왜 why

콜백 함수를 사용할 때,
파라라미터 ( 인자 ) 로 전달하는 방식이 반복되면서 코드들이 충격적인 들여쓰기가 되있는 것
을 볼 수 있다.

이러한 이유는
1. 네트워크에서 데이터를 받아오는 상황
2. 파일을 읽어오는 상황
3. 이벤트 처리
4. 서버 통신 등 ..
비동기적 작업을 수행하게 되면 발생한다.

단점은 가독성이 매우 떨어지고 에러가 생기면 코드를 수정 하기가 어렵기 때문이다.
또, 디버깅 하기도 어렵다.

그래서 이러한 결과를 맞이하게되면 해결할 수 있는 방법이 있다.
비동기적인 기능인 코드를 콜백함수를 대신해 사용되는 object 이다.


자, 이제부터 이러한 상황을 해결할 비동기 코드 구현방식 몇가지를 적어보려고 한다.
  1. Promise
  2. async & await
  3. AJAX
  4. Generator

을 사용하는 것이다.
( 4번인 Generator은 생략하고 1, 2, 3만 정리해보았다.
Generator은 필요로할 때, 찾아볼 예정 )


1. Promise

첫 번째, Promise 이다.

Promise는 무엇인가 ❓
자바스크립트에서 제공하는 비동기를 간편하게 처리하도록 도와주는 object객체 이다.

정해진 기능을 수행하고 나서 수행이 성공했다면 성공의 메세지를,
기능을 수행하다 예상치 못한 에러가 생기면 에러를 표출할 수 있도록 도와준다.

즉 Promise란, 실행은 바로 하되, 결과값을 나중에 원할 때 쓸 수 있는 것.
( 실행은 바로 하지만, 결과값이 나올때는 reslove가 되었을 때나오기 때문에 나중이고, 결과 값을 사용할 때는 더 나중이다. )

실행은 바로 👉🏻 결과 값도 거의 바로 쓰고 싶은데 👉🏻 그 다음에 결과값이 나오면 👉🏻 then, await, Promise.all 이런게 결과값을 기다린 후에 실행한다.

먼저, Promise를 알기 위해 간단한 이론 설명을 해보자.

☝🏻첫번째.
State 상태
: 기능이 지금 어떤 상태인지 성공인지 실패인지 알려주는 상태

✌🏼두번째.
프로듀서 vs 클라이언트 ( 상황과 입장 )
프로듀서 : 정보를 제공해주는 프로듀서
클라이언트 : 정보를 소비하고 사용하는 소비자인 클라이언트

state의 종류

state의 종류에는 3가지로 구분할 수 있다.

☝🏻첫번째,
Pending
: Promise가 만들어져서 기능이 수행중일 때
( 기본 값은 undefinde 이다. )

✌🏼두번째,
Fulfilled
: 성공적으로 끝내고완료한 상태

🤟🏼세번째,
Rejected
: 파일을 찾을 수 없거나 네트워크의 문제가 있어서 에러를 호출한 상태

프로듀서 vs 클라이언트

각, 파트에서 담당하는 역할을 정리해 적어보았다.

기능, 정보를 제공하는 프로듀서 파트

  1. new Promise 를 이용해 promise 만들기
  2. resolve는 성공의 코드, reject는 실패의 코드
  3. rejectnew Error()라는 자바스크립트에서 제공 하는 Object( 객체 )를 사용한다.

기능, 정보를 소비 사용하는 소비자 클라이언 파트

  1. 함수, 변수 명을 메서드로 사용해서 그 기능을 then, catch로 성공, 실패 여부 판단 하기
  • then() : 성공의 여부를 나타내주는 기능
    ( 값을 바로 전달할 수 있고, 또 다른 비동기의 프로미스를 또, 새로운 프로미스를 만들어서 전달 할 수 도 있다. )

  • catch() : 실패했을 때 어떤 에러인지 작성 할 수 있고, catch뒤에 또 다른 then()을 보여줄 수 있게 해준다.
    ( catch를 사용하는 이유는 error로 인해 뒤의 반환될 값들을 모두 먹통으로 만들어 버리기 때문이다. )

  • finally() : 성공, 실패의 여부와 상관없이 어떤기능을 마지막으로 수행하고 싶을 때 사용한다.

❗️ 하나의 TIP ❗️
새로운 프로미스가 만들어 질때는, 엑스큐터( executor )라는 함수가 자동적으로 실행이 된다.
( 그래서 사용자가 원할때
즉, 버튼을 클릭했을 때 어떤 모션을 행할 때 코드를 만들어야되는 것을 생각해야한다. )


자 ! 이론적인 설명은 끝 ! 이제 실제 코드를 보면서 이해해보도록 하자.

1-1. Promise 사용 예시

// 프로듀서
function getData() {
	return new Promise( function ( resolve, reject ) {
		$.get( ‘url 주소 /products/1, function( response ) {
			if ( response ) {
				resolve( response );
			}
			reject(new Error (“Request issued failed”));
		});
	});
}

// 클라이언트
// $.get() 호출 결과에 따라 ‘response’ 또는 ‘Error’ 출력
getData().then( function ( data ) {
	console.log(data);		// response 값 출력
}).catch( function (err) {
	console.log(err);		// Error 출력
}).finally(() => {
	console.log( “ 성공 실패 여부와 상관없이 어떤 마지막의 기능을 수행하기 위한 파이널리!!!!);
});

앞의 이론적인 설명을 잘 기억한다면, 코드를 보고 사용법에 대해 이해가 가능할 것이다.

프로듀서 부분을 보면,
서버에서 응답을 제대로 받아오면, ressolve() 메서드를 호출
서버에서 응답이 없다면, reject() 메서드를 호출

클라이언트 부분을 보면,
호출된 메서드에 따라 then()이나 catch()로 분기해 응답결과 또는 오류를 출력한다.

1-2. Promise chaining (프로미스 체이닝 )

Promise Chaining이란
간단하게 이야기 하자면,
여러개의 Promise를 연결해준다 라는 말이다.

그러면, 어떻게 연결하는지 코드를 한번 살펴보자.

  • setTimeout() API를 사용한 예시
  • setTimeout()을 이용해 2초후에 resolve()를 호출하는 예제 이다.
function getData() {
	return new Promise( function ( resolve, reject ) {
		setTimeout( function () {
			resolve(1);
		}, 2000);
	});
}

// then() 으로 여러개의 프로미스를 연결하는 방식
getData()
	.then( function ( result ) {
		console.log(result);	// 1
		return result + 10;
	})
	.then( function ( result ) {
		console.log(result);	// 11
		return result + 10;
	})
	.then( function ( result ) {
		console.log(result);	// 21
	})

코드를 한 줄 한 줄 설명해보자면,

  1. resolve()가 호출되면 Promise가 대기 상태에서 이행 상태로 넘어가기 때문에 첫 번째 .then()의 로직으로 넘어간다.
  2. 첫 번째 .then()에서는 이행된 결과 값 1을 받아서 10을 더한다. 그 후, 두 번째 .then()으로 넘겨준다.
  3. 두 번째 .then()에서도 마찬가지로 바로 이전 Promise 결과 값 11을 받아 20을 더하고 다음 .then()으로 넘겨준다.
  4. 마지막 .then()에서 최종 결과 값 31을 출력된다.

❗️여기서 잠깐 하나의 TIP❗️
new Promise는 자동으로 호출된다.
Promise 내부함수는 동기이고 Promise안에 setTimeout 내부가 비동기 함수이다.


1-3. Promise Error Handling ( 프로미스 에러 핸들링 )

먼저, 에러를 처리 하는 방법은 2가지가 있다.
일단 코드를 보자 !

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

// 1. then()의 두 번째 인자로 에러를 처리하는 방법
getData().then( function () {
	// … 응답받기 성공 코드
}, function(err) {
	console.log(err);
});

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

과연 이 두가지 방법 중 선호하는 방법은 어떤 방법일까 ?

선호하는 방법은
catch()로 에러를 처리하는 방법이라고 한다.

❓이유는❓
then()으로 에러를 처리 했을 때 에러를 감지 못하는 오류가 있다.

예시를 보자.

  • then()의 두 번째 인자로는 감지 못 하는 오류
function getData() {
	return new Promise( function ( resolve, reject ) {
		resolve( ‘ 성공 !); 
	});
}
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()의 첫 번째 콜백 함수 내부에서 오류가 나는 경우 오류를 제대로 잡아내지 못한다.
코드를 실행하면 이런 에러가 발생한다.

그러면 catch() 메서드를 이용해 에러를 처리한다면 어떻게 나타날지 한번 보자 !

  • catch() 메서드를 이용해 에러를 처리하는 방법
function getData() {
	return new Promise( function ( resolve, reject ) {
		resolve( ‘ 성공 !)
	});
};

getData().then(function ( ressolve, reject) {
	console.log(result);	// 성공 !
	throw new Error(“Error in then());
}).catch(function(err) {
	console.log(‘then error :, err);	// then error :  Error: Error in then()
});

발생한 에러를 console.log()에 출력한 모습이다.
에러를 잘 처리하자.

❗️번외 : 비동기상황에 QUEUE(큐)

Javascript의 동작원리를 공부하면 당연시 알게 되는
이벤트 루프 ( event loop ) 관련된 이야기를 잠깐 하려고 한다.

비동기상황에 큐들을 구분해 보자면,

Macrotask Queue( 매크로테스크 ) : 타이머 함수, 이벤트 리스너
사용자가 어떤 행동을 행할 시 또는 몇초뒤에 이벤트가 행하는 기능들

Microtask Queue( 마이크로테스트 ) : Promise, process.nextTick, queueMicrotask , MutationObserver 만 포함된다.
나머지는 Macrotask Queue매크로테스크로 구분한다.

❗️TMI TIP❗️
이런일이 벌어질 일은 절대 없겠지만,,
비동기 코드 중.
setTimeout가 만약 0초, Promise 중에는
Microtask Queue ( 마이크로테스트 )인 Promise가 먼저 호출된다.


2. async & await

자바스크립트의 비동기 처리 패턴 중 가장 최근에 나온 문법이다.
기존의 비동기 처리 방식인 콜백함수Promise의 단점을 보완하고 개발자가 읽기 좋은 코드를 작성할 수 있도록 도와준다.

기본 문법

async function 함수명() {
	await 비동기_처리_메서드_명();
};
  1. 함수 앞에 async라는 예약어를 붙인다.
  2. 함수의 내부 로직 중 HTTP 통신을 하는 비동기 처리 코드( Promise ) 앞에 await을 붙인다.

❗️주의❗️
비동기 처리 메서드가 Promise 객체를 반환해야 await가 의도한 대로 동작한다.
( await의 대상이 되는 비동기 처리 코드는 axiosPromise를 반환하는 API 호출 함수다. )

Promise Chaining부분에 ❗️여기서 잠깐 하나의 TIP❗️에서 이야기 했지만,

  • Promise내부에서 await전에 선언한, 호출한 것은 동기이다.
  • Promise 내부함수는 동기
  • 서버에 연결하기 위한 API를 사용하는 부분이 비동기

2-1. async & await 사용 예제

// 프로듀서
function getUserList() {
  return new Promise(function(resolve, reject) {
    const userList = ['user1', 'user2', 'user3'];
    resolve(userList);
  });
}

// 클라이언트
async function fetchData() {
  const  list = await getUserList();
  console.log(list);
}
fetchData()	// [ ‘user1’, ‘user2’, ‘user3’ ]

그럼 async & await의 예외 처리는 어떻게 할까 ?

2-2. async & await 예외 처리

async function fetchData() {
	try {
		const userInfo = await getUserList();
		console.log(userInfo);
	} catch (error) {
		console.log(error);
	}
}
fetchData();
  • async & await에서 예외 처리 방법은 try & catch이다.
  • Promise에서 에러 처리를 .catch()를 사용했 듯이 async & await에서는 catch {}를 사용하면 된다.
  • 코드를 실행하다 발생한 네트워크 통신 오류 뿐만 아니라, 간단한 타입 오류 등의 일반적인 오류까지 catch로 잡아낼 수 있다.
  • 발견된 에러는 error 객체에 담기기 때문에 에러의 유형에 맞게 에러 코드를 처리하면 된다.

🌟 Promiseasync & await을 잘 적용해서 사용하는 것이 제일 BEST 인 것 같다.

2-3. 번외 async & awaitfor loop에서 사용하기

배열의 요소를 돌면서 ajax통신을 하는 등.. 비동기 작업할 때가 있다.
loop을 돌 때에는 for, forEach를 많이 쓰는데
for, forEach내부에 async&await 비동기 처리를 하게 되는데, 이때 버그가 발생한다.

❗️문제의 코드❗️

const params = [1, 2, 3, 4];

const resArray = [];
params.forEach(async param => {
	const res = await axios.get(`DATA_URL`);
	resArray.push(res.data);
};

console.log(resArray);		// []

이렇게 코드를 작성하게 되면
for, forEach에서는 모든 비동기 작업이 끝나는 것을 대기하지 않는다.

2-3-1. loop에서 반복문을 사용해 비동기 처리하는 방법

for of 사용 방법

  • for await of
  • 비동기로 열거자를 나열할 때 쓰인다.
const = params = [1, 2, 3, 4];

const resArray = []; 
for await ( const param of params ) {
	const res = await axis.get(`DATA_URL/?id=${param}`);
	resArray.push(res.data);
};

console.log(resArray);		// [x, x, x, x];

for of / for in 방법

  • for await of와 비슷한 방식이다.
  • for of 또는 for in으로 비동기를 제어할 수 있다.
for ( const param of params ) {
	const res = await axios.get(`DATA_URL/?id=${param}`);
	resArray.push(res.data);
};  

for ( const index in params ) {
	const res = await axios.get(`DATA_RUL/?id=${params[index]`);
	resArray.push(res.data);
};

2-3-2. 번외 : 반복문을 사용해 비동기처리에서 사용되는 Promise 메서드

이 부분 따로 업로드를 해야겠다.
지금은 간단한 사용법만 작성하고, 아 - 그냥 이런 메소드가 있구나 ? 하고 넘어가면 될거 같다.
Promise의 병렬과 직렬에 대해 공부해야겠다.
업로드를 한다면, 여기에 링크를 달아 놓을 예정이다.

🌟all, race를 비교하고 이해하면 직렬과 병렬 차이를 알 수 있다.🌟
1. Promise.all([])
2. Promise.allSettled([])
3. Promise.race([])

이 메소드를 사용하는 이유는❓

  • 위의 반복문만 사용한 방법으론
    첫 번째 비동기가 끝날때까지 기다리고, 두 번째 비동기 실행, 두 번째 비동기 끝나면 세 번째 비동기 실행으로 순차적으로 코드가 기다려진다.
  • 그렇지만 1, 2, 3이 모두 실행되고 1, 2, 3이 끝나면 그 때 코드를 흘러가게 할 때도 있는데
    그럴 때, Promise.all()을 사용한다.

사용 법을 보자.

// Promise.all()
const res = await Promise.all(
	paramsList.map(async params => this.$api.getAll(params));
);

console.log(res);		// [결과1, 결과2, 결과3]

모든 비동기가 끝나면 코드가 흐른다.
❗️ 한 번에 비동기가 실행되기 때문에 결과가 순서대로 들어가지 않는다 ❗️

🌟🌟🌟비동기의 결과가 param의 순대로 들어간다면 promise.all()이 아닌
async & await으로 하나 끝나면 결과 넣고, 두번 째 시작하는 방법으로 로직을 구현해야한다.
또는, promise.all()로 순서없이 배열에 추가 후, 원하는 기준으로 sort(정렬)하는 방법도 있다.


3. AJAX + Promise

AJAXasync & await이 나오기 전에 사용되던 HTTP를 통신하는 도구 이다.
최근에도 많이 쓰이기도 한다.

여러 개발자들이 사용하는 도구들은

  • AJAX
  • axios
  • fetch

가 있다.
각 종류마다 장단점이 있다.

번외 느낌으로 PromiseAJAX에 어떻게 적용시키는지 사용법을 적어본다.

3-1. API를 통해 id( key값 ) 받아오기

  • id를 통해 render될 자료를 비동기로 받아와 id에 해당 하는 정보 가져와서 console.log에 출력하는 상황이다.
const dataURL = `https://url`;

let dataId;
let dataInfo = [];

Promise.requset1 = () => {
	return new Promise( function ( resolve, reject ) {
		let xhr = new XMLHttpRequest();

		xhr.open(GET, dataURL, true);
		xhr.onload = function() {
			if ( xhr.status === 200 ) {
				ressolve(dataId);
			} else {
				reject( “request1 Error :+ xhr.status );
			};
		};
		xhr.send(null);
	});
};
Promise.request1().then(data => console.log(data));
  • 위의 코드에서 가져온 idrequest2함수의 인자로 전달해 해당 iddataInfo배열에 담는다.
  • 비동기적으로 이뤄져야함으로 함수를 이용해 스코프를 만들고 배열이 완성되기 전까지 resolve가 되지 않도록 해준다.
Promise.request2 = info => {
	return new Promise((resolve, reject) => {
		let i = 0;

		function userInfo() {
			if ( i < 30 ) {
				let xhr = new XMLHttpRequest();

				xhr.open(GET, url, true);
				xhr.onload = function() {
					if ( xhr.status === 200 ) {
						dataInfo.push(JSSON.parse(xhr.responseText));
						i++;
						userInfo();
					} else {
						reject(“request2 Error:+ xhr.status);
					}
				};
			} else {
				resolve(dataInfo);
			}
		};
		userInfo();
	});
};

잘 담아서 확인했다면 promise pendinguserInfoconsole.log()에 찍힌다.

Promise.request1().then(data => {
	Promise.request2(data)
		.then(function(userInfo) {
			console.log(userInfo);
		})
		.catch(err => console.log(err));
});
profile
어깨빵으로 부딪혀보는 개발끄적이는 양씨 인간

0개의 댓글