[js] 자바스크립트의 식과 문 3 / 3

일단 계획대로 되어 중간에 내용이 길어 편수가 늘어나는 불쌍사없이 이 글의 마지막 차례인 문을 설명하게 되었습니다 ^^

식에 대해 어느 정도 의미를 이해했다면 값을 다루고 저장하는 방법을 알게 된 셈입니다.

하지만 글을 기술함에 있어 기승전결이 있는 것처럼 핵심적인 내용이 값이라 할지라도 , 그 값이 어떻게 만들어진 것인지 흐름을 기술해야 합니다.

당시 너무 복잡하게 작동하던 컴터를 매우 단순하게 정리하여, 1메모리에서 값을 꺼내 2연산한 뒤, 3다시 집어넣는걸 반복하는 것으로 정리해버린 폰노이만 할베 덕분에 현재의 컴터는 존재하게 되었습니다.

하지만 그 댓가로 순서대로 기술해야하 원하는 결과에 도달하는 게 컴퓨터의 본질이 되어버렸죠.

이로 인해 프로그래밍은 일종의 시간적으로 앞 뒤의 순서가 있는 일종의 명령 흐름이 되어버렸고, 흐름을 도중에 막거나 다시 되돌리는 등의 통제를 해야 원하는 결과에 도달하게 되는데 이를 명령흐름제어라 하고 이를 처리하는 언어적인 기호를 제어문이라 합니다.

문은 의외로 공기를 호흡하는 것과 같이 익숙하시기 때문에 존재하는지도 잘 모를 정도입니다만 막상 섬세하게 쓰려면 어렵습니다.

마지막까지 최선을 다해보겠습니다….만..^^;

 
 
 

문에 대해

이 섹션은 어려우니 건너 뛰세요(진담으로!) 문이 무엇인지 좀 소상하게 고찰해보겠습니다…^^;

시리즈 첫 번째 포스트에서 간략히 기술했던 것처럼 문(statement)이란 컴파일러에게 내리는 지시명령입니다. 지시명령은 크게 두 가지로 나눌 수 있는데, 메모리상에 어떠한 구조물을 생성하라는 명령과 흐름을 제어하는 명령입니다.

  1. 구조물을 생성하는 명령은 class, structure, function 등이 해당됩니다만 자바스크립트는 함수가 객체이므로 문이 아니라 식이라 제외됩니다.
  2. 흐름제어는 크게 조건분기와 반복처리입니다.

구조물은 결국 런타임에 모든 구조를 생성하는 스크립트 입장에선 거의 무의미합니다. c나 자바는 프로그램을 만들어 컴파일한 뒤 실행하기까지 여러 단계를 거치는데 메모리상으로 다음의 중요한 절차가 있습니다.

  1. 프로그래밍 시 변수나 함수, 클래스 등을 사용하면
  2. 그 구조물에 해당되는 메모리를 가상으로 할당하여 로직을 컴파일한 뒤
  3. 실행시점에는 OS에게 받은 진짜 메모리에 가상메모리를 직접 연결해주고
  4. 함수나 클래스등 컴파일러가 생성한 구조물을 미리 메모리에 로드한 뒤
  5. 프로그래머가 작성한 코드를 실행한다.

따라서 컴파일러가 중심이 되는 언어에서는 구조물을 생성하는 문이 엄청 많고 그걸 통해 컴파일러는 프로그램이 초기 구동시 처리해야하는 다양한 작업을 초기화합니다.

하지만 인터프리터언어는 그러한 과정이 없이 이미 메모리에 인터프리터가 실행된 상태에서 코드에 따라 런타임에 하나씩 생성하는 식입니다. 따라서 가상메모리 기반으로 변수를 만들지도 않고 코드가 실행되기 전에 미리 메모리를 확보하여 올라가는 구조물도 없습니다.

모든게 실행시점에 생성될 뿐이죠. 그런 면에서 자바스크립트가 가장 핵심적인 구조물인 함수를 값으로 만들어버린건 비효율적인게 아닙니다. 어찌보면 짜피 값이 될 녀석을 공개해둔 정도입니다.

자바처럼 클래스나 함수를 런타임에 로딩하는 것을 기본으로 하는 언어조차 c를 흉내내기 위해 직접 클래스 구조에 접근할 수 있는 편리한 문법을 사용하지 않고 리플렉션을 이용하게 되어있습니다. 사실 리플렉션이 하는 짓을 보면 그걸 구지 그걸 통해서 해야하나 싶을 정도로 모든게 가능합니다.

그런 측면에서 자바스크립트의 함수객체는 걍 리플렉션이 처음부터 문법적으로 지원된다고 생각하면 이해하기 쉽습니다(개인적으로는 향후 ES6에 클래스가 도입된 후에도 혹시 자바 흉내낼거면 차라리 지금 함수가 값으로 맘대로 처리할 수 있는 수준의 기능을 클래스 구조물에도 다 반영된 문법이었으면 좋겠다고 생각합니다)

장황하게 여태 설명한 내용의 요점은 자바스크립트의 문에는 구조물을 생성하는 문법이 전무하다는 것입니다. 문으로 뭔가 메모리에 올라갈 것을 만들지 않습니다. 전부 식처리합니다.

따라서 남는 문은 흐름제어문입니다.

 
 
 

제어문

자바스크립트가 지원하는 문은 제어문, 식문, 공문이 있고 문은 단문이 기본이나 복문을 이용하면 단문이 올 자리를 대체할 수 있습니다(….라고 1번 포스팅에 썼습니다 ^^)

그 중 흐름제어와 가장 큰 관련이 있고 인터프리터가 강력하게 통제하는 제어문을 살펴봅니다. 제어문은 정확한 형식으로 사용해야 하고 그 형식도 완전히 정의되어있습니다.

주요한 문은 다음과 같은 엄격한 형식으로 되어있습니다. 정석대로 BNF로 기술하고 싶지만..일단 가볍게 보죠.

if( 식 ) 문;

if( 식 ) 문1; else 문2;

switch( 식 ){
case 식:
default:
}

for( 식 ; 식 ; 식 ) 문;

while( 식 ) 문;

do 문 while( 식 )

return;

return 식;

var 식별자,식별자 = 식,..

레이블:

break;

break 레이블;

continue;

continue 레이블;

이상에서 재밌는 예외를 많이 발견할 수 있습니다.

  1. 알골60의 자손들 특히 c계열들은 제어문, 복문이 세미콜론을 붙이지 않게 되어있습니다. 예를 들어 do while문의 경우 다음과 같은 표현이 됩니다.
    do a += i; while( i-- )

    문의 형식에 대한 이해가 없으면 엄청나게 이상해 보입니다만, 마찬가지로 보통의 {..} 복문이나 switch(){…} 의 경우에도 세미콜론을 붙이지 않습니다.

  2. 레이블은 사실 아무곳이나 선언할 수 있습니다. 다음과 같이 레이블을 선언하는 곳은 자유입니다.
    lable1:
    a = 3;
    b = 5;
    
    label2:
    c = 3;
    d = 4;
    

    하지만 break, continue만이 오직 레이블로 점프하는 기능이 있는데 이 때 지정가능한 레이블은 직후에 반복문이 와야하는 조건이 있습니다.

    label1:
    a = 3;
    b = 4;
    
    label2:
    for( i = 0 ; i < 3 ; i++ )
    	if( i == 2 ) break label1; //에러 label2만 지정가능!
    

    하지만 최근 연구된 다양한 프로그래밍 구조에 따르면 복잡 루프구문에서 빠져나가기 위해 레이블이 필요없다는걸 증명하고 있습니다. 점점 레이블을 사용되지 않고 있습니다만, 주석 대신 활용하는 경우도 많습니다.

    ajax:
    function loadHTTP(){..}
    
    dom:
    function dom(){..}
    

    이런 느낌이 되죠.

  3. switch문에서 중괄호 내부는 특수 레이블인 case와 default를 포함시킬 수 있습니다. 이 둘은 분류상 레이블이기 때문에 마지막에 콜론(:)을 붙이게 됩니다. case 레이블은 뒤에 식이 오는데 매번 비교시마다 식을 평가하므로 다음과 같은 일이 가능합니다.
    function test( $v, value ){
    	switch( value ){ //값을 평가하는데..
    	//미리 정의된 경우들..
    	case 1:...
    	case 2:..  
    
    	//인자로 받은 특수한 케이스
    	case $v:..  
    	}
    }
    

    위의 함수에서 case $v: 는 함수의 인자에 따라 비교할 값이 달라지는 셈입니다.

  4. var 문은 매우 특수합니다. var 문은 문이기 때문에 기본적으로 값을 반환하지 않습니다….만! for, if, while 등 식을 받는 대부분의 제어문이 식과 함께 var문을 허용합니다. 초 예외적인 경우라 할 수 있습니다.
    for( var i = 0, j = 10 ; i < j ; i++ )...
    

    위의 코드에서 원래 for( 식 ; 식 ; 식 ) 문; 으로 규정된 룰을 깨고 for( 문 ; 식 ; 식 ) 문; 형태가 허용된 경우입니다. 이는 c나 자바에서 할당문이 for의 첫번째 부분에 올 수 있는 것을 그대로 계승하기 위해 도입된 초법적인(?) 조치입니다. 뭐 일단 그렇게 생겼으니 어쩔 수 없습니다 ^^;

 
 
 

if문 if else문

두 개는 별개의 문입니다. if문은 아래와 같은 형식입니다.

if( 식 ) 문;

이에 반해 if else문은 다음과 같습니다.

if( 식 ) 문1; else 문2;

if else문이 재밌는 것은 문이 올자리에 if문이 다시 등장할 때입니다. 우선 아래와 같은 if else문을 생각해봅니다.

if( a > 3 ) //문1
else //문2

문1의 자리에 다시 if else문을 넣으면 아래와 같이 될 것입니다.

if( a > 3 ) if( b > 3 )//문1-1 else //문1-2
else //문2

혹은 문2의 자리에 if else문을 넣으면 아래와 같습니다.

if( a > 3 ) //문 1
else  if( b > 3 )//문2-1 else //문2-2

특히 else 뒤의 문에 다시 if else문이 오는 경우가 엄청 많아 마치 else if 라는 문이 있는 것처럼 여기질 정도입니다. 문1과 문2에 전부 if else문을 실제로 넣어봅니다.

if( a > 3 ) if( b > 3 ) c = 1; else c = 2;
else if( b > 3 ) c = 3; else c = 4;

이는 적당히 끊어쓰기를 하지 않으면 사람이 알아보기가 매우 어려워집니다(이 else가 어디 else야..라는 느낌이 됩니다) 보통은 원래 문의 형에 맞춰서 아래와 같이 끊어 쓰는게 맞겠죠.

if( a > 3 )
	if( b > 3 ) c = 1; else c = 2;
else
	if( b > 3 ) c = 3; else c = 4;

하지만 현실적으로는 else는 붙여쓰는 관습(?)이 있습니다. 이는 많은 알골60의 자손들이 elseif 라는 문을 지원하는 경우가 많아서 그렇습니다. 그래서 if는 분리하고 else는 붙여쓰는 경우가 많습니다.

if( a > 3 )
	if( b > 3 ) c = 1; else c = 2;
else if( b > 3 )
	c = 3;
else
	c = 4;

자바스크립트의 자동 세미콜론 붙이기는 if문에 함부로 작동하지 않기 때문에 맘대로 엔터를 쳐도 상관없습니다만 불안한 사람들은 강제로 중문으로 만들기도 합니다.

if( a> 3 ){
	if( b > 3 ){
		c = 1;
	}else{
		c = 2;
	}
}else{
	if( b > 3 ){
		c = 3;
	}else{
		c = 4;
	}
}

특히 자바스크립트에서 강제로 중문으로 만드는 중요한 이유가 자동줄바꿈처리를 막기 위함인 이상 문의 다음줄이 아니라 옆에 { 가 와야 됩니다. 사실 옆에 붙일것도 아니면 단문을 중문으로 만들 이유는 전혀 없죠(파싱 부하만 커질 뿐입니다)

하지만 이 경우도 관습에 의해 else에 올 if else문은 왠일인지 중문으로 안만드는 경우가 많습니다 ^^

if( a > 3 ){
	if( b > 3 ){
		c = 1;
	}else{
		c = 2;
	}
}else if( b > 3 ){
	c = 3;
}else{
	c = 4;
}

위의 코드에서 else에 올 if else문은 왠일인지 중문에 안들어있습니다. 하지만 이렇게들 쓰니까요 ^^

 
 
 

조건지연

식에 연산지연을 통해 무조건 실행하는 것이 아니라 실행할지 말지 판단하는 것처럼 문에는 조건에 따라 다음 문장을 실행할지 말지를 결정하는 조건 지연이 들어있습니다.

이러한 조건지연은 if, switch 문 등 직접 조건을 통한 분기를 처리하는 문은 물론이고 for, while, do while 등의 반복을 제어하는 문에서도 발견됩니다.

이런 문은 조건이 참일 때만 지정된 문을 실행하기 때문에 조건을 평가하기 전에 명령을 적재해 둘 스택공간을 소비하게 됩니다. 이 스택구조는 사실 함수의 재귀호출과 거의 다를바 없는 구조입니다. 따라서 다음과 같은 코드로 스택오버플로우를 일으킬 수 있습니다.

if( 1 ) if( 1 ) if( 1 ) if( 1 ) if( 1 ) console.log( 1 );

브라우저마다 차이가 있지만 크롬은 4500회 정도를 중첩하면 Uncaught RangeError: Maximum call stack size exceeded 로 죽어버리는데 비해 IE는 거의 무한으로 버팁니다(이번 포스팅 시리즈에서 꾸준히 보셨겠지만 크롬의 V8은 스택구조물은 죄다 진짜 스택구조물로 만들기 때문에 다중 스택구조가 되면 금방 죽어버립니다. 짜피 언어의 기본적인 내용인데 중첩 스택을 해소하는 구조로 얼마든지 만들 수도 있었을텐데 하는 아쉬움이 듭니다)

조건문과 비교해 반복문은 반복문을 몇 번이나 반복시켜도 매 반복마다 스택을 초기화하는 스택클리어(stack clear)기능이 있기 때문에 복잡한 알고리즘에서 유일한 해결책이 됩니다.

 

이에 대해서는 이미 동기화 vs 비동기화 [1], [2], [3] 시리즈에서 다루고 있어 자세한 내용은 생략합니다만, 조건 지연으로 인한 프로그램은 일종의 점프구조를 만들어 프로그램의 이해를 어렵게 합니다. 원래 조건문은 goto 점프문을 보기좋게 정리한 문법이므로 문이 위에서 아래로 흐르지 않고 특정 위치로 점프하게 하기 때문에 인간이 사고할 때 흐름이 끊어져버리는 것입니다.

 
 
 

루틴(routine)과 함수객체

어떤 문의 집합이 특정한 목적을 위해 반복적으로 사용되는 점을 발견한 초기 언어 설계자들은 이러한 문들을 모아둔 뒤 실행시킬 방법을 생각했는데 반복적으로 사용된다는 의미로 루틴이라 불렀습니다. 루틴이란 단어 자체가 단지 반복적이라는 의미가 아니라 규칙적으로 행하는 어떤 일의 순서와 절차라는 뜻을 내포하고 있으니 매우 정확한 표현입니다.

루틴은 대부분의 언어에서 함수문으로 지원되는데 비해 자바스크립트는 함수식으로 지원됩니다.

함수는 값을 반환하기 때문에 본질적으로 모든 문은 식으로 바꿀 수 있습니다. 예를 들어 다음의 if문을 생각해보죠.

if( a > 3 ){
	b = 3;
	for( i = 0 ; i < 5 ; i++ ) b += i;
}else{
	b = 5;
}

위의 if else 문은 if의 복문에 for문을 포함하고 있기 때문에 삼항식 같은 걸로는 절대로 환원시킬 수 없습니다. 이제 문을 함수로 옮기고 적당한 값을 반환합니다.

function IF( condition ){
	if( condition ){
		b = 3;
		for( i = 0 ; i < 5 ; i++ ) b += i;
	}else{
		b = 5;
	}
	return condition;
}

if( IF( a > 3 ) ){
	console.log( b );
}

기존의 if문이 IF함수 안으로 이사가 어떤 형태로든 값으로 환원시키면 다시 식 또는 문의 요소가 되어버립니다.
이러한 관점에서 함수는 루틴의 기능만 제공하는 것이 아니라 문을 식으로 바꾸는 역할도 합니다.

 
 
 

재귀호출과 반복문

문을 식으로 바꾸는 개념은 당연하다면 당연한 건데, 보통 루틴의 목적이 값을 도출하는 것이기 때문입니다. 예를 들어 Array의 indexOf라는 루틴을 생각해보죠.

Array.prototype.indexOf = function( search, start ){
	var i, j;
	for( start = start || 0, i = 0, j = this.length ; i < j ; i++ )
		if( this[i] === search ) return i;
	return -1;

};

indexOf의 최소 버전은 선형검색을 하는 간단한 루프로 이해할 수 있습니다. 이 때 이 루틴 전체의 목적은 i를 찾아 반환하는 것입니다.

이는 손쉽게 재귀함수로 바꿀 수 있습니다.


Array.prototype.indexOf = function( search, start ){
	//이번에 검사할 인덱스
	start = start || 0;
	//길이가 넘어가면 없는것
	if( start >= this.length ) return -1;
	//찾았다
	else if( this[start] === search ) return start;
	//아님 하나 증가시켜 다시 찾기
	else return arguments.callee.call( this, search, ++start );
};

반복문버전과 재귀호출버전을 비교해서 가장 먼저 알 수 있는 것은 i, j 라는 변수가 재귀버전에는 필요없다는 것입니다.

함수와 인자만을 이용해 결과를 도출할 수 있습니다. 어떻게 i, j 가 제거된 걸까요?

  1. 한 단계씩 진행될 때마다 기존의 인자를 갱신하여
  2. 다음번 재귀함수 호출에게 인자로 전달

하기 때문에 별도의 메모리를 유지할 필요가 없기 때문입니다.

그에 비해 반복문 버전에서는 for가 루프를 도는 동안 상태를 유지할 i, j 가 필요합니다.

이론적으로는 재귀함수호출이 그럴듯해보입니다만 제어형언어는 함수를 호출할 때마다 스택메모리를 생성하기 때문에 변수 따위와 비교할 수 없는 막대한 메모리를 소비합니다.

따라서 재귀함수는 스택오버플로우의 위험이 있습니다. 위의 예에서 길이가 100이 넘어가는 배열의 젤 끝에서 매칭되면 재귀버전의 indexOf 는 스택오버플로우로 죽어버립니다.

항상 로직은 제어문으로 작성되야만 안전합니다.

 
 
 

결론

이미 동기,비동기 포스팅시리즈라던가 식 부분에서 상당한 부분을 다뤘기 때문에 자바스크립트의 제어문에 대한 특별한 부분만 짚어봤습니다.

문과 식을 제대로 이해하고 인간의 언어를 섬세하게 프로그램으로 번역을 해보는거죠!