const testVar = function() { var i = 0, sum = 0; for( ; i <= 10000; i++) { sum += i; } } const testLet = function() { let i = 0, sum = 0; for( ; i <= 10000; i++) { sum += i; } } const testVarAndLet1 = function() { let i = 0; var sum = 0; for( ; i <= 10000; i++) { sum += i; } } const testVarAndLet2 = function() { var i = 0; let sum = 0; for( ; i <= 10000; i++) { sum += i; } } compare(100000, testVar, testLet, testVarAndLet1, testVarAndLet2);
크롬 : 흥미로운 결과이지만, 스코프에 따른 비용을 생각하면 당연한 결과일 수도 있겠다. for문 외부에서 let으로 선언한 sum에 for문 내부에서 접근하기 위해서는 블록스코프 체이닝을 한 단계 거쳐야 하기 때문에 비용이 발생한다는 것이다. 아마도 원글에서는 이 부분을 말하고자 했던 것 같다.
파이어폭스 : 네 가지 테스트에 대해 아무런 차이가 없다. 파이어폭스만 놓고 보자면 블록스코프 체이닝으로 인한 성능저하는 고려할 필요가 없을 것 같다.
사파리 : 특이하게 var만 사용한 경우나 let만 사용한 경우엔 성능이 비슷한 반면, 혼용하면 느려진다. ES6전용엔진, ES5전용엔진, ES5 + ES6 엔진이 각각 마련되어 있으며, 혼용엔진의 성능이 좀 떨어지는 것이 아닐까 추측된다.
그렇다면 내장 메소드를 활용한다면 어떨까?
for문을 forEach로 대체한 경우와 reduce를 이용한 직접계산 방식을 테스트해보자.
const testVar = function() { var sum = newArray(10000).fill(0).reduce((a, b, i) => a + i, 0); } const testLet = function() { let sum = newArray(10000).fill(0).reduce((a, b, i) => a + i, 0); } compare(10000, testVar, testLet);
한편 reduce를 이용하면 메소드 내부에서 외부 스코프의 변수를 호출할 일이 없으므로 상대적으로 매우 양호한 성능을 보이며, let과 var 사이의 차이는 없는 것으로 확인된다. 이러한 차이에 대해 보다 자세히 확인하기 전에, 변인을 통제하기 위해 스코프 자체의 생성 비용을 먼저 확인해볼 필요가 있을 것 같다.
const testNoScopeVar = function() { var sum = newArray(1000).fill(0).join(''); } const testNoScopeLet = function() { let sum = newArray(1000).fill(0).join(''); }
const testFunctionScope = function() { (function(){ var sum = newArray(1000).fill(0).join(''); })(); } const testBlockScope = function() { { let sum = newArray(1000).fill(0).join(''); } } compare(100000, testNoScopeVar, testNoScopeLet, testFunctionScope, testBlockScope);
기존까지의 테스트와 달리 이번 테스트는 파이어폭스가 가장 느리게 나왔으며, 사파리의 약진이 돋보여 한 번만 하겠다는 다짐을 깨고 세 번 돌려보았다. 놀랍게도 세 브라우저 모두 스코프 생성 자체는 성능상에 거의 아무런 영향을 주지 않는 것으로 확인된다. 혹시 스코프를 한 번만 생성했기 때문에 영향이 없었던 것은 아닐까? 스코프를 잔뜩 생성해서 테스트 해보자.
const testFunctionScopeVar = function() { var sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); } const testFunctionScopeLet = function() { let sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })(); sum = newArray(10000).fill(0); (function(){ sum = sum.join(''); })() }
const testBlockScopeVar = function() { var sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } } const testBlockScopeLet = function() { let sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } sum = newArray(10000).fill(0); { sum = sum.join(''); } }
이상하다. 앞서 테스트에서는 분명 블록스코프를 형성하는 for문에서 외부스코프에 접근할 때에 비용 차이가 있었는데, 이번에는 그 차이가 전혀 보이지 않는다. for문을 가지고 다른 변인통제장치를 마련하여 다시 한 번 테스트 해보자. ( 파이어폭스가 잠깐 넋이 나간걸까… ? )
외부스코프에 대한 비용 비교 - 2. for문
for문은 자체적으로 블록스코프를 형성하므로, 블록스코프와 즉시실행함수의 성능 차이가 거의 없다는 전제하에 var에 대해서도 let과 마찬가지로 외부스코프의 변수에 접근하게끔 for문 내부에 즉시실행함수를 넣어보았다.
크롬 : 스코프가 전혀 중첩되지 않은 첫번째의 결과가 가장 뛰어나고, 그 다음으로는 블록스코프 하나(for문 자체)로 이루어진 세번째 및 블록스코프 둘(for문 자체 + 내부)로 이루어진 다섯번째 결과가 동일한 성능을 보이고 있다. 반면 즉시실행함수는 상당한 비용을 소모하는 것으로 확인된다. 네번째 결과가 훨씬 높게 나타난 이유는 두번째와 비교해 스코프 중첩이 한 번 더 있기 때문이 아닐까 추측된다.
파이어폭스 : 스코프가 없는 상태의 var(testVarNoScope)보다도 블록스코프가 있는 상태(for)의 let의 결과(testLet)가 더욱 좋은 성능을 발휘한다. 모든 면에서 ES5 이하의 기능으로 구현한 소스보다 ES6에서 추가된 기능들이 더욱 좋은 성능을 보인다.
사파리 : 크롬보다는 좀더 빠른 성능을 보이고 있는데, 결과 사이의 상대적 차이는 크롬과 비슷하다.
이 테스트는 영 개운치 않다. 앞선 테스트에서는 블록스코프와 즉시실행함수 사이에 성능차이가 없었는데, for문 내부에서 호출한 즉시실행함수 만큼은 모든 브라우저에서 현저히 느리다.
A Lexical Environment is a specification type used to define the association of Identifiers to specific variables and functions based upon the lexical nesting structure of ECMAScript code. (중략) Usually a Lexical Environment is associated with some specific syntactic structure of ECMAScript code such as a FunctionDeclaration, a BlockStatement, or a Catch clause of a TryStatement and a new Lexical Environment is created each time such code is evaluated.
(발번역 주의) 렉시컬 환경은 ECMAScript 코드의 렉시컬 중첩구조를 기반으로 식별자(identifier)와 특정 변수 및 함수와의 연관성을 정의하기 위한 스펙 유형이다. 렉시컬 환경은 일반적으로 Function Declaration, Block Statement, try구문의 catch 절과 같은 ECMAScript 코드의 특정 구문 구조와 연결되며, 이러한 코드가 평가될 때마다 새로운 렉시컬 환경이 생성된다.
즉 ‘block statement’가 평가될 때마다 새로운 렉시컬 환경이 생성되며, 이 때 블록스코프가 생성되는 것으로 보아야 할 것이다. let이나 const 선언 유무와 무관하게 블록스코프는 무조건 형성된다. 기존의 함수 내부에 var 변수를 선언하든 하지 않든 함수스코프가 생성되는 것에는 어떠한 영향도 주지 않는 것과 마찬가지로 말이다.
요약 및 결론
각 브라우저별로 상황에 따라 상대적으로 빠른 연산을 수행하기도 하고 반대로 매우 느리게 처리하기도 하는 등, 성능이 다 다르다. 테스트 결과 특별히 어느 브라우저가 제일 뛰어나다는 판단은 내릴 수 없겠다.
블록스코프와 즉시실행함수 자체의 비용 차이는 크지 않은 것으로 확인되었다.
다만 for문 내부에 즉시실행함수를 삽입할 경우에는 모든 브라우저에서 매우 느리게 동작한다.
스코프체이닝으로 외부 변수에 엑세스할 때의 비용 역시 var와 let이 큰 차이를 보이지 않는다. 다만 파이어폭스의 경우 let이 var보다도 조금 더 빠른 성능을 보이고, 사파리의 경우 둘을 함께 쓸 경우 배로 느려진다.
if나 for처럼 그 자체가 블록스코프를 지니는 경우, 그 중에서도 block statement 외부의 변수를 내부에서 사용하는 경우에는, 크롬 및 사파리의 경우 var를 활용하는 편이 더 좋은 성능을 발휘하는 반면, 파이어폭스는 let을 그대로 사용하는 편이 더 낫다.
스코프 체이닝을 최소화하는 것이 좋다는 것은 당연한 상식이다. 그러나 이는 어디까지나 이론상 그렇다는 것이고, 테스트 결과 엔진 내부 로직에 따라(어떻게 구현되었는지는 모르겠지만 아무튼) 블록스코프가 성능에 거의 영향을 주지 않는 경우도 있는 것으로 확인된다(파이어폭스).
일반적인 경우 var 변수가 더 효율적 인지 여부는 브라우저마다, 상황마다 다르다. 기존의 코딩스타일 안에서 새로운 문법시스템을 판단하는 것 자체가 잘못된 접근일 수 있다는 생각도 든다. 기왕 블록스코프가 도입된 이상 블록스코프 내에서 독립적으로 처리할 방안(reduce 등)을 고민하고, 마땅한 수단이 없는 경우에 한해 부득이 외부 변수를 호출하되, var를 쓸지 let을 쓸지는 타겟 브라우저에 따라 판단해야 할 것 같다.
불과 1년 전 var와 let의 성능비교 테스트에 대한 블로그 포스팅을 읽은 적이 있는데, 당시에는 var가 let보다 압도적으로 빠르게 연산을 수행했던 것으로 기억한다. 그 1년 사이 둘의 성능은 같아졌다. 그만큼 최적화가 이루어져왔었기 때문이다. 그렇다면 앞서 확인했던 외부스코프에 대한 접근 성능 역시 점차 최적화가 될 것이라 기대한다.
세 브라우저가 각각의 상황에서 저마다 다른 성능을 보여주었다. 즉 최적화가 얼마나 어떻게 진행되었는지에 따라 성능은 얼마든지 달라질 수 있는 문제이며, 현재의 결론이 1년 뒤에는 또 어떻게 달라질지도 모를 일이다.
개인적으로는 파이어폭스처럼 다른 브라우저들도 외부스코프에 대한 접근 성능이 충분히 최적화될 것이라 기대하면서 지금부터 그냥 let을 쓰겠다. 혼용하는 편이 더 헷갈림.