1. 문제제기
자바스크립트에서 this 라는 키워드만큼 헷갈리는 대상도 없다. 구글에서 검색한 어떤 사이트에서는 "this"를 아주 강력한 키워드라고 소개하는데, 칼이 잘 들면 잘 베는 법, 자바스크립트에서 인스턴스가 생성되고 관리되는 과정을 이해하지 않으면 this가 포함된 함수가 이상한 동작을 할 때 문제를 해결하기 힘들다.
우선 다음과 같은 같단한 자바스크립트를 포함하는 html 페이지를 만들자.
<head><script type="text/javascript"> this.name = "global";</script></head>
<body>
<script type="text/javascript">document.writeln(name);</script>
</body>
브라우저를 "새로"열고 페이지를 열어보면 "name" 이라는 글자가 뜬다.(너무 당연 -_-;)
이제 위 코드에서 this.name 정의를 빼고 새로 고침을 해보자.
<header><!-- script 자체를 날려버림 --></header>
<body>
<script type="text/javascript">document.writeln(name);</script>
</body>
브라우저를 새로 고침하면 스크립트를 없앴는데도 여전히 "name" 이 뜬다.. 이거 뭐가 이상하지 않은가? 코드를 없앴는데 왜 계속 내용이 출력되는가?
2. Jack은 Jack이어야 하는데....
Jack을 나타내는 클래스를 정의한다. 다음처럼.
<head>
<script type="text/javascript">
this.name = "global";
Jack = function(){ this.name = "I'm jack"; }
Jack.prototype.getName= function() { return name; }
</script></head>
이제 다음과 같이 인스턴스를 만들고 테스트하면 뭐가 나올까?
var jack = new Jack();
alert("jack.getName() : " + jack.getName() );
분위기상 "I'm jack"이 나오면 안될 것 같다. 해보면 jack.getName() 은 "global" 을 반환하고 경고창에는
"jack.getName() : global"
이 나온다. 왜? 왜? 왜?
getName은 jack의 인스턴스의 함수이고 그 인스턴스에 대해서 getName() 을 호출했으면 "I'm jack"이 나와야 하는거 아닌가?
위에서 작성한 코드에서
Jack.prototype.getName= function() { return name; }
은 사실 아래의 코드와 같은 효과를 가져온다.(동일하다는게 아니라 결과가 그렇다는 것이다.)
Jack.prototype.getName= function() { return window.name; }
갑자기 튀어나온 window 키워드... 위에서 this.name = "global" 코드가 있는데 여기서 this가 바로 현재 브라우저의 window 인스턴스를 참조하고 있다. 다시 말하면 window.name="global"; 와 동일한 효과를 낸다.
window 키워드는 "현재 떠있는 브라우저 프로세스 내에서 사용자가 코딩해넣은 자바스크립트 코드가 실행되는 최상위 환경" 을 나타낸다. 위에서 우리는 "global" 값을 갖는 string 인스턴스 하나, 그리고 그 값을 반환하는 getName 이라는 이름의 function 인스턴스를 하나 만든 것이다.
getName은 분명히 Jack 클래스에 포함된 propotype 인스턴스가 참조하는 함수인데 어째서 "global"을 출력하는가?
위에서 name 프로퍼티의 owner 가 누구인지 명시적으로 써주지 않고 그냥 return name; 이라고 코딩했다. 이럴 경우 name 은 객체의 "프로퍼티"가 아니라 Local Variable, "지역 변수"로 인식되는 것 같다.(내 추측...)
Local 변수인 name의 선언과 정의를 getName 함수 내에서 찾아보지만 찾지 못한다. 그러면 자신의 상위 블록으로 이동해서 name을 찾는다. getName 함수의 상위블록은 Jack이 아닌가? 아니다. Jack.prototype 인스턴스와 getName 함수는 논리적으로 연결되어있을 뿐이다.(prototype 인스턴스가 getName 이라는 이름으로 함수에 대한 참조를 갖고 있을 뿐이다.)
owner를 빼고 "return name;"으로 코딩하면 인스턴스와 함수간의 논리적인 관계가 아니라, 코드 자체가 정의된 물리적관계, 즉 블록간의 관계를 따져서 name의 owner를 찾는다. getName 함수가 Jack.prototype에 의해서 참조되고 있지만 함수 자체는 엄연히 최상위 window 내에서 정의되고 있다. 따라서 getName 으로 참조되는 함수 내에서 name의 정의를 못찾으면 상위 블록인 최상위 window 블록에서 name의 정의를 찾는다.
getName 으로 참조되는 함수를 Jack 클래스 정의 밖에 선언되서 그런게 아닐까???
의심이 든다면 클래스 정의를 다음처럼 바꿔보자.
Jack = function(){
this.name = "I'm jack";
Jack.prototype.getName = function(){ // Jack 안으로 넣었음.
return name;
}
}
그래도 여전히 "global"을 출력한다. 왜 this.name을 건너뛸까? this.name 은 블록 내에 정의된 Local 변수가 아니라 jack 인스턴스가 생성 된 후 만들어진 프로퍼티이다. Jack 생성자의 행동은
Jack jack = {}; // 또는 Jack jack = new Object();
jack.name = "I'm jack":
위 코드와 같다.
그런데 getName 이 참조하는 함수는 local 변수 name 을 원하기 때문에 프로퍼티인 this.name 을 건너뛴다.
원하는 대로 작동하게 하려면 코드를 다음과 같이 바꾼다.
<head>
<script type="text/javascript">
this.name = "global";
Jack = function(){ this.name = "I'm jack"; }
Jack.prototype.getName= function() { return this.name; }// owner를 명확히 써준다.
</script></head>
이제 명확하게 this가 참조하는 객체에서 name 이라는 프로퍼티를 찾는다.
또는 다음과 같이 해도 작동한다.
<head>
<script type="text/javascript">
this.name = "global";
Jack = function()
{
var name = "I'm jack"; // this.name이 아니라...
Jack.prototype.getName= function(){ return name; }
}
</script></head>
위에서는 함수 정의 자체를 Jack 생성자 안으로 옮겼다. 이제 로컬변수 name을 탐색하다가 바로 상위 블록에서 선언, 정의된 name을 찾고 "I'm jack"이 출력된다.
지금까지 설명한 내용을 요약해서 보여주는 예제가 다음과 같다.
자바진영의 고정관념을 가지고 보면 정신나간 코드지만 자바스크립트에서는 말이 되는 예제다.
Jane = function(){
var name = "I'm Tom"; //LOCAL
this.name = "I'm Jane"; // PROPERTY
Jane.prototype.getName = function() {
return "this.name = " + this.name + ", name = " + name;
}
}
<출력결과>
this.name = I'm Jane, name = I'm Tom
3. 그럼 본론으로 들어가서...
여기서 관심사항은 로컬변수의 탐색이 아니라 this 에 있으니 이제 본론으로 넘어가 보자.
자바스크립트는 매우 유연한 언어라서, 이미 존재하는 클래스에 런타임 시에 메소드의 정의를 바꿔쳐서 이미 생성되어 활동하는 인스턴스들의 행동을 일거에 바꿀 수 있다. 그리고 같은 클래스를 통해서 생성된 인스턴스라도 인스턴스 개개인에게 서로 다른 메소드를 참조하게 해서 같은 클래스에서 나온 인스턴스들이 서로 다른 행동을 하게 만들 수도 있다.
자바에서 한 클래스에서 태어난 인스턴스들은 모두 동일한 행동을 하는 것과는 다르다.
이런 유연함의 바탕이 바로 this 키워드이다.
위에서 owner 없는 변수 참조에서 변수의 정의를 찾기 위해서 상위 블록으로 거슬러 올라가는 장면을 설명했다. this 는 이런 변수 탐색의 방향을 바꾸는 용도로 사용된다.
this 에 대한 고정관념을 깨기 위해서 Interview 클래스를 도입해 보겠다.
this.name = "global";
Jack = function(){ this.name = "I'm jack"; }
Jack.prototype.getName = function() { return this.name; }
Interview = function() { this.name = "INTERVIEW"; }
Interview.prototype.doInterview = function(){
return "hello! " + this.name + " Nice to meet u!!";
}
이제 아래처럼 테스트 하면 this.name 의 this의 참조방향에 의해서 출력 결과가 달라진다.
var jack = new Jack();
var interview = new Interview();
document.writeln("jack.getName() : " + jack.getName() + "<br/>");
jack.getName = interview.doInterview; // 함수의 참조 변경.
document.writeln("interview() : " + interview.doInterview() + "<br/>");
document.writeln("jack.getName() : " + jack.getName() + "<br/>");
<출력 결과>
jack.getName() : I'm jack
interview() : hello! INTERVIEW Nice to meet u!!
jack.getName() : hello! I'm jack Nice to meet u!! // 바꿔치기 한 후
doInterview 함수 내에서 name 변수의 owner를 this라는 키워드로 나타내고 있다.
this는 호출된 함수의 실행 중 함수의 owner를 나타내기 위해서 쓰이는데, 함수 interview.doInterview 내부에서 this는 interview 인스턴스를 가리키고 함수 jack.getName() 내부에서 this 는 jack 인스턴스를 나타낸다. doInterview() 함수는 하나이지만, 그 함수의 owner가 누구냐에 따라서 함수의 출력이 달라진다. 이 과정은 this가 명확하게 결정되는 runtime 때까지 알 수가 없다. 실제 코드가 실행되는 시점의 상태에 따라 결정된다. 이때문에 프로세스 실행 중 언제라도 함수의 owner를 바꿔서 owner 인스턴스가 볼 때 함수의 작동이 변경된 것처럼 보인다.
더 재미있는 예제는 다음과 같다. Jack의 친구 Tom 을 불러보자.
this.name = "global";
Jack = function(){ this.name = "I'm jack"; }
Jack.prototype.getName = function() { return this.name; }
Tom = function(){ this.name = "I'm Tom"; }
Tom.prototype.getName = function() { return this.name; }
다음의 코드를 테스트를 하면 결과는 어떻게 나올까?
document.writeln("jack.getName() : " + jack.getName.apply(tom) + "<br/>"); // ..getName().apply 아님
document.writeln("tom.getName() : " + tom.getName.apply(jack) + "<br/>");
apply( obj ) 는 특정 함수에 대해서 owner 객체를 바꿔서 호출할 때 사용한다. jack.getName.apply(tom) 이라고 하면 현재 getName 함수의 owner 인스턴스는 jack 이지만 tom으로 바꿔서 호출한다는 뜻이다. jack의 함수를 tom에 대해서, 그리고 그 반대로 조작함으로써 출력이 다음처럼 나온다.
jack.getName() : I'm Tom
tom.getName() : I'm jack
실행되는 함수의 owner 객체를 바꾸면 메소드 내용이 동적으로 변경되는 모습이다.
이 예제가 말해주듯이, 자바스크립트에서 함수와 메소드의 관계는 자바에서처럼 단단히 결합된 형태는 아니다. 자바스크립트의 객체라는 개념도 내부 구현은 사실 구조체와 함수에 지나지 않는다. 함수가 다른 함수를 참조하는 형태로 자바의 클래스를 나타내고 있다보니 참조를 살짝 바꾸는 것만으로도 인스턴스의 행동이 판이하게 달라질 수 있다. 바로 이점때문에 자바스크립트에 대한 평가가 극과 극을 달리는게 아닌가 싶다.
개인적으로는 this 키워드를 잘 사용하지 않는데, 함수의 owner가 언제든지 변경될 수 있기 때문에 this 가 가리키는 인스턴스의 타입을 알 수가 없다. 더 심각한 건, typeof 연산자로 타입을 출력해도 object 인지, function 인지밖에 알 수 없다.
typeof 로 타입을 확인하면 Interview, Jack, Tom 모두 object 로 나오고 누가 누군지 명확히 구분할 수 없기 때문에 this 를 이용하는 함수 내부에서 undefined, 가 출력되거나 "...... 는 메소드가 아닙니다" 라든가 심할때는 메소드가 존재하지 않아서 예외가 던져지고 스크립트가 종료되더라도 화면에는 아무 변화가 없다.
이렇게 되면 대체 뭐가 문제인지, html 태그의 순서가 잘못된건지 엉뚱한데서 문제를 찾게 된다. 특히! FireFox는 에러가 발생한 위치라도 정확히 짚어주지만 IE는 정말이지... 으.. -_-;;;;
그렇다고 this 를 아예 사용할 수도 없는게, 프로퍼티를 var name = "jane"; 처럼 local 변수로 만들면 new Jane() 으로 수많은 인스턴스를 만들어도 name 은 하나의 값으로 공유된다. 자바에서
private static String name = "jane";
와 같다. -_-;; 결국, 외부에 알려져서는 안되는 값들을 인스턴스들끼리만 공유하려고 할때나 local 변수를 선언해야한다는 거... 또는 객체 하나에 값을 초기화하고 계산한 다음 출력하고, 그다음 값으로 초기화하고 계산한 후 출력하고... 뭐 ㅇ이런 용도로도 쓰면 좋을 듯.
this.name 처럼 선언하는 순간 캡슐화의 이점을 누릴 수가 없기 때문에 사실상 getter/setter 메소드는 필요가 없다.
jane.name = "name";
writeln(jane.name);
으로 read, write 하면 된다. 가독성을 높이려면 getter/setter 넣어서 클래스 길이 왕창 늘여도 뭐랄 사람은 없다. 한꺼번에 값을 계산해서 반환하는 경우는 필요할 수도 있겠다.
'Dev' 카테고리의 다른 글
RequestDispatcher의 경로값 (0) | 2007.12.22 |
---|---|
fairly considerable SWT articles (0) | 2007.12.22 |
[ Persistence API ] More simplified than ever, but still difficult (0) | 2007.12.19 |