0%

Concurrency

Concurrency

동시성


자바스크립트는…

동기식, 싱글스레드 언어 이다. 이는 즉,

  • 호이스팅 이후 순차적으로 코드를 실행함을 의미하며,
  • 페이지에서 자바스크립트 블록이 실행 중이라면 다른 자바스크립트는 페이지에서 실행되지 못함

을 의미한다.

이 때, 싱글스레드 프로그래밍 언어이기 때문에 동기적 방식에 비동기 처리가 필수적이다. 비동기 처리는 그 결과가 언제 반환 될지 알 수 없기 때문에 동기식과 같이 결과 반환을 예상할 수 있게 처리하는 기법들이 사용되어야 한다. 자바스크립트에서 이와 같은 비동기 처리의 대표적 방식은 setTimeOut , callback , promise 등이 있다.

자바스크립트는 Ajax를 호출하는 등의 기능을 수행할 때만 (자동적으로) 비동기식 언어로 작동한다. Ajax 호출에 대한 응답이 돌아와야만 (성공적이든 거절되든) Ajax 호출 실행이 중단되고 다른 (다음) 코드가 실행된다. 이 때, 콜백함수는 동기적으로 작동한다. 응답이 돌아오기 전까지, 콜백함수가 실행 중일 때는 다른 코드는 실행되지 않으며 실행 중인 다른 코드 (작동 중인 다른 프로그램) 를 방해하지는 않는다.


동기(synchronous)적 방식 : 현재 실행 중인 코드가 완료된 후 다음 코드를 실행

  • 요청 처리가 완료된 후 다음 요청을 처리 하는 방식으로 이전 요청을 처리하는 시간이 다음 요청에 영향을 준다.
    • 요청과 응답이 같은 시간대에 있다. (사이에 시간 간격을 두지 않음)
  • 일반적으로 작성한 코드는 보통 동기 방식으로 처리된다.

비동기(asynchronous)적 방식 : 현재 실행 중인 코드의 완료 여부 (매개변수가 아닌 함수 실행 여부) 와 무관하게 즉시 다음 코드로 넘어가서 실행

  • 요청과 응답이 다른 시간대에 일어날 수 있다.

  • setTimeout()

single-threaded

싱글스레드 언어는 한 번에 하나의 작업만 수행할 수 있다.

🤔 그렇다면 자바스크립트를 주로 사용하는 웹 사이트에서는 어떻게 한번에 여러 요청을 받을까? 그리고 여러 요청이 오갈 수 있는 자바스크립트는 왜 싱글 쓰레드일까?

정확하게 말하면 자바스크립트의 메인 쓰레드인 이벤트 루프가 싱글 쓰레드이기 때문에 자바스크립트를 싱글 쓰레드 언어라고 부른다. 하지만 이벤트 루프만 독립적으로 실행되지 않고 웹 브라우저나 NodeJS같은 멀티 쓰레드 환경에서 실행된다. 즉, 자바스크립트 자체는 싱글 쓰레드가 맞지만 자바스크립트 런타임은 싱글 쓰레드가 아니다.


콜백 지옥

콜백 함수를 익명 함수로 전달하는 과정이 반복되어 코드의 복잡도가 커지는 현상으로, 엄청나게 많은 중괄호 중첩을 사용하여 가독성이 떨어지고 코드를 수정하기 어려움.

  • 이벤트 처리나 서버 통신과 같은 비동기적인 작업을 수행하기 위해 콜백 함수를 이용함

e.g. 아래 코드는 0.5초마다 커피 목록을 수집하고 출력합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
setTimeout(
(name) => {
let coffeeList = name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);

setTimeout(
(name) => {
coffeeList += ', ' + name;
console.log(coffeeList);
},
500,
'Latte',
);
},
500,
'Mocha',
);
},
500,
'Americano',
);
},
500,
'Espresso',
);
1
2
3
4
5
> 출력값
Espresso (0.5초)
Espresso, Americano (1.0초)
Espresso, Americano, Mocha (1.5초)
Espresso, Americano, Mocha, Latte (2.0초)

각 콜백은 커피 이름을 전달하고 목록에 이름을 추가합니다. 정상적으로 실행되지만 들여쓰기 수준이 과도하게 깊어지고 값이 아래에서 위로 전달되어 가독성이 떨어집니다.


콜백 지옥 탈출
  • 기명함수 : 가독성 문제와 어색함을 동시에 해결하는 가장 간단한 방법은 익명의 콜백 함수를 모두 기명함수로 전환하는 것입니다.

    • 코드의 가독성을 높일 수 있고 함수 선언과 함수 호출만 구분할 수 있다면 위에서부터 아래로 순서대로 읽는데 어려움이 없다.
    • 일회성 함수를 전부 변수에 할당하는 것은 코드명을 일일이 따라다녀야 하기 때문에 오히려 헷갈림을 유발할 소지가 있다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    let coffeeList = '';

    const addEspresso = (name) => {
    coffeeList = name;
    console.log(coffeeList);
    setTimeout(addAmericano, 500, 'Americano');
    };

    const addAmericano = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addMocha, 500, 'Mocha');
    };

    const addMocha = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    setTimeout(addLatte, 500, 'Latte');
    };

    const addLatte = (name) => {
    coffeeList += ', ' + name;
    console.log(coffeeList);
    };

    setTimeout(addEspresso, 500, 'Espresso');
  • Promise

    • new 연산자와 함께 호출한, Promise 의 인자로 넘겨주는 콜백 함수호출 시 바로 실행되지만 (비동기적) 그 내부에 resolve 또는 reject함수를 호출하는 구문이 있을 경우 둘 중 하나가 실행되기 전까지는 then또는 catch로 넘어가지 않는다 (동기적). 따라서 비동기 작업이 완료될 때 resolve 또는 reject 를 호출하는 방법으로 비동기 작업의 동기적 표현이 가능해진다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    new Promise((resolve) => {
    setTimeout(() => {
    let name = 'Espresso';
    console.log(name);
    resolve(name);
    }, 500);
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Americano';
    console.log(name);
    resolve(name);
    }, 500);
    });
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Mocha';
    console.log(name);
    resolve(name);
    }, 500);
    });
    })
    .then((prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    let name = prevName + ', Latte';
    console.log(name);
    resolve(name);
    }, 500);
    });
    });

    아래는 위 코드와 같은 내용을 간결하게 표현한 코드이다.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    const addCoffee = (name) => {
    return (prevName) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    const newName = prevName ? `${prevName}, ${name}` : name;
    console.log(newName);
    resolve(newName);
    }, 500);
    });
    };
    };

    addCoffee('Espresso')()
    .then(addCoffee('Americano'))
    .then(addCoffee('Mocha'))
    .then(addCoffee('Latte'));
  • Generator

    • Generator 함수 : function* () {}
    • Generator 함수를 실행하면 Iterator가 반환되는데, Iterator next 메서드를 가지고 있다. 이 next 메서드를 호출하면 앞서 멈췄던 부분부터 시작해서 그 다음에 등장하는 yield 전까지 코드를 실행한다. 따라서 비동기 작업이 완료되는 시점마다 next 메서드를 호출하면 Generator 함수 내부의 소스가 위에서부터 아래로 순차적으로 진행되는 것과 같다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    const addCoffee = (prevName, name) => {
    setTimeout(() => {
    coffeeMaker.next(prevName ? `${prevName}, ${name}` : name);
    }, 500);
    };

    const coffeeGenerator = function* () {
    const espresso = yield addCoffee('', 'Espresso');
    console.log(espresso);
    const americano = yield addCoffee(espresso, 'Americano');
    console.log(americano);
    const mocha = yield addCoffee(americano, 'Mocha');
    console.log(mocha);
    const latte = yield addCoffee(mocha, 'Latte');
    console.log(latte);
    };

    const coffeeMaker = coffeeGenerator();
    coffeeMaker.next();
  • Promise + async/ await

    • 비동기 작업을 수행하고자 하는 함수 앞에 async 를 붙이고, 함수 내부에서 비동기 작업이 필요한 위치에 await 를 붙임으로써 해당 라인의 코드 내용을 Promise 로 (자동) 전환하고 해당 내용이 resolve 된 이후에야 그에 대한 코드가 진행된다.

    e.g.

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    const addCoffee = (name) => {
    return new Promise((resolve) => {
    setTimeout(() => {
    resolve(name);
    }, 500);
    });
    };

    const coffeeMaker = async () => {
    let coffeeList = '';
    let _addCoffee = async (name) => {
    coffeeList += (coffeeList ? ', ' : '') + (await addCoffee(name));
    };
    await _addCoffee('Espresso');
    console.log(coffeeList);
    await _addCoffee('Americano');
    console.log(coffeeList);
    await _addCoffee('Mocha');
    console.log(coffeeList);
    await _addCoffee('Latte');
    console.log(coffeeList);
    };

    coffeeMaker();



  1. Callback 대신 Promise 사용하기

    • ES2015/ ES6 기준
    • 콜백 지옥 방지
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// bad
require('request')
.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', (requestErr, response) => { // 1차 콜백
// catch 구문
if (requestErr) {
console.error(requestErr);
}
// then 구문 ... 반복
else {
require('fs')
.writeFile('article.html',
response.body, (writeErr) => { // 2차 콜백
if (writeErr) {
console.error(writeErr);
} else {
console.log('File written');
}
}
);
}
}
);

// good
require('request-promise')
.get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
// Promise가 resolved를 반환하였을 때
.then((response) => {
return require('fs-promise').writeFile('article.html', response);
})
// 다음 Promise
.then(() => {
console.log('File written');
})
// Promise가 rejected를 반환하였을 때
.catch((err) => {
console.error(err);
});
  1. Async/ Await 은 Promise보다 더욱 깔끔하다.

    • ES2017/ ES8 기준
    • 콜백에 대한 Promise을 단독으로 이용할 때보다 더 깔끔한 해결책으로, Promise을 활용한 방식이다.
    • 함수 앞에 async 붙이기
    • 함수의 연속적인 처리 (논리적으로 연결) 를 위해 then 을 계속 이어붙여 코드를 작성하지 않아도 됨
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    // not bad
    require('request-promise')
    .get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin')
    .then(response => {
    return require('fs-promise').writeFile('article.html', response);
    })
    .then(() => {
    console.log('File written');
    })
    .catch(err => {
    console.error(err);
    })

    // better
    async function getCleanCodeArticle() {
    // then 구문
    try {
    // then 작성 없이 await으로 synchronization
    const response = await require('request-promise')
    .get('https://en.wikipedia.org/wiki/Robert_Cecil_Martin');
    // then 구문 // await로 Promise 처리
    await require('fs-promise').writeFile('article.html', response);
    console.log('File written');
    }
    // catch 구문
    catch(err) {
    console.error(err);
    }
    }


Ref

https://velog.io/@yujo/JS%EC%BD%9C%EB%B0%B1-%EC%A7%80%EC%98%A5%EA%B3%BC-%EB%B9%84%EB%8F%99%EA%B8%B0-%EC%A0%9C%EC%96%B4