본문 바로가기
코딩과 알고리즘

node.js express | 템플릿쪽지함 #8. 기다려! await!

지난 스토리에서는 mysql2 모듈을 사용할 때 프로미스에 대해서 알아보았는데요.

https://itadventure.tistory.com/444

 

node.js express | #7. mysql2 프로미스

지난 스토리에서 다루었던 쪽지 템플릿에는 MYSQL 이라는 저장하는 부분을 비롯하여 클래스라는 부분까지 여러 요소가 추가되었는데요. node.js의 프로미스라는 특징적 부분까지 다루어야 해서 예

itadventure.tistory.com


프로미스란 일종의 약속 덩어리이기 때문에 콜백(callback) 라는 프로그래밍 개념과는 다른 의미이며
mysql 에서는 보통 아래와 같이 사용된다고 살펴보았습니다.

db.query(
  "SELECT x, y, r, memo FROM memolist"
).then((result) => {
  console.log(result[0]);
});


그런데 이런 프로미스는 일련의 순차적인 일을 처리할 때 한가지 문제가 있습니다.
이를테면 A라는 일이 처리되고 난 다음 B 라는 일을 처리해야 하는데,

A라는 일이 처리되는 중에도 바로 다음 코드로 진행이 이어지기 때문에
아래와 같은 코드로 작성시 정상적인 작동이 이루어지지 않습니다.
A, B, C가 순서대로 작동되지 않는다는 것이지요.

프로미스함수(A).then((result) => {
  결과A;
});
프로미스함수(B).then((result) => {
  결과B;
});
프로미스함수(C).then((result) => {
  결과C;
});


이를테면 아래와 같은 기능이 필요하다고 합시다.
별로 실효성은 없겠지만 시간이 대단히 많이 걸리는 상황을 가상으로 테스트할 목적입니다.

First 라는 작업은 3초가 소요됩니다.
Second 라는 작업은 2초가 소요되고,
SQL 결과를 받아오는 작업은 뭐.. 0.001초가 걸린다고 보도록 하죠.

그리고 이 처리 과정에는 아래와 같이 순차적 처리가 되어야 한다고 합시다.

First 라는 작업이 마친후에  Second 라는 작업이 시작되어야 하고,
Second 라는 작업을 마친후에 SQL  결과를 받아오는 부분이 실행되어야 한다고 합시다.

몇초간 지연하는 동작의 프로미스를 흉내낼 수 있는 기능으로는 setTimeout() 함수가  있는데요.

setTimeout(() => { 처리결과 }, 지연시간);


얼핏 생각하기는 아래와 같이 코드를 작성하면 작동할 것으로 생각할 수 있습니다.

// 첫번째 작업
setTimeout(() => { console.log("First"); }, 3000);
// 두번째 작업
setTimeout(() => { console.log("Second"); }, 2000);
// 세번째 작업
db.query(
	"SELECT x, y, r, memo FROM memolist ORDER BY number LIMIT 1"
).then((result) => {
	console.log(result[0]);
});


하지만 위 처리 결과는 아래와 같은 결과를 낳습니다. 

[
  TextRow {
    x: 162,
    y: 37,
    r: 18,
    memo: '내 아들아 네가 만일 나의 말을 받으며 나의 계명을 네게 간직하며'
  }
]
Second
First


완전 순서가 정반대이군요. 왜 그럴까요?

그것은 첫번째 기능이 실행이 된 상태에서 바로 두번째 기능이 실행이 이어서 실행되고,
두번째 기능이 역시 실행이 된 상태에서 세번째 기능이 실행되기 때문입니다.

간단히 말해 첫번째 작업과 두번째 작업을 걸어놓고 세번째 작업도 걸어놓었다고 보시면 됩니다.
이렇게 3개의 작업을 걸어놓은 상태에서는 가장 먼저 끝나는 작업이 가장 먼저 결과를 내뱉습니다.

SQL 쿼리문은 0.001초밖에(예를 들어) 안 걸리기 때문에 제일 먼저 끝납니다.
그러므로 쪽지 1개를 받아온 정보를 바로 화면에 출력하고,

[
  TextRow {
    x: 162,
    y: 37,
    r: 18,
    memo: '내 아들아 네가 만일 나의 말을 받으며 나의 계명을 네게 간직하며'
  }
]

그 다음으로는 2초가 지난 다음 Second 작업이 끝나게 됩니다.
그래서 아래와 같은 결과를 출력할텐데요.
이 때 확실히 아셔야 할 것은 작업이 걸린 시점으로부터 2초이지,
SQL문의 출력이 끝난 후 2초가 아니라는 것입니다

Second


그리고 이어서 3초가 걸리는 First 작업이 끝날텐데요.
역시 Second 작업이 끝나고 나서 3초가 아니라, 작업이 걸린 시점으로부터 3초입니다.
그러니까 Second 가 출력되고 나서 1초 후에 First 가 출력됩니다.

First


시간적인 흐름으로 보자면 아래 표와 같습니다.

그러면 만약 아래와 같이 처리하려면 어떻게 해야 할까요?
First -> Second -> SQL 문이 순서대로 실행되어야 합니다.

setTimeout 함수는 프로미스 함수가 아니기 때문에 콜백 함수로 처리해야 하는데요.
콜백함수를 이용한다고 할 때 아래와 같이 사용할 수 있습니다.

// 첫번째 작업
setTimeout(() => { 
	console.log("First"); 
	// 두번째 작업
	setTimeout(() => { 
		console.log("Second"); 
		// 세번째 작업
		db.query(
			"SELECT x, y, r, memo FROM memolist ORDER BY number LIMIT 1"
		).then((result) => {
			console.log(result[0]);
		});		
	}, 2000);
}, 3000);


첫번째 작업이 끝나는 시점에 두번째 작업을 시작하는 것이고,
두 번째 작업이 끝나는 시점에 SQL문을 실행하는 것인데요.
그 결과는 아래와 같습니다. 의도한대로 시간지연까지 충실하게 이행이 되지요.

First
Second
[
  TextRow {
    x: 162,
    y: 37,
    r: 18,
    memo: '내 아들아 네가 만일 나의 말을 받으며 나의 계명을 네게 간직하며'
  }
]


기능적으로는 문제가 없으나, 개발적인 측면에서는 문제가 있습니다.
무슨 문제가 있을까요?
바로 코드가 복잡해 진다는 것입니다.
3개의 순차작업 정도는 어느정도 해석이 수월하다 치더라도
만일 10개의 프로미스 기능이 순차적으로 실행되어야 한다면 어떨까요?

코드를 무려 10번을 들여써야 합니다. 아래와 같은 코드가 한없이 들여쓰기 된다 생각해봅시다,
복잡이라는 단어보다는 난잡이라는 단어가 생각나지 않으신가요? :)

// 첫번째 작업
함수1(() => { 
    결과1
    함수2(() => { 
        결과2
        db.query("쿼리").then((result) => {
            console.log(result[0]);
            함수3(() => { 
                결과3
                함수4(() => { 
                    결과4
                        :
                        :
                }, 1000);
            }, 500);
       });
    }, 2000);
}, 3000);


이 것을 해결하기 위해 나온 것이 await ( 어웨잇 ) 입니다.

await 는 프로미스 함수와 함께 사용되는데, 기능이 실행이 완료될 때까지 '기다려준다'는 기능을 가지고 있습니다.
await 기능을 사용하는 데는 한가지 제약이 있는데요.
바로 async ( 에이씽크 ) 함수 내에서만 사용이 가능하다는 것입니다.

이게 무슨 말이냐 하면,
아래와 같이 여러 기능들을 하나의 대표함수로 묶은 다음에, ( await + 프로미스 함수, 일반함수 혼합이 가능합니다 )

function 대표함수(){
    await 프로미스함수1();
    await 프로미스함수2();
    일반함수3();
    await 프로미스함수4();   
}


대표 함수 앞에 async 를 선언해줘야 합니다.
그렇지 않으면 오류가 발생하여 노드서버가 아예 실행조차 되지 않습니다.

async function 대표함수(){

그리고 이 대표함수를 호출해주면 안에 들어 있는 내용이 순서대로 쭈욱 실행이 됩니다.

대표함수();


그런데 여기서 한가지, setTimeout 은 프로미스 함수가 아닙니다.
단순히 콜백기능을 가지고 있는 함수이지요.
그래서 setTimeout 함수를 프로미스 함수로 바꿔주는 선언을 먼저 해줘야 우리가 의도하는 대로 진행할 수 있습니다.
보통 인터넷에 널려 있는 setTimeout 을 프로미스 코드로 바꿔주는 함수 정의입니다.

const sleep = milisec => new Promise(resolve => setTimeout(resolve, milisec));


이 부분은 현재 이해하기보다는 단순히 아래와 같이 프로미스를 위한 시간 지연 기능을 사용할 수 있는 함수를 만들어주는 것이라고 접근해 주시기 바랍니다.

await sleep(3000);


자 이제 3가지 작업을 묶는 함수를 하나 정의해 봅시다.

async function main () {
	await sleep(3000);
	console.log("First");

	await sleep(2000);
	console.log("Second");

	const result = await db.query(
		"SELECT x, y, r, memo FROM memolist ORDER BY number LIMIT 1"
	);
	console.log(result[0]);
}


그리고 이 함수를 실행해주면 됩니다.
그러면 main() 함수내에서 순서대로 실행이 되는데요. 10개든 100개든 순차적으로 실행하는데 무리가 없습니다.

main();


또한 이 코드는 자바스크립트의 최근 문법에 따라 아래와 같이 변환할 수 있습니다.

(async function () {
	await sleep(3000);
	console.log("First");

	await sleep(2000);
	console.log("Second");

	const result = await db.query(
		"SELECT x, y, r, memo FROM memolist ORDER BY number LIMIT 1"
	);
	console.log(result[0]);
})();


자바스크립트의 최신 문법에 추가된 부분이 과거와 달라 좀 혼동될 수 있는데요.
위 내용을 단순히 해석하자면 아래와 같습니다.

함수를 정의합니다.
async function () { ... }

이걸 괄호로 범위지은 다음
(async function () { ... })

뒤에 () 를 한번 더 붙여 괄호로 정의된 함수를 실행합니다.
이로서 함수를 정의하면서 동시에 실행이 가능합니다
(async function () { ... })();


이 때 얻는 장점으로는 main 과 같은 함수 이름 짓기를 할 필요가 없는 장점이 있습니다.
한단계 더 나아가서 아래와 같이 함수정의를 고쳐쓸 수 있습니다.

(async function () { ... })();
=> 변경
(async () => { ... })();


이렇게 정의된 함수로 이루어진 최종 전체 소스는 아래와 같습니다.
앞의 다중 들여쓰기 방식보다는 훨씬 간결해 보이지 않으신가요? :)

const express=require('express');
const bodyParser = require('body-parser');
const ejs=require("ejs");
const app=express();
const db = require('./db');

const sleep = milisec => new Promise(resolve => setTimeout(resolve, milisec));

(async () => {
	await sleep(3000);
	console.log("First");

	await sleep(2000);
	console.log("Second");

	const result = await db.query(
		"SELECT x, y, r, memo FROM memolist ORDER BY number LIMIT 1"
	);
	console.log(result[0]);
})();
node ex4.js

<결과>
First
Second
[
  TextRow {
    x: 162,
    y: 37,
    r: 18,
    memo: '내 아들아 네가 만일 나의 말을 받으며 나의 계명을 네게 간직하며'
  }
]


한가지 추신 설명을 하자면 이러한 async 함수 내에서는 await 방식을 사용할 수도 있고 안할수도 있다는 점입니다.
만일 아래와 같이 코드를 작성하면, Zero 와 First 가 동시에 출력됩니다.

sleep(3000)
.then((result) => {
	console.log("Zero");
});
	
await sleep(3000);
console.log("First");

오늘은 프로미스 함수와 함께 사용할 수 있는 await 과 async 에 대해 알아보았습니다.
아무쪼록 필요하신 분이 계셨다면 도움이 되셨길 바라며 다음에는 node.js의 클래스에 대해서 다뤄보도록 하겠습니다.
오늘도 여기까지 읽어주셔서 감사합니다. :)


다음 스토리 : https://itadventure.tistory.com/447?category=715914 

 

node.js express | 클래스? (Class)

중학교 영어시간에 이런 예문을 보신 적이 있으신가요? We were in the same class at school. ( 우리는 학교에서 같은 반이었다. ) 영어회화에서 class 는 대부분 학급, 수업 등의 의미로 사용되는데요. 컴퓨

itadventure.tistory.com