1. 동기, 비동기, 블로킹, 논블로킹의 개념

- 동기(synchronous)의 사전적 정의는 ‘동시에 일어난다’를 의미하며, 비동기(asynchronous)는 반대로 ‘동시에 일어나지 않는다’를 뜻한다. 컴퓨터 공학에서 ‘동시에 일어난다/일어나지 않는다’는 개념은 특정 시간 동안 또는 그 전후로 작업을 수행하는 주체가 둘 이상인 경우에 사용되며, 작업을 같은 시간에 시작해서 동시에 끝내는 경우 또는 한 주체가 작업을 끝내는 즉시 다른 주체가 작업을 시작하는 경우(즉, 한 주체의 종료 시간 = 다른 주체의 시작 시간)를 ‘동기적’이라 하고 작업을 시작/종료하는 시간이 서로 일치하지 않는 경우를 ‘비동기적’이라 한다.

- 한편으로, 한 주체가 어떤 작업을 시작했을 때 다른 주체가 그 주체의 작업 종료 시까지 자신에게 주어진 작업을 수행하지 않는 경우를 생각할 수 있다. 이처럼 다른 주체가 작업을 수행함으로 인해 자신의 작업 수행이 중단되는 것을 블로킹(blocking)이라 하고, 반대로 다른 주체가 작업을 수행한다 하더라도 자신의 작업 수행을 중단하지 않고 계속 작업을 수행하는 것을 논블로킹(non-blocking)이라 한다.

- 보통 동기적으로 수행된다면 블로킹 방식으로도 수행되고 비동기적으로 수행된다면 논블로킹 방식으로 수행되는 것을 생각하기 쉬우나, 동기적이라 하더라도 논블로킹 방식으로 수행되는 경우도 있고 비동기적이라 하더라도 블로킹 방식으로 수행되는 경우도 있다.

2. 자바스크립트의 비동기 처리

1
2
3
4
5
6
7
function print_start_to_end(start, end)
{
    for (let i=start; i<=end; i++)
        console.log(i);
}
print_start_to_end(5, 10);
print_start_to_end(20, 30);

- 병렬처리를 하지 않는 프로그래밍을 하는 대부분의 경우 함수 호출은 동기적-블로킹 방식으로 작동한다. 예를 들어 위 코드의 경우 print_start_to_end(5, 10) 부분에서 함수를 호출했을 때 5부터 10까지 숫자를 다 출력한 그 다음에 비로소 print_start_to_end(20, 30) 부분에서 함수가 호출된다.

- 경우에 따라 함수의 호출 및 수행이 비동기적으로 이루어져야 할 때가 있다. 예를 들어 fetch() 함수는 특정 URL의 내용을 가져와 함수값으로 리턴하는 함수인데, 만약 이 함수가 동기적으로 동작한다면 이 함수 때문에 그 URL의 내용을 모두 다운로드 할 때까지 전체 페이지의 로드가 중단되는 일이 발생하게 된다. 이를 방지하기 위해 자바스크립트의 fetch() 함수는 비동기적으로 처리되며, 그외에도 비동기적으로 처리되는 함수가 여럿 있다.

* 비동기적으로 처리되는 함수를 그것이 비동기적으로 수행된다는 사실을 고려하지 않고 일반적인 동기 함수를 다루듯 다루면 다음과 같은 문제가 발생할 수 있다.

1
2
const response = fetch("config.json");
console.log(response.status);

- fetch() 안에 넣은 경로가 정상적이라 하더라도, 위 경우 콘솔에는 (정상적으로 페이지를 열었음을 뜻하는 200이 출력되지 않고) undefined가 출력된다. fetch()가 비동기적으로 처리되는 함수이므로, fetch() 함수의 호출이 있다 하더라도 fetch() 함수의 함수값 리턴 여부와 무관하게 호출 즉시 곧바로 다음줄의 실행으로 넘어가기 때문이다.

3. Promise 객체

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
let fruit1 = `사과`;

...

function constructor_func1(func2, func3)
{
    if (fruit1 === `사과`)
        func2(`fruit1의 값은 사과`);
    else
        func3(`fruit1의 값은 ${fruit1}`);
}

function cb_func1(param1)
{
    console.log(param1);
}

function cb_func2(param1)
{
    console.log(param1);
}

const obj = new Promise(constructor_func1);
obj.then(cb_func1).catch(cb_func2);

- Promise 객체는 파라미터로 함수를 전달하여 생성자를 호출해 생성하는 객체이며, 생성된 Promise 객체에 대하여 메서드로 then()과 catch()를 호출할 수 있다.

  • Promise 객체의 생성자 함수를 선언할 때 파라미터를 최대 2개까지 쓸 수 있는데, 첫 번째 파라미터를 흔히 resolve 함수, 두 번째 파라미터를 흔히 reject 함수라 한다. (생성자 함수는 일반적인 다른 함수 호출과 같이 파라미터를 전달하여 호출하는 경우가 없으므로, 이들 파라미터는 각각을 생성자 함수 코드 내에서 함수의 형식으로서 호출할 때 그 함수가 resolve인지 reject인지를 구분하기 위한 이름으로서 사용되는 것이라 볼 수 있다.)

  • then()과 catch() 메서드는 파라미터를 한 개 갖는데, 이들은 파라미터로 함수를 전달해야 한다.

  • then() 또는 catch() 메서드를 호출할 경우, 먼저 해당 Promise 객체를 생성할 때 생성자의 파라미터로 전달한 함수(생성자 함수)가 실행되며 then()/catch() 메서드에 파라미터로 전달한 함수는 그 생성자 함수 내부의 코드가 실행되는 과정에서 호출되게 된다. (resolve 함수가 호출될 때 then() 메서드에 파라미터로 전달한 함수가, reject 함수가 호출될 때 catch() 메서드에 파라미터로 전달한 함수가 호출된다.)

- Promise 객체의 특성을 이용하여, 비동기식으로 처리되는 함수를 마치 동기 함수처럼 사용할 수 있다. 예를 들어 fetch() 함수는 사실 Promise 객체를 함수값으로 리턴하는데, 이를 이용하여 위 2번에서 undefined를 출력했던 코드를 아래와 같이 수정할 수 있다.

1
2
const response = fetch("config.json");
console.log(response.status);
1
2
fetch("confing.json")
    .then(response => console.log(response.status));

아래 코드는 (undefined가 콘솔에 출력되는 위 코드와 달리) config.json의 내용을 가져오는 Promise 객체의 생성자 함수가 실행된 후 status 변수를 멤버로 갖는 객체를 resolve 함수의 파라미터로 전달하여 resolve 함수가 실행되며, 그 결과로 콘솔에 정상적으로 페이지를 열었음을 뜻하는 200이 출력된다.

- then() 메서드가 반환하는 함수값 또한 Promise 객체이므로, then() 메서드를 여럿을 이어붙여 호출할 수도 있다. 이 경우 앞의 then() 메서드의 인자 함수가 리턴하는 값이 그 바로 뒤 then() 메서드 인자 함수의 인자로 전달된다. 예를 들어, 다음과 같이 코드를 쓸 수 있다.

1
2
3
4
fetch("config.json")
    .then(response => response.json())
    .then(json => console.log(json))
    .catch(errorMsg => console.log(errorMsg));

- 참고로, then(), catch() 메서드의 인자 함수 내용이 위 코드의 3-4번째 줄과 같이 파라미터로 받은 변수를 인자로 그대로 넘겨 함수를 호출하는 것뿐이라면 다음과 같은 축약이 가능하다.

1
2
3
4
fetch("config.json")
    .then(response => response.json())
    .then(console.log)
    .catch(console.log);

4. await/async

1) async 예약어

- 함수의 function 예약어 앞에 async 예약어를 쓰면 그 함수가 반환하는 함수값이 Promise 객체로 변환되어 리턴된다. 예를 들어, 다음과 같은 코드를 쓸 수 있다.

1
2
3
4
5
6
async function func1()
{
    return `나는 사과가 좋아.`;
}

func1.then(str => console.log(`${str} 그리고 딸기도 좋아해.`));

2) await 예약어

- Promise 객체를 쓰는 경우 보통 그 뒤에 .then() 구문을 줄줄이 쓰게 되는데, 이것이 길어지면 가독성이 나빠질 수 있다. await 예약어를 사용하면, 비동기식으로 처리돼 Promise 객체를 리턴하는 함수를 쓰면서도 동기함수를 쓰듯 코딩할 수 있어 이러한 단점을 해소할 수 있다. 아래는 3번에서 썼던 코드를 await 예약어를 이용해 다시 쓴 것이다.

1
2
3
fetch("config.json")
    .then(response => response.json())
    .then(console.log)
1
2
3
4
5
6
async function printConfig()
{
    const response = await fetch("config.json");
    console.log(await response.json());
}
printConfig();

- 이는, Promise 객체를 반환하는 함수 앞에 await 예약어를 쓰면 (1)마치 동기 함수를 호출하는 것과 같이 그 함수가 Promise 객체를 반환할 때까지 기다리며 (2)await 예약어가 함수로부터 리턴된 Promise 객체를 Promise 객체의 결과값으로 변환한다는 특성을 이용한 것이다. 이러한 특성을 이용하면 Promise 객체를 반환하는 함수를 마치 동기 함수 쓰듯 쓸 수 있다.

- 단, 내부에서 await 예약어가 사용되는 함수는 항상 그 함수의 선언부의 function 예약어 앞에 반드시 async 예약어가 붙어야 한다(위 코드의 ‘async function printConfig()’ 구문을 보라). async 예약어가 붙어있지 않은 함수 내에 await 예약어가 사용되면 에러가 발생한다.

- 그런데 function 예약어 앞에 async 예약어가 붙으면 그 함수가 리턴하는 함수값이 Promise 객체형으로 변환된다고 했으므로, 이 함수를 호출하는 경우에는 반드시 Promise 객체를 다루듯 사용해야 한다. 예를 들어, 다음 코드에서 콘솔창에 출력되는 것은 json값이 아니라 출력값이 Promise 객체라는 표시(Promise {<pending>})다.

1
2
3
4
5
6
async function printConfig()
{
    const response = await fetch("config.json");
    return await response.json();
}
console.log(printConfig());

- 다만 이는 async function을 await 예약어를 쓰지 않고 호출하는 경우에 한하며, 같은 async function에서 다른 async function을 await로 호출하는 경우에는 마치 일반적인 함수값이 리턴되듯 리턴된다.

1
2
3
4
5
6
7
8
9
10
11
async function getConfig(){
{
    const response = await fetch("config.json");
    return await response.json();
}
  
async function printConfig()
{
    console.log(await getConfig());
}
printConfig();