다른 언어와 마찬가지로 자바스크립트도 하나의 언어 체계를 갖고 있습니다. 서구의 많은 언어가 라틴어로부터 파생된 것처럼 자바스크립트도 여러 조상뻘 언어로부터 다양한 특징을 상속받은 언어입니다.
프로그래밍이란 인간의 언어로 기술된 것을 프로그래밍 언어로 번역하는 과정이라 할 수 있는데, 언어의 기능이 아니라 문법적인 특징을 깊이 이해하면 보다 풍부한 표현과 바른 사용이 가능해집니다.
예를 들어 ‘그녀들은 이쁘다’ 를 영어로 번역하려 하면 ‘그녀들’ 이란 단어가 없습니다. 따라서 바른 번역은 ‘they’ 인 것입니다. 이러한 언어 상의 상이함은 인간의 언어 간에도 굉장한 격차가 존재하는데, 하물며 컴퓨터용 언어와 인간의 언어 사이의 번역은 말할 필요도 없습니다.
문법을 구석구석 이해하면, 번역 시 세밀하고 정확하게 할 수 있는 기초체력이 됩니다.
이번과 2번째 포스트는 식과 연산자를 집중적으로 다루고, 3번째 포스트에서 문과 식, 문 병합 및 고급주제를 다룹니다.
언어의 문법이란 미묘하고 세심하며 동시에 많은 기호로 표현됩니다. 따라서 깊이 이해하려면 고도의 집중력을 필요로 합니다….도중에 포기하실지 모릅니다 ^^
자바스크립트 언어의 양대 구성요소
자바스크립트도 알골60의 자손답게 언어의 뼈대를 문과 식으로 구성했습니다.
우선 식은 expression 이라 하는데, 간단히 말해 하나의 값이 되는 것입니다. 여기서 값이란 기본값은 물론 객체 등의 참조값도 포함합니다.[1. 식은 많은 번역서에서 혼동을 줄이기 위해 표현식이라는 단어로 표기하기도 하지만, 수학적인 의미의 식과 같으므로 그냥 식이라 부르는 것이 더 타당하다고 생각합니다.]
자바스크립트가 인정하는 식에는 다음과 같은 식이 있습니다.
- 값 식 : 값 자체도 식으로 인정합니다. 다음과 같은 코드는 전부 올바른 값 식입니다.
3 //숫자 ok! 'test' //문자 ok! {a:3} //객체 ok! undefined //undefined ok! null //null ok!
- 연산식 : 연산자가 포함된 경우 결과는 반드시 하나의 값이 되어 최종적으로 값 식이 됩니다.
1+3 //4 값 수렴 ok! 5 > 3 // true 값 수렴 ok!
- 리터럴 : 리터럴은 대부분 객체 생성을 위한 일종의 매크로 입니다. 리터럴의 결과는 참조값이 됩니다.
{} //오브젝트리터럴 function(){} //함수리터럴 [] //배열리터럴 /abc/ //정규식리터럴
- 함수호출 : 함수를 호출한 결과는 값이 됩니다. 자바는 void를 반환하면 값이 될 수 없습니다만 자바스크립트는 명시적으로 반환하지 않아도 undefined 가 반환되어 언제나 값으로 수렴됩니다.
사실 모든 식은 오직 값 식만이 존재하는 것입니다. 결론적으로 다른 모든 식은 하나의 값이 수렴하여 값 식이 되는 것이므로 식 == 값 으로 이해해도 무방합니다.
-
-
- *
-
이에 반해 문이란 statement 라 하는데 의미 상으로 보면 인터프리터에게 내리는 지시문이라 할 수 입니다.
따라서 문은 파싱된 이후에 아무것도 남지 않게 되므로 값으로 쓸 수 없습니다.
var a = if( b) return 3; //에러! 문은 아무것도 남기지 않는다!
위에서 if( 식 ) 문; 형식은 미리 인터프리터에 정의된 제어문입니다. if문은 식이 참일 때 지정된 문을 실행하도록 되어있는 인터프리터를 위한 지시자 일 뿐 어떠한 값이 되지 않습니다. 따라서 이를 a 에 할당하려 해도 아무것도 없으므로 할당이 불가능합니다.
자바스크립트는 변수에 식이 아닌 문을 할당하려고 하면 위와 같은 이유로 모든 실행을 정지하고 죽어버립니다.
이러한 문은 당연하게도 인터프리터의 파싱 규칙 그 자체이므로 언어 규격 상 매우 엄격하게 정의되어 있습니다. 자바스크립트는 다음과 같은 문을 허용합니다.
- 제어문 : 미리 엄격하게 정의된 형식의 문들입니다. 다음과 같이 현재 버전의 자바스크립트에서 통용되는 문은 확정적으로 지정되어있으며 이외에도 미래를 위해 스펙만 구현된 문도 존재합니다.
if, if else, switch, for, for in, while, do while, case, label, continue, break, return, var, try, catch - 공문(빈문) : 아무것도 없는 문도 문으로 인정합니다. while문은 while(식) 문; 의 형태인데 아래와 같은 구문에서 공문을 문으로 인정하기 때문에 에러가 나지 않는 것입니다.
var i = 10; while( i-- ); //while에 공문! alert( i ); // i == 0
- 식문 : 자바스크립트의 큰 특징인데 식을 문으로 인정합니다. 식은 값이므로 값문이라 해도 무방합니다. 즉 아래와 같은 식문이 전부 에러가 나지 않고 정상적으로 작동합니다.
3; //식문 ok! 'aaa'; //식문 ok! func(); //식문 ok! {a:'test'}; //식문 ok! 3+5; //식문 ok!
문 자체는 단일한 하나의 문장입니다. 하지만 중괄호로 묶어 마치 하나의 문처럼 사용할 수 있는데 이를 중문이라고 합니다[2. 한 줄로 된 상대적인 개념은 단문입니다.]
다양한 제어문에서 문이 올 자리는 단문 또는 중문이 올 수 있습니다. if문의 경우 if( 식 ) 문; 의 형태인데 아래와 같이 단문도 올 수 있고 중문도 올 수 있습니다.
if( 3 ) 5; //if( 값식 ) 식문; 으로 성립! if( 1 ){ //if( 값식 ) 중문{식문; 식문;} 으로 성립! 5; 6; }
다른 알골60의 자손인 자바나 c계열에서는 중문을 만들면 변수의 스코프도 동시에 생성되어 지역변수 개념이 생깁니다. 자바스크립트에서 변수란 해시맵의 키이므로 함수를 통한 스코프객체를 만들어야 변수를 쓸 수 있습니다. 따라서 단지 중문을 생성한 것만으로는 지역 스코프가 생성되지 않습니다.
이 외의 문에 대한 자세하고 미묘한 사용법은 3번째 포스팅에서 다룹니다. 우선 식을 생각해보겠습니다.
식
이제 본격적으로 식을 살펴보겠습니다. 식과 관련된 주제는 다양합니다만 모두 설명하기는 불가능하므로 아래와 같은 순서로 간단간단히(^^;;) 짚어보겠습니다.
- 기본값과 래퍼객체
- 참조값과 가비지컬렉팅
- 연산자, 연산지연
- 식응용
식은 당연한 듯 하지만 매우 어렵습니다. 주의 깊게 특성을 이해하고 암기하지 않으면 능숙하게 사용할 수 없습니다. 많은 개발자가 식의 이해를 포기하고 대략 사용하기 때문에 수 많은 버그의 근원이 되거나 비효율적인 프로그램이 되어버립니다.
제어형 언어는 기본적으로 변수를 통한 상태로 알고리즘을 정의합니다. 상태의 핵심은 값이고, 값을 처리하는 모든 문법적인 요소가 식입니다. 식의 바른 이해야말로 알고리즘의 기본이라 할 수 있습니다.
식은 주제가 방대하므로 이번 포스트는 2번까지만 다룹니다. 2번 포스트에서 3, 4 번을 다룰 예정입니다.
기본값과 래퍼객체
기본형, 기본자료형, 기본값 등으로 불리는데 영어로는 primative 라 합니다. 저는 그 중 기본값이란 단어로 설명하겠습니다.
다른 언어와 마찬가지로 자바스크립트에서도 기본값이란 참조되지 않고 복제되며 immutable[3. 한번 정의되면 변화시킬 수 없는 상태를 갖는 것] 한 자료형(data type)이란 뜻입니다. 이에 해당되는 것으로 string, number, boolean, undefined, null, NaN 이 있지만, 의외로 많은 브라우저가 null 이나 undefined 를 오브젝트로 만들어 참조가 되도록 잘못 설계한 오류가 있습니다. 이 오류 때문에 브라우저에 따라 아래와 같은 말도 안되는 상황도 벌어집니다.
undefined = 3; if( undefined === 3 ) alert( 'ok' ); //ok!
쨌든 위에 열거한 자료형을 사용하면 참조되지 않고 복제되며 복제불가능하게 되는 것이 기본입니다만 각 기본값은 대응되는 객체래퍼클래스가 존재합니다.
만약 이를 이용하여 생성한 경우는 객체가 되기 때문에 복제가 아닌 참조로 작동하게 됩니다.
var a = "test"; //기본값으로 작동! alert( typeof a ); // 'string' var b = new String( "test" ); //참조값으로 작동! alert( typeof b ); //'object'
사실 String 객체의 많은 메서드는 무조건 새로운 문자열을 보내기 때문에 설령 b를 함수의 인자로 보낸다고 해도 b 자체가 변경될 가능성은 매우 희박합니다만, typeof 에서 문제가 발생하거나 문자열로 사용시 toString을 추가로 호출하는 오버해드가 발생합니다.
왜 아런 불편하기만 한 래퍼객체가 존재할까요? 그것은 객체로서 다양한 메서드를 이용할 수 있기 때문입니다.
var a = new String( "test" ); alert( a.charAt(0) ); //t alert( a.length ); //4
그런데 래퍼객체가 아니라도 아래와 같이 기본값형태에서 메서드를 쓸 수 있습니다.
var a = "test"; alert( a.charAt(0) ); //t
이건 어찌된 일인가 보면 현대 대부분의 언어가 지원하는 박싱이란 기능입니다. 즉 아래와 같은 일이 일어납니다.
var a= "test"; temp = new String( a ); alert( temp.charAt(0) ); temp = null;
박싱이란 결국 개발자를 위해 사용하기 편하게 만들어준 매크로 기능입니다. 박싱의 경우 래퍼객체를 직접 사용하면 개발자가 완전히 코드로 통제할 수 있습니다만 c로 된 인터프리터가 하기 때문에 박싱이 훨씬 빠릅니다. 하지만 박싱할 때마다 임시객체가 만들어지는 것은 엄연한 사실입니다.
아래 코드는 별 생각없이 쓸 수도 있습니다.
var a = "test"; var b = a.length + ":" + a.charAt(0) + ":" + a.charAt(1) + ":" + a.charAt(2) +":"+ a.char(3);
하지만 박싱처리되므로 내부 사정은 다음과 같을 것입니다.
var a = "test"; t1 = new String( a ); t2 = new String( a ); t3 = new String( a ); t4 = new String( a ); t5 = new String( a ); var b = t1.length + ":" + t2.charAt(0) + ":" + t3.charAt(1) + ":" + t4.charAt(2) +":"+ t5.char(3); t1 = null; t2 = null; t3 = null; t4 = null; t5 = null;
래퍼객체를 생성하고 사용하는 것보다는 박싱문법이 빠르지만, 객체를 만들고 가비지컬렉팅의 부하를 늘리는데는 변함없습니다. 브라우저 구현에 따라 임시객체를 키로 저장했다가 delete 시키는 경우는 그나마 즉시 메모리에서 해지되지만 위와 같이 null을 할당하는 구현에서는 과도한 가비지컬렉팅 부하를 걸기도 합니다.
따라서 레퍼 메소드 사용 빈도에 따라 직접 래퍼객체를 생성하는 편이 유리한 경우도 많습니다.
var a = new String("test"); var b = a.length + ":" + a.charAt(0) + ":" + a.charAt(1) + ":" + a.charAt(2) +":"+ a.char(3); a = null;
이러한 방식의 문제는 번잡하고 객체를 직접 관리해야 한다는 점입니다. 또한 최신 브라우저 중에는 박싱을 자바스크립트 머신 위가 아닌 c의 인터프리터층에서 처리하는 최적화를 도입한 경우도 있습니다.
하지만 구형 브라우저의 경우 단지 b에 한 번 할당하는 수준이 아니라, for로 대규모 처리된다던가 하면 성능이 크게 차이나게 됩니다.
ECMA표준은 모든 메서드를 정적으로 쓸 수 있게 규정하고 있습니다만, 실제 구현여부는 브라우저마다 차이가 심합니다.
var a = "test"; var b = a.length + ":" + //속성은 안되고! String.charAt(a, 0) + ":" + //첫번째인자가 대상 String.substr(a, 1); //est
각 기본값에 대응하는 함수는 Boolean, Number, String 입니다. 자바스크립트에서 박싱은 숫자, 불린, 문자열에 전부 작동됩니다.
alert( 3.57.toFixed(1) );// 3.5 alert( true.toString() ); //"true"문자열
참조값
참조값의 특징은 1복제되지 읺고, 2원본은 하나만 유지된 상태로, 3주소만 공유되는 것입니다. 자바스크립트에서는 위에서 언급한 6가지 기본값을 제외한 모든 것이 참조값입니다. 구분하기는 쉽죠.
참조값의 주요한 문제는 가비지컬렉팅입니다.다음과 같은 코드를 고려해보죠.
var a = b= {key:'test'};
동일한 객체에 대한 참조를 두 변수가 갖고 있습니다. 이중 한 쪽이 참조를 풀어줍니다.
var a = b= {key:'test'}; a = null;
하지만 여전히 남은 하나(b)가 참조를 쥐고 있습니다. b도 해지 합나다만 이번엔 null 이 아니라 다른 참조를 넣어 해지해 봅니다.
var a = b = {key:'test'}; a = null; b = {};
이제서야 최초 생성된 {key:’test’} 는 가비지컬렉팅 됩니다. 별거 아닌 이것이 참조변수 관리입니다만 이 별것 아닌걸 잘 못해서 가비지컬렉팅 되지 못한 객체들이 메모리를 잠식해 브라우저가 점점 느려지는 원인이 됩니다.
실제 객체가 참조되는 속성 때문에 함수의 인자 등으로 전달되면 원본이 훼손될 수 있습니다. c나 자바는 이를 막는 별도의 문법을 도입했습니다(final, const 등)
하지만 자바스크립트는 이를 통제할 방법이 없으므로 개발자의 의중을 파악하기 힘들고 오류의 원인이 됩니다.
var a = [3, 4, 5]; function test( arr ){ arr[0]++; //원본이 훼손되었다! return arr[0] + arr[1]; } test( a ); // (3+1=4) + 4 = 8 test( a ); // (4+1=5) + 4 = 9
언어가 지원하지 않기 때문에 명시적으로 final 이란 메서드를 구상해두는 것도 나쁘지 않은 아이디어입니다. 변경불가를 걸 방법은 없으므로 대안으로 복제본을 넘겨보죠.
Array.prototype.final = Array.prototype.concat; //복제본을 넘긴다! var a = [3, 4, 5]; function test( arr ){ arr[0]++; //복제본을 훼손할뿐.. return arr[0] + arr[1]; } test( a.final() ); // (3+1=4) + 4 = 8 test( a.final() ); // (3+1=5) + 4 = 8
요점은 자바스크립트에서는 개발자가 코드를 보고 인자가 객체로 주어질 경우 변경가능성이 있는지 아닌지를 코드로 표현할 수 있는 방안을 직접 강구해야한다는 점입니다 =.=;
가비지컬렉팅
자바스크립트의 메모리 기본 전략은 가비지컬렉팅입니다. 더 이상 참조하는 변수가 없으면 객체를 메모리에서 제거하는 것입니다. 반대로 가비지컬렉팅을 사용한다는 것은 자바스크립트에게 객체를 생성하거나 할당할 때의 메모리를 맡긴다는 뜻입니다.
문제는 바로 여기서 일어나는데 개발자가 자유롭게 객체 생성과 해지를 반복하면 메모리 단편화(fragmentation) 현상이 생깁니다. 이는 디스크에 생기는 단편화 현상과 같은데 다음과 같이 설명할 수 있습니다.
- 전체 가용 메모리 공간이 10메가라하자.
- 메모리영역의 가운데 쯤에 1메가짜리 객체를 할당하면
- 그 객체의 메모리를 기준으로 앞쪽에 4메가, 뒤쪽에 5메가의 공간으로 분할된다.
- 따라서 남는 메모리가 9메가지만 5메가 이상의 객체를 할당할 방법이 없어진다.
이를 해결하려면 메모리를 재배치해야합니다. 위의 경우라면 1메가짜리 객체를 가용메모리공간 제일 앞으로 옮기면 9메가가 통으로 남아서 5메가 이상의 객체를 할당할 수 있게 됩니다.
문제는 처음말한대로 메모리 어디에 객체를 할당할지는 개발자가 정하는게 아니라 자바스크립트가 정한다는 사실입니다.
따라서 메모리 재배치를 통해 단편화를 해결할 수는 없습니다. 그럼 어찌할까요..
가장 많이 사용하는 방법은 인생에서 가장 중요한 것부터 해라! 라는 거죠.
그 왜…넷상에 떠도는 글 중에 어떤 강의실에서 큰 항아리에, 먼저 큰 돌넣고, 작은 돌넣고, 마지막에 모레를 넣으면서 사람들에게 물었더니 사람들은 여유가 없는거 같아도 있다라고 답했는데 강사는 중요한것부터 하지 않으면 안된다고 답한다는 얘기 있잖아요.
그것과 같은 전략을 사용해야합니다. 프로그램이 구동되는 최소 시점에 프로그램이 사용할 객체 중 가장 덩치가 큰 객체를 미리 할당해버리는 거죠. 사실 이 전략은 거의 모든 언어에서 공통입니다. 단지 자바스크립트도 개발범위가 점점 확장되면서 이걸 받아들일 시기가 되었을 뿐입니다.
참고로 크롬이나 파폭은 이 문제를 대응하기 위해 메모리를 동적으로 재배치하거나 메모리를 불출할 때 단편화 현상을 최소화하는 휴리스틱알고리즘을 발전시켜가고 있습니다. 리눅스의 파일시스템이 단편화를 근본적으로 감소시킨 것처럼, 언젠가는 모두가 단편화에서 자유로워질 날이 올 것입니다. 그때까지는…
단편화와 더불어 또 한 가지 문제는 new의 비용이 비싸고 null 을 할당하여 가비지컬렉팅 대상이 늘어나는 것도 비용이 상당하다는 점입니다.
new를 줄이는 방법은 객체를 재사용하는 것입니다. 객체의 여러 속성을 다시 초기화하여 사용하는 것이 훨씬 고속으로 작동합니다. 예를 들어 아래와 같은 두 가지 경우를 생각해봅니다.
var i, arr; i = 1000; while(i--){ arr = [i]; //매번만든다 func( arr ); } i = 1000; arr = []; while( i-- ){ arr[0] = i; //하나를 재활용! func( arr ); }
둘 사이의 성능 차이는 어떤 브라우저를 막론하고 상당하게 납니다. 매번 새로운 객체를 만드는 비용과 그 때마다 기존의 arr 에 있던 객체를 가비지컬렉팅 대상으로 보내는 것은 비용이 만만치 않기 때문입니다.
이러한 객체의 재활용은 보통 풀링(pooling) 이라 알려져 있습니다. 가장 간단한 형태의 풀링은 단순히 object 를 통해 구현할 수 있습니다. 배열 풀을 만들어 관리해 봅니다.
var pool = {length:0}; function getArray(){ //풀에 있으면 length를 줄이고 반환, 없으면 새로 생성 return pool.length ? pool[--pool.length] : []; } function disposeArray( $arr ){ //초기화하여 풀에 넣고 length를 증가 pool[pool.length++] = $arr.length = 0, $arr; } a1 = getArray(); //최초 꺼내면 새 배열 a1[0] = 3; ... disposeArray( a1 ); //반납한다 a1 = getArray(); // 아까 반납한 배열을 재사용! a1[0] = 5;
적절한 풀링 정책을 세워 필요한 객체를 관리하면 new비용도 줄이고 가비지컬렉팅 대상도 크게 줄여 성능을 개선하게 됩니다.
결론
식과 문의 첫번째로 식의 구성요소인 기본값과 참조값의 다양한 특성을 알아봤습니다. 다음에는 식의 두번째 포스팅으로 연산자를 집중해서 알아보겠습니다.
disposeArray에서
pool[pool.length++] = $arr.length = 0, $arr;
를
pool[pool.length++] = ($arr.length = 0, $arr);
로 수정해야 할까요?