타임라인 조율하기
지난 글에서는 공유 자원을 동시성 기본형으로 안전하게 공유했습니다.
이번 글에서는 눈에 보이는 공유 자원은 없지만 타임라인이 함께 협력해야 하는 경우가 있습니다. 타임라인을 조율하기 위한 두가지 동시성 기본형을 만들어 봅시다.
- Cut: “병렬 콜백을 모두 기다린 뒤 다음 단계로 넘어가기”
- JustOnce: “정말 딱 한 번만 실행하기”
스터디 회차: 17회차 (2025년 9월 18일)
모든 병렬 콜백 기다리는 기본형 Cut
예제: 장바구니 계산 로직 속도 개선
tsx
function add_item_to_cart(item) {
cart = add_item(cart, item);
update_total_queue(cart);
}
function calc_cart_total(cart, callback) {
var total = 0;
cost_ajax(cart, function (cost) {
total += cost;
});
shipping_ajax(cart, function (shipping) {
total += shipping;
callback(total);
});
}
function calc_cart_worker(cart, done) {
calc_cart_total(cart, function (total) {
update_total_dom(total);
done(total);
});
}
var update_total_queue = DroppingQueue(1, calc_cart_worker);- 원래는
shipping_ajax가cost_ajax의 내부 콜백으로 순차 실행됐습니다. - 속도를 위해 병렬 호출로 바꾸면 평균 시간은 줄지만, 콜백 완료 순서가 바뀔 수 있어 간헐적 오동작이 납니다.
왜 속도가 더 빠른가요?

- 순서대로 처리하는 것보다 병렬로 처리하는게 더 빨리 끝납니다.
- 다만 나란히 실행되기 때문에 실행 순서를 보장할 수 없습니다.
total은 지역변수인데 왜 타임라인에 추가되나요?
- total은 지역변수입니다.
- 지역변수는 같은 타임라인에서 접근하기 때문에 액션이 아니었고 그래서 타임라인에서 제거했습니다.
- 하지만 이제는 total은 서로 다른 타임라인에서 공유하기 때문에 다이어그램에 그려야합니다.
타임라인으로 나타내기

- 속도를 개선한 코드를 타임라인으로 그리면 위와 같습니다.
문제점

- 서로 다른 타임라인에서 실행되는 두 콜백은 기대와 다른 결과가 나타날 수 있습니다.
- 두 콜백의 타임라인이 서로 다른 시각에 total을 업데이트하고, 그 사이에 DOM 업데이트가 끼어드는 상황이 생깁니다.
개선하기: Cut 이용하기

- 동시에 도착하는 ajax 응답을 모두 기다렸다가 DOM을 업데이트하면 해결할 수 있습니다.
- Cut을 이용해서 두 콜백이 모두 끝나고 다음 단계를 실행합니다.
Cut은 경쟁 조건을 막을 수 있습니다.
- 경쟁 조건은 어떤 동작이 먼저 끝나는 타임라인에 의존할 때 발생합니다.
- Cut은 여러 타임라인이 다른 시간에 종료되어도 기다릴수 있는 간단한 동시성 기본형입니다.
Cut 구현하기
tsx
function Cut(num, callback) {
var num_finished = 0;
return function () {
num_finished += 1;
if (num_finished === num) callback();
};
}- 호출 될때마다 호출된 횟수를 증가시키고 마지막 타임라인이 함수를 호출했을때 callback을 실행합니다.
- 따라서, 앞선 num 만큼의 호출을 기다리고 그 뒤에 실행하는 것을 보장합니다.
코드에 Cut 적용하기
Cut()을 적용하기 위해서는 두 가지를 고민해야합니다.
- Cut()을 보관할 범위
- Cut()에 어떤 콜백을 넣을지
tsx
function calc_cart_total(cart, callback) {
var total = 0;
var done = Cut(2, function () {
callback(total);
});
cost_ajax(cart, function (cost) {
total += cost;
done();
});
shipping_ajax(cart, function (shipping) {
total += shipping;
done();
});
}- 두 응답 콜백을 만드는 calc_cart_total 에 Cut()을 선언하는 게 좋습니다.
- calc_cart_total에는 이미 total 계산 후 부르는 콜백이 있으므로 Cut() 콜백에서 그대로 실행하면 됩니다.
- Cut() 은 두 콜백이 모두 실행되고 나서 Cut()의 콜백이 실행되는 것을 보장합니다.
자바스크립트에서 동시성 기본형
- 자바스크립트에서는 이미 구현된 동시성 기본형이 많이 있습니다.
- Cut()과 매우 비슷한 Promise.all()이 있기 때문에 이것을 사용하면 됩니다.
딱 한 번만 호출하는 기본형 JustOnce
예제: 처음 방문한 사용자에게 환영 메세지 보내기
tsx
function sendAddToCartText(number) {
sendTextAjax(
number,
"Thanks for adding something to your cart. Reply if you have any questions!"
);
}- 딱 한번만 실행되어야합니다.
- 하지만 sendAddToCartText는 계속 실행될 수 있습니다.
JustOnce 구현하기
tsx
function JustOnce(action) {
var alreadyCalled = false;
return function (a, b, c) {
if (alreadyCalled) return;
alreadyCalled = true;
return action(a, b, c);
};
}- alreadyCalled 는 false 값을 가지고 한번 실행되면 true가 됩니다.
- 여러번 호출하더라도 alreadyCalled 에 의해 action은 한 번만 실행됩니다.
개선하기: JustOnce 이용하기
tsx
var sendAddToCartTextOnce = JustOnce(sendAddToCartText);
sendAddToCartTextOnce("555-555-5555-55"); // 실행
sendAddToCartTextOnce("555-555-5555-55"); // 무시
sendAddToCartTextOnce("555-555-5555-55"); // 무시
sendAddToCartTextOnce("555-555-5555-55"); // 무시- JustOnce로 인해 첫번째 함수만 실행됩니다.
멱등원
- 최초 한 번만 효과가 발생하는 액션을 멱등원이라고 합니다.
- JustOnce 는 어떤 액션이든 멱등원으로 만들 수 있습니다.
정리하기
- 문제의 본질은 값이 아니라 순서입니다. 지역변수라도 여러 타임라인이 교차하면 공유자원처럼 관리해야 합니다.
- Cut은 N개의 병렬 콜백이 모두 끝난 시점을 한 군데로 모아 레이스 컨디션을 없애는 기본형입니다. 자바스크립트에서는
Promise.all로 대체할 수 있습니다. - JustOnce는 “딱 한 번” 실행을 보장하는 기본형입니다.
