본 포스팅은 양성익님의 속깊은 Javascript를 읽고 정리해본 글입니다.
웹과 자바스크립트
웹에 대해선 위코드 커리큘럼 중 2주차에 세션을 통해 간단히 정리해본 적이 있는데, 그 글이 궁금한 분들은 여기로 이동 고고
1. 자바스크립트와 ECMAScript
자바스크립트, 누군가는 더 줄여서 자스라고 부르는 JS의 원래의 이름은 Mocha! 넷스케이프의 Brendan Eich가 2주간 만들었다고 와… 세상 대단한 사람!!! 이후 LiveScript라는 이름으로 세상에 공개쓰..! 브랜단씨의 회사인 Netscape가 Sun Microsystems사와 협업하면서 자바 애플릿이 웹의 트랜드를 주도하게 되었고, 이에 따라 이름도 자바스크립트가 되었다.
그렇다면 ECMAScript는 무엇일까? European Computer Manufacturers Association의 줄임말로써, 넷스케이프에서 자바스크립트의 표준화를 위한 작업을 한 것! 이런 표준화 작업과 여러 개발자들의 오픈소스 프로젝트 참여로 인해 지금의 JS가 존재한다고 할 수 있겠다!
2. 웹 개발 방법론의 변화
<1> 2000년대 이전의 웹
2000년대 이전의 웹 페이지를 떠올려보면 거의 신문이나 다름 없는 글자 투성이의 웹페이지였다.
이미지 같은 멀티미디어 요소는 최대한 자제하고, Html 파일로 제공하는 텍스트가 주였 그 시절..!
정적인 페이지는 html로, 동적인 페이지는 c언어 내부에서 html 태그들이 생성되어 웹서버를 통해 사용자에게 전달되었던 신기한 시스템!
언어가 이것저것 섞이다 보니까 공부해야할 것도, 소스 관리도 어려웠다.
그러다 2001년쯤 ASDL같은 고속 인터넷이 보급화되면서 웹 페이지는 더이상 정적이지 않게 되었다.
나를 포함한 전국의 초딩들이 플래시게임에 빠져있던 그 시절!
개발자들에게도 플래시는 핫해서, 웹사이트에 필수요소로 사용되곤 했다.
<2> 2000년대 초반의 웹
세월이 쪼꼼 더 흘러흘러, 다양한 웹 언어와 프레임워크의 발달로 사용자간 커뮤니케이션 또한 점점 활성화되었다. 쌓여가는 커뮤니케이션 내용을 보관하기 위해 데이터베이스의 필요성 또한 점증되었다. 데이터를 관리하는것 뿐만 아니라 무결성을 지켜줄수 있는 DBMS(Data Base Management System)의 역할이 중요해진 것이다!
커뮤니케이션이 강조되는 웹의 역할로 인해 프로그래밍 언어는 C,Perl(빨리 읽기 금지)에서 ASP, JSP, PHP로 넘어갔으며 데이터베이스로는 MySQL, Oracle, SQL Server가 활용되기 시작했다. 그 당시 웹 프로그래밍 언어는 JS와 PHP가 뒤섞여 가독성도 안 좋고, 유지보수가 어려웠다.
<3> AJAX의 등장
2005년에 구글은 AJAX를 이용해 만든 Google Map을 전세계에 공개했다. AJAX(Asynchronous Javascript And XML)는 말 그대로 자바스크립트를 이용해 비동기로 HTTP Request를 서버로 보내는 기술이다. 이 기술이 나오기 전까진 페이지 주소를 입력하거나 다른 웹페이지의 링크를 눌러야 정보가 전송되곤 했는데, 이제는 화면의 변화 없이도 Request가 가능해진 것이다.
웹 언어로 페이지 전체를 생성하는게 아니라, 동적으로 구현할 부분은 별도로 생성하는 웹 언어를 두고 AJAX로 내용을 불러오는 방식을 사용하게 된 것! 이 때부터 웹 표준과 웹 2.0과 같은 말이 유행하기 시작쓰.
이로써 데이터베이스에서 처리해주던 내용은 그대론데, 웹서버에서 처리하던 많은 일들이 클라이언트 단으로 넘어오게 되었다. 그러면서 UX(User eXperience) 또한 중요한 이슈가 되었다.
<4> HTML5 표준화 작업
이전처럼 XML이나 HTML을 전송받는 방식이 아닌 다양한 텍스트 혹은 JSON 규격의 텍스트를 받는 식으로 개발방식이 변경되었다. 이 때부터 웹서비스라고 불리는 API들이 등장하며, JSON을 사용하는 것이 일반화되었다. 제 3자에게 정보를 제공하는 API 뿐만 아니라 개발을 도와주는 개발 프레임워크와 라이브러리들도 무수히 쏟아졌다. 이 때 나온게 바로 jQuery와 Prototype이다. 그러면서 유지보수를 위한 소스 관리의 또한 중요해졌다.
<5> 모바일 시대의 웹 애플리케이션
2008년 이후 다양한 API들과 함께 AJAX가 활성화되고, 웹 표준도 HTML5로 정착해갔다. 기존 애플리케이션은 Win32 등을 실행해서 데스크톱에서 사용했어야 하는데, 이제는 완전 웹페이지에서 굴러가게끔 되었다! 대표적인 예시가 바로 Google Docs이다. 옛날엔 한컴이나 워드파일이 필수템이었는데 요즘은 구글독스가 있으니까 굳이..?ㅎ 세상 참 좋아졌다.
여튼 모바일시대에 들어오면서 프론트와 백앤드 기술이 명확하게 구분되기 시작했다. 웹 서버에서 HTML을 생성하던 부분들이 클라이언트쪽으로 넘어오며 웹서버의 역할은 점점 줄고, 클라이언트가 웹서비스를 위해 수행해야 한 부분들이 많아졌다! 클라이언트가 많은 역할을 수행하게 된 것에는 개발 방법론의 변화도 있지만 클라이언트 단말의 성능과 브라우저의 발전 때문이기도 하다!
자바스크립트가 내부적으로 어떻게 동작하는지 이해하려면 ECMAScript 표준을 이해해야 하기 때문에 앞으로도 열심히 파볼 예정!
스코프와 클로저
(출시된 지 쪼꼼 된 책이어서 변수선언자 var가 많이 등장할 예정)
스코프란?
현재 접근할 수 있는 변수들의 범위를 말한다. 변수가 스코프안에서 선언되었으면 해당 스코프 안에서는 변수에 접근해서 읽거나 쓸 수 있으며, 스코프 밖에서는 해당 변수에 접근할 수 없다. 예시를 한번 보자.
<div id="div0">0번</div>
<div id="div1">1번</div>
<div id="div2">2번</div>
<script>
var i, len=3 // ---> ★
for(i=0; i < 3; i++) {
document.getElementById("div"+i).addEventListener("click", function() { // ---> ♥︎
alert(i)
}, false)
}
</script>
각 div들을 눌러보면 0,1,2가 아닌 3
이라는 창이 나온다.
이 문제는 스코프 문제라고 하기보단 스코프가 생성되고 유지되는 방법때문에 생긴다.
바꿔말하자면 클로저때문에 생긴 문제!
각 div에 대한 클릭이벤트 핸들러 콜백함수는 ♥︎가 있는 줄에서 작성되었다.
이때 콜백함수는 ★표가 있는 줄에서 선언된 변수들에 접근하는 스코프를 생성하게 된다.
이후 div에 클릭이벤트가 발생해서 콜백함수가 호출되는데,
클릭에 설정한 이벤트 핸들러의 콜백함수는 ★표가 있는 줄의 변수들에 계속 접근하는 스코프를 가진다.
for문을 통해 div에 순서대로 클릭 이벤트 핸들러를 부여해도 ★표 줄의 변수인 i가 0부터 3까지 증가한 후, 끝나고 나서도 유지된다.
for문을 돌 때는 별도의 스코프가 생기지 않고, i가 글로벌 스코프에 존재한다.
그러다 addEventListener로 콜백함수를 설정할때 익명함수가 선언되면서 스코프체인이 생성된다.
스코프의 생성
예시 먼저 공개!
for(var i=0; i < 10; i++) {
var total = (total || 0) + i;
var last = 1;
if (total > 16) {
break;
}
}
console.log(typeof total !== "undefined")
console.log(typeof last !== "undefined")
console.log(typeof i !== "undefined")
console.log("total === " + total + ", last === " + last)
다른 프로그래밍 언어에선 세번째줄까지 for문 스코프안에 선언된 변수들을 그 스코프 밖인 콘솔찍는 줄에서 조회하면 에러가 생기는데
자바스크립트에선 모든 값에 접근할 수 있다.
즉, 타 언어와는 달리 일반적인 블록 스코프를 따르지 않는다.
(하지만 let이나 const를 쓴다면 상황은 달라지지 후후..)
자바스크립트에선 function
, catch
, with
를 통해 스코프 체인이 생성된다.
<1> function에서의 스코프 생성
function foo() {
var b = "Can you access me?";
}
console.log(typeof b === "undefined")
위의 콘솔을 찍어보면 true가 나온다. b는 undefined라는 말 for문은 블록 외부에서 내부에 있는 변수들에 접근할 수 있었지만, foo함수 외부에서 내부에 선언된 변수에는 접근할 수 없다.
<2> catch 구문의 스코프 생성
try {
throw new exception("fake")
}
catch (err) {
var test = "can you see me";
console.log(err instanceof ReferenceError === true)
}
console.log(test === "can you see me");
console.log(typeof err === "undefined")
콘솔의 결과는 전부 true가 나온다. catch 내부의 parameter로 넘겨지는 err변수는 catch블록 내부에선 접근 가능하지만, 외부에선 접근할 수 없다. 그렇지만 test변수는 catch변수 외부에서도 접근할 수 있다.
<3> with 구문의 스코프 생성
with ({ inScope : "You can't see me"}) {
var notInScope = "but you can see me";
console.log(inScope === "You can't see me")
}
console.log(typeof inScope === "undefined");
console.log(notInScope === "but you can see me")
with구문은 생전 처음 보는데 catch에서처럼 인자로 받은 변수만 스코프 내부에서 접근가능쓰. (with는 eval과 함께 자바스크립트 개발자들이 쓰지 말아야할 구문이라고 한다ㅋㅋ)
위에서 div를 클릭했을때 3만 나오는 예제 코드가 있었는데, with구문을 쓰면 해결된다고 한다. 하지만 우리에겐 좀 더 쉬운 방법이 있쥐! let과 const를 쓰는 것ㅋㅋ
스코프의 지속성
스코프가 생성되는 방식은 기존언어와 다르지 않지만, 지속되는 건 자바스크립트만의 강점이라고 한다. 이를 통해 새로운 스코프가 생성, 스코프 체인을 참조하는 함수를 변수에 넣고, 다른 함수의 인자로 넘겨주고, 함수의 반환값으로 사용할 수 있게 된다. 다시 말해, 함수가 선언된 곳이 아닌 전혀 다른 곳에서 함수가 호출될 수 있기 때문에 해당 함수가 현재 참조하는 스코프를 지속할 필요가 있는 것이다.
<1> 함수를 이용한 스코프 문제 해결
아까 3번만 나오던 그 예시를 또 가져왔다.
아래와 같이 함수를 분리해서 처리하는 방식이 있고..
<div id="divScope0">0번</div>
<div id="divScope1">1번</div>
<div id="divScope2">2번</div>
<script>
function setDivClick(index) { document.getElementById("divScope"+index).addEventListener(
"click", function() {
alert(index)
}, false)
}
var i, len = 3;
for(i=0; i < len; i++) {
setDivClick(i)
}
</script>
<2> 클로저를 활용한 문제해결
공부하면서 늘 피하고 싶었던 클로저.. 여기서 만나는구나! 코드를 먼저 살펴보자.
<div id="divScope0">0번</div>
<div id="divScope1">1번</div>
<div id="divScope2">2번</div>
<script>
var i, len = 3;
for(i=0; i < len; i++) {
document.getElementById("divScope"+i).addEventListener(
"click",
(function (index) {
return function() {
alert(index);
};
} (i)),
false);
}
</script>
이벤트핸들러의 두번째 인자로 들어가는 함수를 살펴보면
var func = function (index) { /* 생략쓰 */ } // 1번
var returnVal = func(i) // 2번
이렇게 쪼개볼수 있고, 이걸 하나로 합치면
returnVal = (function (index) { /*생략쓰 */ }) // 3번
이렇게!
위와 같은 코드는 IIFE(Immediate Invoke Function Expression), 즉시호출함수라고 한다. 위의 코드를 좀 더 설명해보자면, 1번은 익명함수를 func 변수에 넣고, 2번에선 이 func변수에 i의 값을 넘겨준다. 이를 통해 3번에선 function(index){} 부분과 (i)부분을 하나로 합쳐서 함수를 선언하고 바로 호출한다.
클로저에 대해 자세히 알아보자!
(책에는 with구문에 대한 설명이 있지만, 쓰지 말아야할 문법이라 정리를 안하기로 했다.)
클로저(closure)
특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프는 계속 유지하는데, 그 함수와 스코프를 묶어서 클로저라고 한다.
클로저의 가장 기본적인 환경은 스코프 안에 스코프가 있을때!
즉, function 안에서 function이 선언되었을 때이다.
function outer() {
var count = 0;
var inner = function() {
return ++count;
};
return inner;
}
var increase = outer();
console.log(increase())
console.log(increase())
count변수는 outer함수의 로컬변수이다. outer함수 내부에서만 접근할 수 있다는 말!
이어서 outer함수 내부에 다시 함수를 하나 선언해 inner변수에 할당했다.
inner변수에 할당한 함수는 outer함수의 로컬변수인 count에 접근에 숫자를 증가시키고, 그 값을 반환한다.
그러고 outer함수의 반환값으로 inner변수를 지정하며 함수를 정의한다.
방금 정의한 outer함수를 호출해 그 결과를 글로벌 영역에 있는 increase변수에 할당한다.
이때 할당되는 값은 outer함수의 반환값으로 지정한 inner 변수이다.
따라서 increase변수를 핫무로 호출하면 결과적으로 inner함수가 호출되어 count변수의 값을 1만큼 증가시킨다.
원래 count변수는 outer함수의 로컬변수이므로 일반적으로 외부에서 접근할 수 없다.
마치 객체지향언어에서 말하는 private 변수와 비슷하다는데..
근데 count변수에 접근하는 또 다른 함수 inner를 outer함수의 반환값으로 지정하고, 이를 글로벌 영역에 있는 increase변수에 할당함으로, outer 함수 외부에서도 increase 변수를 통해 count변수에 접근할 수 있게 되었다.
이것이 바로 클로저의 기본개념이다. 길다길어~
아까와 비슷하지만 쪼꼼 다른 코드를 보자.
function outer() {
var count = 0;
return {
increase : function() {
return ++count;
},
decrease : function() {
return --count;
}
};
}
var counter = outer();
console.log(counter.increase())
console.log(counter.increase())
console.log(counter.decrease())
var counter2 = outer()
console.log(counter2.increase())
이번엔 함수를 바로 반환하지 않고 함수를 2개 가진 객체를 생성해서 반환한다.
이때 반환된 객체는 counter변수에 들어가고, 이로써 counter변수의 속성으로 increase와 decrease 함수를 호출할 수 있게 된다.
이 2개의 함수는 outer함수의 내부에서 선언되어 동일한 count변수를 참조한다. 따라고 increase함수로 count변수를 증가시키고, decrease함수로 감소시킬 수 있다.
콘솔을 찍어보면 counter와 counter2는 각각 다른 스코프를 생성해 count변수를 따로 저장한다.
이번엔 즉시실행함수를 이용해보자.
var countFactory = (function () {
var staticCount = 0;
return function() {
var localCount = 0;
return {
increase : function() {
return {
static: ++staticCount,
local: ++localCount
};
},
decrease : function() {
return {
static: --staticCount,
local: --localCount
};
}
};
};
}());
var counter = countFactory(), counter2 = countFactory();
console.log(counter.increase());
console.log(counter.increase());
console.log(counter2.decrease());
console.log(counter.decrease());
콘솔에 대한 결과는 각각 이러하다.
{ static: 1, local: 1 }
{ static: 2, local: 2 }
{ static: 1, local: -1 }
{ static: 0, local: 1 }
클로저 쉽게 이해하기
function sum(base) {
var inClosure = base;
return function (adder) {
return inClosure + adder;
}
}
var fiveAdder = sum(5) // inClosure = 5 and return function
fiveAdder(3); // === inClosure(5) + adder(3) === 8
var threeAdder = sum(3) // inClosure = 3 and return a new function
위 코드에서 함수 안에 다른 함수를 선언하고 외부에서 내부 함수를 사용하는 클로저의 특징을 확인할 수 있다. 외부에서 sum함수를 호출하면 파라미터 base를 통해 넘어온 값은 inClosure변수에 저장된다. 그리고 내부함수에서 inClosure 변수를 참조한다.
위의 템플릿을 토대로 함수가 실행되는 과정을 살펴보면.. sum함수가 먼저 호출되고, sum함수 안의 인자인 base는 5로 넘어와서 inClosure 변수도 5가 된다. 그러고 inClosure변수를 참조하는 내부함수를 반환해 fiveAdder에 저장한다.
클로저의 장점
- 클로저로 한번만 DOM을 탐색
- 노드/템플릿을 JS로 만들어두고 필요할때마다 복제 생성해 활용
- 콜백 변수를 활용해 대상에 따라 동적으로 콜백함수 호출
- 이벤트가 발생한 대상 엘리먼트를 크로스 브라우저에서 가져오기
- HTML5 스펙에 맞는 사용자 정의 data-속성 사용
그 밖에도 다양한 장점들이 있는 클로저. 장점이 있는 만큼 단점도 존재한다.
클로저의 단점
- 메모리 소모
- 스코프 생성과 이후 변수 조회에 따른 퍼포먼스 손해
- 익숙지 않으면 사용하기 어렵다
먼저 메모리를 소모하는 점을 살펴보자. setTimeout 등 이벤트에 대한 콜백함수들이 메모리에 계속 남아있으면 해당 클로저도 같이 메모리에 남아있게 된다. 클로저를 생성할때는 하나의 거대한 클로저를 생성하기 보단 각 변수나 함수들의 생명주기를 분석해 효율적으로 나누면 좋다.
퍼포먼스 측면에선 스코프 체인이 한 두개면 큰 차이는 없다는데 과하게 사용될 경우 퍼포먼스에 영향을 미친다고 한다.
마지막 단점은.. 자바스크립트를 공부하는 개발자라면 클로저를 꼭 알아야 하는데, 또 처음 접하는 사람들도 있을 수 있어서 협업할땐 주석과 문서화를 생활화 해야한다!
클로저는 자바스크립트의 핵심 중 하나이자 특징이니까.. 포기하지 말고 계속 이해하려 하고 연구하고, 써먹어야 겠다.
그런 의미에서 다음번 포스팅은 클로저 원리를 이용한 커링에 대해 다룰 예정!
Reference
- 속깊은 Javascript(양성익 저)