JavaScript/문법 공부

async, await, promise로 비동기 동기 처리하기

한땀코딩 2020. 8. 16. 14:49

자바스크립트를 배워나가는 시점에서 가장 헷갈리는 것은 '비동기'인 것 같습니다. 어떤 개념인지는 이해하고, 대표적인 함수들은 알겠지만, 새로운 것을 가져올 때 의도한 대로 항상 동작하지 않아서 인터넷을 찾아보면 대부분 비동기였기 때문에 일어나는 문제였습니다.

자바스크립트에서도 다른 언어들처럼 db와 연결하여 쿼리문을 통해 데이터를 조회하거나 수정할 수 있습니다. 이걸 연습하던 도중, 쿼리문으로 조회한 내용을 어딘가 저장해두거나, 함수 안에서 받아서 추가적인 연산을 하고 싶거나, 혹은 그 값으로 다시 새로운 쿼리문을 작성해야 할 때 고민이 생겼습니다. 비동기다 보니, 그 값을 어딘가 담아두기도 애매하고 (node js 표준 입력에서 겪은 문제와 비슷한 거 같습니다) 하나를 처리하고 그다음으로 넘어가야 하는데, 콜백으로 구성을 하기가 힘든 문제가 있었습니다.

처음부터 설계를 잘해서 콜백 하나만 깔끔하게 넘겨주거나, 혹은 이벤트 리스너 등으로 처리할 수도 있을 것 같다는 생각은 들었지만, 무엇이 언제 바뀔지 모르는 시점에서 뽑아온 값은 안전하게 변수로 만들어서 반환시키고 싶다는 생각이 종종 들었습니다. 그렇게 인터넷을 찾다가 알게 된 방법이 바로 async, await, promise의 적절한 믹스였습니다.

최초 접근

let result = null;
db.all('SELECT * FROM users', [], (err, rows) => {
	if (err) throw "query error"
	else {
		// console.log(rows[0].name);
		result = rows[0].name;
	}
})

console.log(result); // null 이 출력된다. 의도대로라면 'Grace'가 출력되어야 한다. 

data.db 라는 SQLite 파일 안에 users라는 테이블이 있다고 가정해봅시다. 거기서 모든 유저를 뽑아와서 그중 첫 번째 유저의 이름을 저장해 두고 다른 데서도 활용하고 싶어서 처음에 이렇게 테스트를 해보았습니다. 그러나 콘솔 창에는 null이 출력됩니다. db에 비동기적으로 접근하기 때문입니다. 넣어주기 전에 이미 그다음 console.log함수가 실행되어 버리는 겁니다.

프로미스를 사용해보자!

그럼 여기서 미치는 생각은, 저 db.all 부분이 다 마치기 전까지 그다음이 진행되지 않도록 처리하면 되겠다였습니다.

let result = null;
new Promise((resolve) => {
	db.all('SELECT * FROM users', [], (err, rows) => {
		if (err) throw "query error"
		else {
			// console.log(rows[0].name);
			resolve(rows[0].name);
		}
	})
})
.then((result => {console.log(result)}));

이렇게 프로미스를 사용하면 resolve에서 값을 넘겨주어야만, 그다음 then에서 정의한 것들이 순차적으로 일어나게 됩니다. 저렇게 then에 넣어줄 콜백 함수가 명확하다면 프로미스 체이닝으로 작업을 할 수 있습니다.

그러나 여기서 제가 궁금해진 건, 저렇게 콜백이 없이 그냥 db를 조회해서 얻어낸 값을 반환만 해주는 함수를 만들려면 어떻게 할까였습니다. 소위 말해서 select를 할 수 있게 해주는 wrapper함수를 만드는 것이죠. 이 경우에 대해서 검색을 해보니 얼핏 보면 복잡할 수 있지만 async, await, promise가 같이 쓰이면 이런 효과가 있구나 하는 걸 깨닫게 되었습니다. 요약하자면, async함수로 만들고, 위에서 생성한 프로미스 앞에 await을 붙여서 resolve가 반환하는 값을 또 다른 프로미스 객체가 아니라 값으로 저장하는 것입니다.

async function select() {
  let result = null;
  result = await new Promise((resolve) => {
    db.all("SELECT * FROM users", [], (err, rows) => {
      if (err) console.log(err.message);
      else {
        resolve(rows[0].name);
      }
    });
  });
  return result; // 프로미스 객체를 반환한다. 
}

async function main() {
  console.log(await select());  // Grace가 출력된다.
}

main();

작성하고 보니 밀려오는 현타... 프로미스를 값으로 바꿨다가 그게 다시 반환되면서 프로미스가 되고 그걸 또 받아와서 값으로 바꾸는 기적의 코딩이더군요. 뭔가 좋지 않은 접근이라는 생각은 들었지만 몇 가지 사실을 다시 확인하게 가게 되는 계기였습니다.

  • promise앞에 await을 붙이면 resolve로 전달하는 것이 프로미스 객체가 아니라 값의 형태로 반환이 된다.
  • async 함수는 그 안에서는 값이었더라도 반환되면서 프로미스 객체로 바뀌기 때문에 그 값을 다시 조회하려면 그 함수가 불리는 함수 또한 async가 되어야 하고, 호출하면서 앞에 await을 붙여야 한다.

이런 접근이 처음 몇 번 값을 필요로 할 때는 급하게 사용할 수 있을지는 몰라도, 뒤로 갈수록 남발하면 코드의 가독성을 많이 해치게 될 것 같다는 생각이 듭니다. 이런 접근을 하기 전에 콜백으로 깔끔하게 넘겨주거나, 프로미스 체이닝 정도로 해결을 보는 것이 가장 좋지 않나 싶습니다.

번외: async 함수 내에서의 error handling

async 함수로 신나게 누더기 코딩을 하다가 갑자기 '왜 되지...' 상태에 진입한 적이 있습니다. 아래의 코드가 그 범인입니다.

async function test() {
	let result;
	await new Promise((resolve, reject) => {
		result = 1;	
		reject(3);
	});
	return result;
}

async function main() {
	try {
		console.log(await test());
	}
	catch(err) {
		console.error(err);
	}
}

main(); // 출력 결과 : 3

저는 null 이 출력되겠거니 하고 생각하고 있었는데, async 함수 내의 프로미스가 reject 할 때 반환하는 3이 출력이 되는 것을 확인할 수 있었습니다. 프로미스 객체 내의 reject는 그 객체 뒤에서. catch로 처리한다고만 생각하고 있었는데, 바깥에 있는 main 함수의 try~catch가 이 reject를 잡아내고 있는 것이죠. 알아보니, async 함수 안에서 발생하는 reject는 바로 throw와 동일한 작동을 한다고 합니다. 즉, reject를 만나는 순간에 에러라고 간주하고 그 에러를 throw 하고 그걸 밖에서 감싸고 있던 catch가 받은 것입니다. 이 점을 잘 알고 있으면 async 함수 내에서의 에러 핸들링이 조금 간편해질 수 있을 것 같습니다.

'JavaScript > 문법 공부' 카테고리의 다른 글

Node JS 표준 입력 받기  (2) 2020.07.27
JavaScript 비동기 - Promise 알아보기  (0) 2020.07.25