[js] 동기화 vs 비동기화 1 / 3

동기로직과 비동기로직은 브라우저 시스템 곳곳에 적용되어 자바스크립트에서는 이를 매우 일반적으로 사용하고 있습니다.
하지만 웹이 점점 고도의 앱으로 변화함에 따라 순수한 자바스크립트만의 로직도 무거워져 비동기화된 로직을 직접 구현해야 할 상황에 이르렀습니다.

동기화 로직의 문제점은 무엇이고, 어느 시점부터 비동기화 로직으로 바꿀 것인가를 간단한 예제를 통해 차근차근 접근해 봅니다.
이 포스팅은 총 3편으로 나뉘어 씁니다(원래 2부작으로 기획했으나 길어져서 3부작으로..)

( 저번 시리즈가 너무 길다고 핀잔을 많이 받아서…별거 아닌데..)

 
 
 

재귀호출과 반복문

제어형 언어에서 로직수준의 설계 전략은 크게 보면 반복문을 쓸 것이냐, 재귀호출를 쓸 것이냐로 나눌 수 있습니다.

우선 재귀호출을 사용하면 조건이 달성될 때까지 최초 호출된 함수가 종료되지 않은 상태로 내부 호출을 차곡차곡 쌓아가게 됩니다.
함수는 호출 시마다 스택메모리가 생성되는데, 이 스택메모리가 즉시 제거되지 않고 모든 재귀 조건을 달성되어야만 제거됩니다.

문제는 스택메모리의 한계가 있으므로 보통 100번 정도 내부에서 함수 호출를 하면 스택오버플로우(stack overflow) 에러를 토하면서 뻗어버립니다.
또한 자바스크립트에서는 스택메모리를 EC[1. Execution_Context 참조]로 계속 만들어야 하기 때문에 더욱 느립니다(그래도 100번만에 죽일 정도는 아닌데 일부러 그렇게 설정한 듯…)

제어문에 있는 for, while 등의 반복문을 사용하면

각 반복 주기마다 반복문 내부의 내용을 스택에 쌓지 않고 스택 클리어

하는 기능이 있습니다.

따라서 반복문을 몇 억번이든 반복시켜 실행할 수 있지만, 대신 일정 시간이 넘어가면 스크립트타임아웃(script timeout)에 걸려 뻗어버립니다.
특히 모바일브라우저는 불과 5초 정도면 스크립트타임아웃이 발생하기 때문에, 무게감 있는 로직을 반복문으로 처리하는데 한계가 너무 빨리 옵니다.

이상 두 가지의 로직 설계 전략은 서로 다른 한계를 갖고 있지만, 공통점이 있습니다. 바로 동기화 로직이란 거죠.
동기화 로직이란 간단히 말해 그 문장이 완전히 실행 완료되기 전까지는 다음 문장이 실행되지 않는 것을 말하며 또한 한 프레임 내에서 전부 실행될 것을 말합니다.
예를 들어 아래의 코드를 생각해볼 수 있습니다.

var a;
a = 3;
alert( a );
  1. a 가 선언된 뒤,
  2. a 에 3이 할당되고,
  3. 그 이후에 alert 이 실행되어 3이 출력될거라

생각할 것입니다. 순차적으로 각 문장이 실행 완료된 후, 그 다음 문장이 실행된다는 가정이 있기 때문입니다.
일반적으로 동기화로직이란 바로 이러한 순차적이면서 각 문장의 완료성을 가정한 상태에서 작성된 로직입니다(호이스팅은 대표적으로 동기화로직을 흐트러트립니다)

근데 한 프레임에서 실행된다는 것은 무슨 뜻일까요?
 
 
 

실행프레임

자바스크립트 엔진은 일종의 매 초마다 일정하게 호출되는 거대한 타이머 루프로 볼 수 있습니다.

브라우저 차원에서는 자바스크립트의 실행 뿐만 아니라 화면 그리기를 포함하여 다양한 시스템을 지속적으로 호출합니다.
감이 안오시면 그저 매초마다 자바스크립트 실행기, dom 렌더러, 이벤트처리기 등을 한 번씩 부른다고 생각하면 됩니다.

  • 자바스크립트 실행기는 매 프레임에서 적재대기하고 명령이 있다면, 실행하고 아니면 패스합니다.
  • dom 렌더러도 마찬가지로 화면의 일부나 전체를 갱신해야 한다면 하고 아니면 패스합니다.
  • 이벤트 처리기도 그 사이 발생한 이벤트가 있으면 통보해주고 아니면 패스.

브라우저는 뒷편에서 이걸 끝없이 반복하고 있는 것입니다.

일반적으로 개발자가 작성한 자바스크립트 코드는 전부 첫 번째 프레임에 적재됩니다.

만약 다음 프레임에 실행하고 싶다면 프레임지연 명령과 합께 코드를 작성해야합니다.

자바스크립트가 지원하는 프레임 지연명령 중 개발자가 통제할 수 있는 건 세 가지 입니다.

  • setTimeout : 일정 시간 후의 프레임에서 실행하도록 함수 안의 명령을 적재한다.
  • setInterval : 일정 간격마다의 프레임에서 계속 반복 실행하도록 함수 안의 명령을 적재한다.
  • requestAnimationFrame : 바로 다음 렌더링 프레임 타이밍에 맞춰 명령을 적재한다.

이 세 가지 명령을 이용하면 다른 프레임으로 명령을 적재할 수 있습니다. 그 외에는 내장 객체에 의존한 이벤트 리스너를 이용해 다른 프레임에서 실행할 수 있습니다.

  • dom 이벤트 : 해당 이벤트가 발생한 프레임에서 명령을 실행한다.
  • ajax 이벤트 : 응답과 관련된 내용을 수신하다가 특정 내용이 올 때마다 그 발생한 프레임에 미리 걸어둔 함수를 적재한다.
  • 기타 다수의 위와 같은 내장 객체의 이벤트 시스템 : img.onload 등

보통 이벤트리스너를 자주 사용하는게 일상인 자바스크립트 개발자는 프레임의 개념을 알던 모르던 여러 프레임에 명령을 적재하는 프로그램에 익숙한 것입니다.

샘플을 통해 이해를 강화해 봅니다.

window.onload( function(){
	alert( a ); //3
} );

var a = 3;

왜 3이 출력되었을까…이 원리가 바로 실행프레임의 원리입니다.

  1. var a = 3; 은 최초 1프레임에 적재되어 최초에 실행됩니다.
  2. function(){alert(a);} 의 경우는 window.onload가 발생한 시점의 다음 프레임에 실행됩니다.
  3. 예를들어 4프레임에서 이벤트가 발생했다면 5프레임에 미리 지정되었던 함수가 적재됩니다.
  4. 결과적으로 a = 3 이 1프레임에서 먼저 실행된 후 alert( a ); 가 실행되므로 3 이 나옵니다.

2

이벤트 리스너의 경우는 시스템에서 루프를 돌다가 이벤트가 발생되면 프레임시스템에 리스너를 적재해주는 방식으로 작동합니다.
window.onload 가 아무리 번개처럼 발생해도 절대로 1프레임에서 동시에 일어나지 않습니다.
반드시 다음 프레임으로 넘겨집니다.

setTimeout 은 어떻게 작동할까요?

setTimeout( function(){
	alert(a);
}, 1000 );

var a = 3;

우선 처음에는 위와 동일하겠죠.  
setTimeout 을 호출한 시점의 시간을 기록한 뒤,
내부 타이머가 1초가 넘어가는 순간을 대기하다가 넘어가면 그 다음 프레임에 호출할 겁니다. 간단히 그림만 보죠.

3

여기서 재밌는 건 setTimeout 과 setInterval 의 성능 차이가 꽤 난다는 것입니다.

setTimeout 은 우선권이 낮은 건지 자바스크립트에서 1000ms 라고 설정해도 1056ms 나 심지어 1100ms 가 넘어가서 반응하기도 합니다.
즉 설정한 시간보다 심하게는 0.1초 이상 늦게 반응합니다.

그에 비해 setInterval 의 경우는 오차가 일정하게 발생하고 그 차이도 0.1초 이내인 경우가 대부분입니다.
따라서 다음과 같이 setTimeout 을 setInterval 대용으로 사용하시는 경우 시간이 지날수록 오차가 점점 커질 거라 생각해야합니다.

function repeat(){
	//...
	setTimeout( repeat, 100 );
}
setTimeout( repeat, 100 );

requestAnimationFrame 은 최신 브라우저에 도입된 새로운 기능입니다. 이는 코드가 다음 번 렌더링 갱신 프레임에 맞춰 실행될 것을 지정하는 것입니다.
이 원리 상이라면 바로 다음 프레임으로 명령을 지연시킬 수 있습니다. 유명한 기능이므로 설명은 생략합니다.

 
 
 

비동기로직의 의미

결국 자바스크립트에서 말하는 비동기로직이란 여러 개의 프레임으로 명령을 분산해 적재하는 것을 말합니다.

  1. 명령을 개발자가 직접 적재하는 방법도 있고 : setTimeout, requestAnimationFrame, setInterval
  2. 시스템이 적절하게 적재하는 방법도 있습니다 : 이벤트 리스너 지정 등

자바스크립트는 왜 전통적인 쓰레드 시스템 등으로 직접 개발자가 제어하게 하지 않고 이러한 모델을 사용하는 걸까요?
.
.
우선 싱글쓰레드 제약을 생각해볼 수 있습니다. 자바스크립트는 webworker 를 사용하지 않는 이상 쓰레드를 하나만 사용합니다. 기본적으로 동시성 있는 작업을 처리할 방법이 없습니다.
이를 극복하기 위한 전통적인 솔루션은 빈자의 쓰레드(poor man’s thread) 입니다. 게임 만들 때는 이 스타일을 즐겨 씁니다.

즉 하나의 쓰레드가 계속해서 큐를 체크해가며 할 일이 있으면 하는 거죠.
이 시스템에서는 뭔가 하고 싶은 일이 있으면 큐에 넣어두고 쓰레드가 돌면서 큐에 있는 내용을 꺼내 실행하는 식이 됩니다.

빈자의 쓰레드는 멀티 쓰레드에 비해 분명히 느리고 멀티코어를 이용하지 못하는 단점은 있지만 장점도 있습니다.

  1. 쓰레드자원을 하나만 소비하는 것만으로 고성능 알고리즘을 구현할 수 있고,
  2. 싱글쓰레드고 큐의 순차실행을 보장하기 때문에 동기화 문제가 발생하지 않고, 처음부터 동기화 자체도 필요없습니다.

이런 싱글 쓰레드 모델에서는 빈자의 쓰레드처럼 큐를 사용하는 방법도 있지만, 코루틴(coroutine)화 시키는 방법도 있습니다.

자바스크립트는 오히려 개념 상으로는 코루틴에 가까운데, 자바스크립트 파서를 하나의 함수라고 할 때,

처리할 스크립트가 있으면 처리한 뒤, wait하다가  프레임이 바뀌면 다시 호출되는 식의 코루틴으로 볼 수 있습니다.

 

요점은 자바스크립트에서의 비동기 개념은 일반적인 언어에서 비동기와는 다르다는 점입니다.

다른 실행프레임에서 실행될 명령을 함수화하여 시스템에게 넘겨주면, 시스템이 이벤트등 관련된 데이터의 변화를 감지한 순간 해당되는 프레임에 그 명령을 적재해주는 식의 시스템 전반을 가리키는 말입니다.

c 나 자바같은 언어는 언어 자체가 프레임이라는 개념이 없으므로, 비동기라는 것은 함수 등의 호출 후 즉시 제어권이 넘어오는 것을 의미합니다. 즉시 제어권을 넘기려면 쓰레드를 생성하여 넘겨주던가 하는 방식으로 처리됩니다. 전혀 자바스크립트와는 다른 양상으로 전개되죠.

 

싱글 쓰레드 제약 내에서 블록킹(blocking)되는걸 막고자 기본적으로 제공해주는 시스템이 바로 프레임시스템이고 다른 프레임에서 실행되도록 명령을 지연시키는 것을 자바스크립트의 비동기로직이라 합니다.

 
 
 

비동기로직의 사용처

명령을 프레임별로 다르게 실행시키는데는 중요한 네 가지 이유가 있습니다.

  1. 1프레임에 너무 많은 명령을 적재하면 최초 1프레임의 렌더링이 늦어져 유저가 화면을 볼 수 있는 대기 시간이 길어진다.
  2. 애니메이션 처럼 시간자체를 지연하고 싶어 지연한다.
  3. 시스템이 수신하는 데이터(이벤트)는 시스템만 루프를 돌며 대기하고 스크립트는 발생한 후 통보를 받는 식으로 하여 부하를 줄일 수 있다.
  4. 한 프레임에서 실행되는 명령에는 시간 제약이 있으므로 무거운 로직은 여러 프레임에 걸쳐 처리하도록 해야한다.

그 외에도 많이 있습니다만…일단 포스팅에서는 이 정도만 정리할 예정입니다.

 
 
 

#1. 최초 렌더링의 문제

최초의 1프레임은 매우 중요합니다.

자바스크립트 입장에서는 가장 많은 명령이 적재되는 프레임이고,

유저에게는 최초의 화면이 그려지길 기다리는 시간이기 때문입니다.

렌더링은 사실 자바스크립트의 해석이 완전히 끝나면 그려지는게 아니라 로딩된 html 만큼 지속적으로 렌더링을 하면서 갱신해가는 시스템을 쓰고 있습니다.

하지만 도중에 자바스크립트를 만나면 반드시 이를 다 해석하고 실행해야 이어서 렌더링이 진행됩니다.

html 을 파싱하고, css 를 적재하여 화면을 그리는 것은 자바스크립트를 처리하는 것과 별도이기 때문에 만약 1프레임에 아무런 자바스크립트도 없다면 html이 렌더링되는데 있어 방해물이 없으므로 유저는 훨씬 신속하게 화면을 볼 수 있게 됩니다.

바로 이 점이 최대한 1프레임에서 자바스크립트를 제거하고 싶은 이유입니다. 이는 아주 쉽게 달성할 수 있습니다. 스크립트 로딩이나 자바스크립트의 실행을 다른 프레임으로 지연시켜버리면 됩니다.

<script>
setTimeout( function(){
	var s = document.createElement( 'script' );
	document.getElementsByTagName('head')[0].appendNode( s );
	s.src = "main.js";
}, 50 );
</script>

이렇게만 해도 충분하겠죠. 전부를 미루지 않는다고 해도 될 수 있는 한 1프레임에서 스크립트를 적게 사용할수록 렌더링이 쾌적해집니다.

 
 
 

#2. 애니메이션

애니메이션은 원래 명령을 일정 시간마다 실행시켜 실현할 수 밖에 없습니다. 따라서 별도의 시스템이 애니메이션을 처리해주는 트랜지션을 사용하면 좋습니다만 IE 9까지 안되는 문제도 있고 하니 당분간은 실행지연으로 해결해야겠죠.

1초간 div를 0~100만큼 오른쪽으로 이동시켜봅니다.

var d = document.getElementById( "test" );

//1초후
var endTime = 1000 + new Date;

var interval = setInterval( function(){

	var duration = endTime - new Date;

	if( duration > 0 ){ //진행중
		d.style.left = 100*(1-duration/1000)+'px';
	}else{ //종료
		clearInterval( interval );
		d.style.left = '100px';
	}

}, 10 );

애니메이션을 위해 setInterval 을 통해 함수의 명령을 지속적으로 다른 프레임에서 실행해 매번 다른 값을 left 에 넣어주고 있습니다.

 
 
 

결론

아직 결론을 낼 때가 아닙니다.

다음 포스팅에 이어서 남은 두 가지 주제인 이벤트리스너와 무거운 동기화 로직을 알아보고 xml파서와 코루틴을 작성해보겠습니다.