요즘 카이 호스트만의 코어 자바8로 Java 8을 공부하고 있다. 작년 초에 개발자로 전향하여 Java 8의 stream 같은 것을 매우 신기해하며 어깨너머로 배워 사용했었는데 알고보니 Java 8이 처음 출시된 것은 2014년 3월 18일이었다.(Java 릴리스 페이지 참고) 지금은 Java 9가 나오려고 몸을 들썩이는 중이다. Java 8을 제대로 공부도 해보지 않았는데 벌써 Java 9가 나온다고 하니 젊은 나이에 뒤처지는 것 같은 기분이 들었다. 이런 까닭에 황급히 서점에서 책을 사서 공부하는 중이다. 원저의 제목 Core Java for the Impatient가 내 상황에 잘 들어맞는 것 같다. 더 늦기 전에 Java 8을 공부하면서 중요하다고 생각하는 것들을 정리해보려고 한다.
인터페이스
객체 지향 프로그래밍에서 인터페이스는 기능의 생김새만 나타낸다. 인터페이스는 어떤 기능에 대한 추상이며, 실제 구현은 그 인터페이스를 구현하는 클래스에게 맡긴다. 해당 인터페이스를 사용하는 입장에서는 실제 클래스가 어떻게 구현되어 있는지 몰라도 인터페이스의 생김새에 따라 함수를 호출하기만 하면 된다. 마치 복잡한 시스템의 UI(유저 인터페이스)와 같다. 구글 검색 엔진은 복잡한 시스템이지만 사용자에게 보여주는 건 질의어를 입력하는 텍스트 박스밖에 없다. 구글 검색 엔진
추상화가 잘되어 있다는 것은 (구글 검색 엔진처럼)객체의 필요한 기능만 드러내고, 복잡하고 굳이 드러내지 않아도 되는 내용들은 숨겼다는 것을 의미한다. 이전에는 이러한 인터페이스의 추상성을 철저히 지켰기 때문에 인터페이스가 어떤 상태(인스턴스 변수)나 구현된 메서드를 갖는 것이 불가능했다. 하지만 Java 8부터 인터페이스가 조금 더 유연하게 바뀌었다.
정적 메서드
기술적으로 Java에서 인터페이스에 정적 메서드를 추가하지 못할 이유는 없었다. 정적 메서드는 어차피 인스턴스와 관계가 없기 때문이다. 다만 정적 메서드도 구현된 메서드라는 점에서 인터페이스의 추상성을 해친다는 것이 문제였다. Java 8에서는 그러한 제약이 없어졌고, 인터페이스에 정적 메서드를 추가할 수 있게 되었다.(사실 이전에도 인터페이스에 정적 필드는 정의할 수 있었기 때문에 정적 메서드가 Java 8에 와서야 추가된 것은 조금 의아하다.) 기존의 제약을 깨고 정적 메서드를 추가한 것은 개발 편의성을 높이려는 시도로 보인다. Java 8 이전의 표준 라이브러리에서는 인터페이스와 관련된 정적 메서드들을 동반 클래스(companion class)에서 제공했다. 대표적인 예로 Collection 인터페이스와 Collections 동반 클래스가 있다.
Java 8에서는 인터페이스에 기본 구현을 정의할 수 있게되었다. 기본 구현이 제공되는 메서드는 구현 클래스에서 구현하지 않아도 컴파일이 가능하다. 기본 메서드는 기존의 인터페이스에 메서드를 추가해야하는 경우에 아주 유용하다. 인터페이스가 변경되는 일이 없도록 프로그램을 잘 작성하는게 좋겠지만 변경이 불가피한 상황이 생길 수도 있다. 인터페이스에 메서드를 추가하면 해당 인터페이스를 구현하는 모든 클래스에서 추가된 메서드를 구현해야하기 때문에 문제가 생긴다. 구현 클래스가 9개라면 인터페이스까지 10개의 파일을 수정해야 한다. 하지만 추가되는 메서드의 구현이 대부분 동일하다면 인터페이스에 기본적인 메서드 구현을 정의하고 유별난 클래스만 수정해주면 된다. 연료 유형을 포함하는 Car 인터페이스를 예로 들어보자.
publicinterfaceCar{
String fuelType();
}
연료 유형에 따른 구현 클래스들도 있다.
publicclassDieselCarimplementsCar{
@Override
public String fuelType(){
return"DIESEL";
}
}
publicclassGasolineCarimplementsCar{
@Override
public String fuelType(){
return"GASOLINE";
}
}
자동 주행 차량에 발빠르게 대응하기 위해서 Car 인터페이스에 자동 주행 차량 여부를 확인할 수 있는 메서드가 추가되어야한다고 생각해보자. Car는 아래와 같이 변경되어야 한다.
publicinterfaceCar{
String fuelType();
booleanautodrive();
}
이 경우에 autodrive() 메서드는 기본 구현을 제공하지 않으므로 DieselCar, GasolineCar에서 구현해줘야 한다. 하지만 기존 차량들은 자율 주행이 안될 것이기 때문에 아래와 같이 기본 구현을 제공할 수 있다.
publicinterfaceCar{
String fuelType();
defaultbooleanautodrive(){
returnfalse;
}
}
autodrive()는 FutureCar와 같은 유별난 클래스에서만 따로 구현해주면 된다.
publicclassFutureCarimplementsCar{
@Override
public String fuelType(){
return"SOLAR";
}
@Override
publicbooleanautodrive(){
returntrue;
}
}
인터페이스의 기본 메서드는 클래스의 계층을 좀 더 단순하게 만들어준다는 장점도 있다. Java 2부터 있어왔던 AbstractCollection은 Collection 구현 클래스들의 공통 기능을 제공한다. Java 8 이전에는 구현 클래스들의 공통 기능들을 묶기 위해 인터페이스와 구현 클래스 사이에 추상 클래스를 정의하는 것이 일반적이었다. 하지만 Java 8에 와서는 더 이상 추상 클래스를 추가할 필요 없이 기본 메서드를 정의할 수 있게 되었다. 이런 변화로 인터페이스와 추상 클래스의 경계가 모호해졌다는 느낌이 들지만 여전히 인스턴스 변수의 유무 차이는 존재한다.
기본 메서드의 충돌 해결하기
Java에서 하나의 클래스는 여러 인터페이스를 구현할 수 있다. Java 8 이전에는 여러 인터페이스가 같은 메서드를 갖더라도 어차피 구현은 클래스에서만 제공했기 때문에 문제가 되지 않았다. 하지만 Java 8에서 인터페이스들이 각각 동일한 메서드의 기본 구현을 제공하고, 클래스에서 충돌이 발생하는 메서드를 명시적으로 오버라이드 하지 않으면 컴파일러가 어떤 기본 메서드를 사용해야할 지 선택할 수 없기 때문에 문제가 발생한다. 책에 나와있는 예시를 살펴보자.
publicinterfacePerson{
String getName();
defaultintgetId(){ return0; }
}
publicinterfaceIdentified{
defaultintgetId(){ return Math.abs(hashCode()); }
}
publicclassEmployeeimplementsPerson, Identified{
...
}
Person과 Identified 인터페이스는 getId() 기본 메서드를 정의하고 있고, Employee 클래스는 두 인터페이스를 구현한다. Employee 클래스에서 getId()를 오버라이드 하지 않으면 Employee inherits unrelated defaults for getId() from types Person and Identified 라는 컴파일 에러가 발생한다.
한 쪽에서 기본 메서드를 구현하지 않으면 문제가 해결될까? Identified를 아래와 같이 기본 메서드 구현을 하지 않도록 바꾸고 컴파일 해보자.
publicinterfaceIdentified{
intgetId();
}
될 것 같지만 메시지가 Employee is not abstract and does not override abstract method getId() in Identified 라고 바뀔 뿐 여전히 컴파일은 되지 않는다. Person과 Identified 인터페이스가 동일한 메서드를 갖고 있긴 하지만 컴파일러 입장에서는 두 개가 정말 같은 목적의 메서드인지 알 길이 없다. 따라서 같은 모양의 메서드지만 두 메서드를 다른 것으로 보고 클래스가 Identified를 구현하지 않았다고 판단한다.(개인적으로는 이것이 일관성이 부족하다고 생각하는데 그 이유는 두 인터페이스 모두 기본 메서드를 구현하지 않는 경우에는 충돌이 일어나지 않기 때문이다.)
가장 좋은 해결 방안은 충돌이 나는 경우를 만들지 않는 것이다. 두 인터페이스가 동일한 메서드를 갖고 있다면 인터페이스 간에 상속 관계가 있지는 않은지, 메서드 이름을 너무 포괄적으로 정한 것은 아닌지 따져보고 충돌 상황을 피하는 게 좋다.
메서드 이름이나 기본 메서드 구현을 포기하지 않고 컴파일 에러를 해결하는 방법은 아래와 같다.
클래스에서 충돌 메서드 구현
가장 단순한 방법으로 클래스에서 충돌 메서드의 구현을 덮어 버리는 것이다. 이 때 클래스에서 구현을 새로 할 수도 있지만 어느 한 쪽의 기본 메서드를 사용할 수도 있다.
publicclassEmployeeimplementsPerson, Identified{
@Override
publicintgetId(){
// Person의 기본 메서드를 사용하고 싶은 경우.
// Identified의 기본 메서드를 사용하려면 Identified.super.getId()를 반환한다.
return Person.super.getId();
}
}
인터페이스간 상속
Person 인터페이스가 Identified를 상속받도록 하면 Employee에 구현 메서드가 없어도 문제를 해결할 수 있다. 하지만 이런 결정을 하기 전에 인터페이스 사이의 관계를 잘 고려해야한다.
자바에서 데이터를 가공하거나 특정 메서드를 수행할 때 새로운 클래스를 만들어서 이를 인스턴스화 해서 쓸건지 아니면 static 으로 쓸건지 고민하게 될 때가 있다. 사실 후자는 객체지향적 관점에서 그리 좋은 선택은 아니다. Vamsi Emani라는 프로그래머가 stack overflow에 남긴 질문 Why are static variables considered evil? 과 가장 많은 지지를 받은 두개의 답변을 번역했다.
Q by V. Emani
I am a Java programmer who is new to the corporate world. Recently I’ve developed an application using Groovy and Java. All through the code I’ve used quite a good number of statics. I was asked by the senior technical lot to cut down on the number of statics used. I’ve googled about the same, and I find that many programmers are fairly against using static variables.
저는 현업에 갓 뛰어든 자바 프로그래머입니다. 근래에 Groovy와 Java를 이용하는 어플리케이션을 개발하고 있습니다. 그동안 자바로 개발할 때 “static” 변수(그리고 static 메소드)를 꽤나 많이 이용하는 습관을 가지고 있었습니다. 근데 제 시니어는 static 의 개수를 줄이라고 말합니다. 그 이유가 궁금해서 구글에 검색을 해봤는데 많은 자바 프로그래머가 static 을 사용하는 것을 꺼린다는 것을 발견했습니다.
I find static variables more convenient to use. And I presume that they are efficient too (please correct me if I am wrong), because if I had to make 10,000 calls to a function within a class, I would be glad to make the method static and use a straightforward class.methodCall() on it instead of cluttering the memory with 10,000 instances of the class, right?
사실 저는 static 을 이용하는 것이 보다 편하고 효율적이라고 생각합니다. (제가 틀린 부분이 있으면 지적해주세요!) 어떤 클래스 내에서 만번의 함수 호출을 하게 된다면 그 함수를 static으로 만들어서 class.methodCall()의 형태로 사용하는 것이 만개의 인스턴스를 생성해서 메모리를 어지럽히는 것보다 훨씬 낫지 않을까요?
Moreover statics reduce the inter-dependencies on the other parts of the code. They can act as perfect state holders. Adding to this I find that statics are widely implemented in some languages like Smalltalk and Scala. So why is this oppression for statics prevalent among programmers (especially in the world of Java)?
PS: please do correct me if my assumptions about statics are wrong.
게다가 static 변수는 코드의 상호의존성(inter-dependency)을 줄여준다고 생각합니다. 상태를 저장하는데 있어서 static 변수들은 아주 유용하게 사용될 수 있습니다. 사실 static은 자바 뿐만 아니라 Smalltalk나 Scala 와 같은 프로그래밍 언어에서도 널리 이용되고 있습니다. 근데 왜 유독 자바 프로그래밍 세계에선 개발자들이 static의 사용을 꺼리는 걸까요?
PS: static 변수에 대한 제 생각에 틀린 부분을 지적해주시면 감사하겠습니다.
A by J. Skeet
Static variables represent global state. That’s hard to reason about and hard to test: if I create a new instance of an object, I can reason about its new state within tests. If I use code which is using static variables, it could be in any state – and anything could be modifying it.
Static 변수는 global state(역주: 전역 상태. 프로그램 혹은 한 모듈 전체의 상태)를 상징합니다. Global state는 추론과 테스트가 매우 까다롭습니다. 가령 코드에서 static 변수를 사용한다고 하면, 이 변수의 상태는 코드 여러 부분에서 영향을 받을 수 있게 되고 따라서 변화를 추적하기가 어려워집니다. 반면에 이를 객체화하여 인스턴스로 생성하면 테스트 중에 그 변수가 어떤 상태를 가지고 있는지 추론하는 것이 보다 간단해집니다.
I could go on for quite a while, but the bigger concept to think about is that the tighter the scope of something, the easier it is to reason about. We’re good at thinking about small things, but it’s hard to reason about the state of a million line system if there’s no modularity. This applies to all sorts of things, by the way – not just static variables.
프로그래머로서 제가 그동안 경험해온 바에 따르면 큰 개념에 대해서 그리기 위해선 일단 이해하고자 하는 범위를 좁혀 쉽게 추론할 수 있어야 합니다. 일반적으로 우리는 작으면 작을수록 그 대상을 쉽게 이해합니다. 다시 말해, 모듈화를 제대로 하지 않는다면 백만 줄 짜리 시스템의 상태에 대해서 추론하는 것은 굉장히 어려운 일입니다. 이것은 단순히 static 변수 뿐만 아니라 모든 프로그래밍 이슈에 대해서 적용할 수 있는 중요한 사실입니다.
A by A. Lockwood & J. Brown
Its not very object oriented: One reason statics might be considered “evil” by some people is they are contrary the object-oriented paradigm. In particular, it violates the principle that data is encapsulated in objects (that can be extended, information hiding, etc). Statics, in the way you are describing using them, are essentially to use them as a global variable to avoid dealing with issues like scope. However, global variables is one of the defining characteristics of procedural or imperative programming paradigm, not a characteristic of “good” object oriented code. This is not to say the procedural paradigm is bad, but I get the impression your supervisor expects you to be writing “good object oriented code” and you’re really wanting to write “good procedural code”.
첫째로, static은 객체 지향적이지 않습니다: 개발자들이 static 변수를 ‘악’으로 규정하는 이유는 static 변수가 객체 지향의 패러다임과 상반되기 때문입니다. 특히나 static 변수는, 각 객체의 데이터들이 캡슐화되어야 한다는 객체지향 프로그래밍의 원칙(역주: 한 객체가 가지고 있는 데이터들은 외부에서 함부로 접근하여 수정할 수 없도록 해야 한다는 원칙)에 위반됩니다. 질문자께서 스스로 설명했듯이 static은 스코프(역주: 한 변수가 유효한 범위)를 고려할 필요가 없는 경우, 즉 전역 변수를 사용할 때에 유용합니다. 이는 절차지향적 프로그래밍 혹은 명령형 프로그래밍(역주: C가 대표적인 절차지향적, 명령형 프로그래밍 언어이며 Java 역시 큰 범위에서 절차지향적, 명령형 프로그래밍 언어라고 할 수 있다.)에서 매우 중요한 개념입니다. 하지만 이 것이 객체지향의 관점에서 좋은 코드라고 얘기하기는 힘듭니다. 절차지향 패러다임이 나쁘다는 것이 아닙니다. 다만, 당신의 시니어는 당신이 “객체지향적으로 좋은 코드”를 짜기를 바라는 것입니다. 반대로 당신은 “절차지향적으로 좋은 코드”를 짜기를 원하는 것이라고 말할 수 있을 것입니다.
There are many gotchyas in Java when you start using statics that are not always immediately obvious. For example, if you have two copies of your program running in the same VM, will they shre the static variable’s value and mess with the state of each other? Or what happens when you extend the class, can you override the static member? Is your VM running out of memory because you have insane numbers of statics and that memory cannot be reclaimed for other needed instance objects?
사실 자바에서 static을 사용하기 시작하면 예측이 어려운 문제가 많아지게 됩니다. 예를 들어서 하나의 가상머신에서 어떤 프로그램 두 카피가 돌고 있다고 가정해봅시다. 만약 이 두 카피가 동일한 static 변수를 공유하게 된다면, 서로의 상태에 영향을 주게 되지 않을까요? 더불어서 오버라이딩을 할 수 없는 static 멤버들 때문에 클래스를 확장하는게 어려워질 것입니다. 뿐만 아니라 지나치게 많은 static 변수를 사용하게 되면 이들로부터 메모리 회수를 할 수 없어서 가상머신이 메모리 부족을 겪게 될 것입니다.
Object Lifetime: Additionally, statics have a lifetime that matches the entire runtime of the program. This means, even once you’re done using your class, the memory from all those static variables cannot be garbage collected. If, for example, instead, you made your variables non-static, and in your main() function you made a single instance of your class, and then asked your class to execute a particular function 10,000 times, once those 10,000 calls were done, and you delete your references to the single instance, all your static variables could be garbage collected and reused.
객체의 라이프타임: 추가로, static 변수는 프로그램이 실행되고 있는 내내 살아있게 됩니다. 즉, 그 클래스를 이용한 작업을 끝내더라도 static 변수가 점유하고 있는 메모리는 garbage collector(역주: 사용하지 않는 메모리를 회수하는 기능)에 의해서 회수되지 않게 됩니다. 반대로, 프로그래머가 그 변수를 인스턴스화 해서 main() 함수 내에서 하나의 인스턴스로 생성하게 되면, 그리고 그 인스턴스에게 만번의 함수 호출을 시키게 되면 그 만번의 함수 호출이 끝난 후 인스턴스는 소멸됩니다. 따라서 메모리를 훨씬 절약할 수 있게 됩니다.
Prevents certain re-use: Also, static methods cannot be used to implement an interface, so static methods can prevent certain object oriented features from being usable.
static은 재사용성이 떨어집니다: 또한, static 메서드는 interface를 구현하는데 사용될 수 없습니다. 즉 static 메서드는 프로그래머가 (재사용성을 높여주는)이러한 자바의 유용한 객체지향적 기능들을 사용하는 것을 방해합니다.
Other Options: If efficiency is your primary concern, there might be other better ways to solve the speed problem than considering only the advantage of invocation being usually faster than creation. Consider whether the transient or volatile modifiers are needed anywhere. To preserve the ability to be inlined, a method could be marked as final instead of static. Method parameters and other variables can be marked final to permit certain compiler optimizations based on assumptions about what can change those variables. An instance object could be reused multiple times rather than creating a new instance each time. There may be complier optimization switches that should be turned on for the app in general. Perhaps, the design should be set up so that the 10,000 runs can be multi-threaded and take advantage of multi-processor cores. If portability isn’t a concern, maybe a native method would get you better speed than your statics do.
static의 대안들: 프로그래머에게 효율(여기서는 속도)이 가장 중요한 문제여서 객체를 생성할 때 마다 생기는 사소한 불이익에도 민감한 상황일 수 있습니다. 이 경우에도 여전히 static 대신에 다른 방법들을 사용하는 것이 가능합니다. 먼저 “transient”나 “volatile”과 같은 제어자(modifier)를 쓸 수 있는지 먼저 고려해봅니다. 실행 속도를 빠르게 해주는 메소드 인라이닝(역주: 실제 메소드를 호출하지 않고 바로 결과값을 돌려주는 방식)을 위해 “final” 메서드를 사용하는 것도 생각해볼 수 있습니다. 또한 메서드 파라미터들과 변수들이 final로 선언되면 컴파일러 단에서의 최적화 작업이 가능해집니다. 인스턴스를 사용할 때마다 새로 생성하는 대신에 여러번 재사용할 수도 있습니다. 아마도 컴파일러 단의 최적화 작업이 switches that should be turned on for the app in general. 어쩌면 멀티스레드를 이용해서 멀티코어 프로세스의 장점을 극대화하기 위해선 이런 디자인이 필수적일 수도 있습니다. 이식성(역주: 다른 플랫폼으로 쉽게 옮길 수 있는 특성)이 중요한 것이 아니라면, native 메서드를 사용해서 static을 사용하는 것보다 더 빠르게 만들 수도 있을 것입니다.
If for some reason you do not want multiple copies of an object, the singleton design pattern, has advantages over static objects, such as thread-safety (presuming your singleton is coded well), permitting lazy-initialization, guaranteeing the object has been properly initialized when it is used, sub-classing, advantages in testing and refactoring your code, not to mention, if at some point you change your mind about only wanting one instance of an object it is MUCH easier to remove the code to prevent duplicate instances than it is to refactor all your static variable code to use instance variables. I’ve had to do that before, its not fun, and you end up having to edit a lot more classes, which increases your risk of introducing new bugs…so much better to set things up “right” the first time, even if it seems like it has its disadvantages. For me, the re-work required should you decide down the road you need multiple copies of something is probably one of most compelling reasons to use statics as infrequently as possible. And thus I would also disagree with your statement that statics reduce inter-dependencies, I think you will end up with code that is more coupled if you have lots of statics that can be directly accessed, rather than an object that “knows how to do something” on itself.
만약 여러개의 인스턴스를 만드는 것을 피하고 싶다면 싱글톤 디자인 패턴을 이용하는 것이 훌륭한 대안이 될 수 있습니다. 싱글톤 디자인은 (싱글톤을 제대로 구현했다는 전제하에) 스레드 안정성을 가지고, lazy-initialization(역주: 객체가 필요할 때마다 만들어 쓰는 기법)을 허용하며, 객체가 사용될 때마다 제대로 초기화 된다는 것을 보장합니다. 뿐만 아니라 서브 클래싱(sub-classing) 기법을 가능하게 하고, 테스트와 리팩토링이 매우 용이합니다. 다음의 상황을 가정해봅시다. 프로그래밍을 하다가 어느 시점에서 지금까지의 설계를 바꿔야겠다는 생각이 들게 되면 두말할 것도 없이 하나의 인스턴스를 수정하는 것이 모든 static 변수들을 리팩토링 하는 것보다 훨씬 편할 것입니다. 사실 static을 사용하다가 refactoring을 해야하는 상황은 매우 흔한 일입니다. 그것은 유쾌하지 않은 일일 뿐 아니라 훨씬 많은 클래스를 수정하게 만들기도 합니다. 이렇게 또다시 클래스들을 수정하다보면 새로운 버그를 만들어낼 소지가 매우 커집니다. 이런 상황을 피하기 위해서 처음에 “제대로”(위에서 언급한 방식들대로) 디자인하여 코딩하는 것이, 그 방식이 몇가지 단점을 가지고 있는 것 처럼 보여도 훨씬 나은 선택입니다. 사실 이런 끔찍한 재수정 작업이 요구될지도 모른다는 소지가 제가 static을 되도록 쓰지 않으려는 가장 큰 이유 중 하나입니다. 정리하자면, 저는 질문자께서 static이 코드의 상호의존성(inter-dependency)을 줄여준다고 말하신 것에 동의할 수 없습니다. 인스턴스화 되어 있는 객체들을 쓰지 않고 static 변수에 직접 접근하는 방식으로 코드를 짜다보면, 결국 작성한 모듈들이 서로 더 많이 엮이는 (바람직하지 않은) 상황에 처하게 될 것입니다.
가자고팀은 백엔드 어플리케이션을 Java로 개발하고 있다. Java로 개발을 하다보면 자주 쓰는 메서드들을 static (클래스 메서드)으로 선언하고자 하는 유혹이 생겨난다. 객체화를 할 필요도 없고 접근이 훨씬 용이하기 때문이다. 하지만 위의 두 개발자의 답변대로 static의 남용은 프로그램의 상태를 추정하기 어렵게 만들고 결과적으로 객체지향적이지 않은 코드를 작성하게 만든다.
물론 static을 ‘evil’이라고 규정하고 있는 이들의 의견에 전적으로 동의하는 것은 아니다. 그러나 필자도 개발 중에 static 을 빈번하게 사용하면 겪게 되는 문제들을 경험해본 적이 있다. ‘좋은 코드’라는 것에 결코 절대적인 기준이 있는 것은 아니지만, 객체지향적 프로그래밍의 원칙들을 되새겨볼때 분명 static의 사용에 심사숙고할 필요가 있어보인다.
Generic 메서드는 자신의 타입매개변수를 가진 메서드이다. generic 타입을 선언하는 것과 비슷하지만 타입매개변수의 스코프는 메서드로 제한된다. 일반 메서드, 정적 메서드 generic 클래스의 생성자는 이 룰을 동일하게 적용된다. Generic methods are methods that introduce their own type parameters. This is similar to declaring a generic type, but the type parameter's scope is limited to the method where it is declared. Static and non-static generic methods are allowed, as well as generic class constructors.
generic 메서드 문법은 return 타입 전에 꺽쇠 사이에 타입매개변수를 표기한다.static generic 메서드를 위해 타입매개변수는 반드시 메서드의 return 타입 이전에 위치해야한다. The syntax for a generic method includes a type parameter, inside angle brackets, and appears before the method's return type. For static generic methods, the type parameter section must appear before the method's return type.
Util 클래스는 두 Pair 객체를 비교하는 generic 메서드를 포함하고 있습니다. The Util class includes a generic method, compare, which compares two Pair objects:
publicclassUtil{
publicstatic <K, V> booleancompare(Pair<K, V> p1, Pair<K, V> p2){
return p1.getKey().equals(p2.getKey()) &&
p1.getValue().equals(p2.getValue());
}
}
publicclassPair<K, V> {
private K key;
private V value;
publicPair(K key, V value){
this.key = key;
this.value = value;
}
publicvoidsetKey(K key){ this.key = key; }
publicvoidsetValue(V value){ this.value = value; }
public K getKey(){ return key; }
public V getValue(){ return value; }
}
이 메서드를(static boolean compare)를 호출한 문장은 다음과 같다. The complete syntax for invoking this method would be:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.<Integer, String>compare(p1, p2);
Util.<Integer, String>compare(p1, p2) 강조한 영역처럼 타입을 명시하였다. 타입인자는 컴파일러가 타입 추론할때 필요한 정보가 된다. The type has been explicitly provided, as shown in bold. Generally, this can be left out and the compiler will infer the type that is needed:
Pair<Integer, String> p1 = new Pair<>(1, "apple");
Pair<Integer, String> p2 = new Pair<>(2, "pear");
boolean same = Util.compare(p1, p2);
위의 코드는 타입 추론을 나타낸것이다. 꺽쇠에 타입 명시하지 않고 generic 메서드가 아닌 일반 메서드를 호출해도 타입 추론이 이루어진다. 이 주제는 뒤에 나오는 타입 추론 섹션에서 더 논의한다. This feature, known as type inference, allows you to invoke a generic method as an ordinary method, without specifying a type between angle brackets. This topic is further discussed in the following section, Type Inference.
* Closure는 자바스크립트에서 수 많은 응용들을 할 수 있는 정말로 중요한 개념이나 자바스크립트라는 언어를 더욱더 빛내줄 수 있는 특징이다. Closure를 모르고 자바스크립트를 개발하는 것은 10년전의 웹 언어 중심의 개발 방법론에 머무르고 있는 것과 같은 것이기 때문에 10년전 웹개발자에서 진정한 자바스크립트 개발자로 나아가기 위한 기본을 이제부터 들여다보자.
: 이전글에서 Closure에 대하여 이해를 할 수 있는 여러 가지 예들을 들여다 봤지만 실제적으로 이해는 약간 힘들었을 것이다. Closure는 자바스크립트에 있어서 C로 치자면 C에서 포인터를 바라보는 관점하고 똑같다. C에서 포인터를 이해하기를 포기하고 돌아서게 되면 진정으로 깊이 있는 C 개발자가 못 되듯이 자바스크립트에서도 Closure를 이해하지 못하면 깊이 있는 자바스크립트 개발자, 또는 웹 개발자가 되지 못하게 되는 것이다. 하지만 중요한 것은 "Closure는 뛰어난 기술이 아니다"라는 것이다. 포인터의 개념을 주소라는 개념으로 받아들이기 시작하면 아주 쉽게 이해하듯이 closure 또한 scope chain에서 하나의 scope를 생성해준다는 개념으로 이해한다면 아주 쉽게 이해가 가능할 것이다. 그렇다면 먼저 scope chain에 대해서 알아봐야할 것이다.
* Scope chain
: Scope chain은 이전 글에서 이미 다뤘던 내용이다. 하지만 이 개념과 closure를 반드시 연결해서 생각해야만 closure를 이해할 수 있다. Scope chain에 대해서 설명을 다시 하겠지만, 이전 글을 다시 훑어보고 와도 괜찮을 것이다.
<div id="divScope0">Click me! DIV 0</div>
<div id="divScope1">Click me! DIV 1</div>
<div id="divScope2">Click me! DIV 2</div>
<script>
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click", function () { // #1
alert("You clicked div #" + index);
}, false);
}
var i, len = 3;
for (i = 0; i < len; i++) {
setDivClick(i);
}
</script>
: 여기서 이전글에서 말했던 closure가 생성되는 규칙을 발견할 수 있을 것이다. function 키워드를 따라가면서 확인해보면 위의 setDivClick 함수가 선언된 부분 안에 #1의 부분에 함수가 하나 선언 되어 addEventListener 함수의 인자로 넘어가고 있다. 바로 function 안에 function을 넣은 것이다. 그리고 이전 글 중에서 [속깊은 자바스크립트 강좌] 자바스크립트의 Scope와 Closure 기초의 글에서 Scope는 function을 기반으로 생성된다는 말을 했다. 그렇다면 function 안에 다른 function을 다시 선언한다는 것은 다른 말로 하나의 scope안에 다른 scope를 선언한다고도 볼 수 있다. 이것이 바로 scope 안에 scope를 만들어서 scope chain을 생성하는 과정이다. 위의 글에 있었던 그림도 다시 한번 보자.
위의 예에서 scope chain이 형성된 그림
: 여기서 setDivClick으로 생성된 scope의 안에 각 addEventListener의 인자로 선언된 함수들의 scope가 하위에서 setDivClick 함수의 scope를 참조하는 것을 알 수 있다. 이렇게 여러 개의 함수 안에 함수가 호출되면 하위 scope를 생성하여 상위 scope의 변수들을 접근할 수 있는 것을 scope chain이라고 하는 것이다. 여기서 중요한 것은 하위 scope에 해당하는 function이 살아있다면 상위의 scope들은 죽지 않고 계속 살아있게 된다는 것이고 이것이 closure의 가장 기본적인 개념이다. 즉, 위의 div0.onclick 이라는 함수가 살아있는 동안 setDivClick을 통해 생성되었던 scope는 계속 살아있게 된다.
* 기본적인 예
: Closure를 이해하기 쉽게 sum 이라는 함수를 선언해보자.
function sum(base) {
var inClosure = base; // #1
return function (adder) { // #2
return inClosure + adder;
};
}; // #3
var fiveAdder = sum(5); // #4: inClosure를 5로 설정하고 #2의 함수 리턴
fiveAdder(3); // === inClosure(5) + adder(3) === 8
var threeAdder = sum(3); // #5: inClosure를 3으로 설정하고 #2의 함수 리턴
: 위의 예에서도 보면 function 안에 function이 선언되고 내부의 function이 리턴 되는 것을 볼 수 있고 외부 함수에서 base 인자가 넘어오면 내부의 함수에서는 inClosure 변수를 설정하게 된다. 이렇게 소스코드를 통해서 scope가 생성되는 것을 한번 살펴보면 아래와 같이 된다.
: 이것은 scope의 뼈대, 또는 template이라고 보면 된다. 중요한 것은 위의 scope가 실제로 생성 된것이 아니라는 점이다. 그냥 만약에 scope가 생성된다면 위의 구조를 가지게 된다는 것을 그림으로 표현해본 것이다. 만약 sum 함수의 내부 함수를 받아서 사용하게 된다면 위의 scope를 따르게 된다. 그럼 위의 뼈대를 토대로 하나씩 실행되는 과정을 한줄씩 살펴보자.
var fiveAdder = sum(5);
: 위의 구문을 통해 function sum이 호출 되고 base는 5로 넘어오고 inClosure 변수도 5로 설정한다. 그리고 inClosure 변수를 참조하는 내부 함수를 리턴하여 fiveAdder에 대입한다. 현재 fiveAdder의 scope 상황은 다음과 같을 것이다.
: 위의 scope 뼈대에서 fiveAdder는 실제로 사용하게 되는 함수 A를 할당 받게 되어 위의 scope 뼈대를 통해 하나의 scope chain을 생성하여 가지고 있게 된다. fiveAdder는 오른쪽의 A 함수를 받아 가지고 있게 된다. 여기서 fiveAdder에서 할당 받는 함수 A는 어디서 온것인가 보면, 바로 #2에서 리턴한 그 함수가 바로 A인 것이다.
: 이제부터 fiveAdder를 통해 함수를 접근하게 되면 위의 scope chain을 따르게 된다. 여기서 하나 짚고 넘어가자면, 그림상으로 왠지 순환 구조를 가지고 있는 듯 하지만 모든 scope는 global scope에서 끝나게 된다. 그리고 global scope에 있는 fiveAdder는 scope chain이 이어진 것이 아니라 오른쪽 함수 A의 레퍼런스, C에서 말하자면 포인터를 가지고 있는 것이고, 나중에 fiveAdder를 실제로 '호출'하게 될 때에 위의 오른쪽 A는 해당하는 scope chain을 사용하게 된다는 것이다. 이 scope chain은 fiveAdder가 또 다른 함수의 인자로 넘어가든 fiveAdder가 메모리에서 사라질때까지 계속 유지하게 된다. 이것이 이전 글에서 말했던 '퍼포먼스의 문제'에 대하여 조금 더 깊게 이해할 수 있을 것이다. 이제 fiveAdder(3)을 호출하게 되면 오른쪽 A의 adder 인자에 3이 들어가게 되고, 내부 함수에서 inClosure + adder를 하게 되면 바로 위의 scope에 있는 inClosure = 5와 인자인 adder = 3을 이용하여 8이라는 값을 리턴하게 된다. 여기까지는 그렇게 특별할 것이 없다. 이제 다음으로 넘어가면,
var threeAdder = sum(3);
: 그렇다면 이번에는 다시 threeAdder를 호출하게 되면 어떻게 될까. 이번에는 상당히 생각이 많아질지도 모른다. 기분상 fiveAdder의 inClosure까지 변환되어 버릴 것 같지만 그렇지 않다.
: 위와 같이 함수 B가 사용하는 하나의 새로운 scope chain이 생성되어 threeAdder에서 사용하도록 된다. 같은 함수를 통해서 받은 리턴 값의 함수가 A와 B 2개로 각각 생성되어 이제 fiveAdder(3)을 하게 되면 8의 결과 값이, threeAdder(3)을 하게 되면 6의 결과 값이 나오게 된다. 이렇게 closure를 통해서 각 함수들은 자기만의 고유의 값을 가지고 scope chain을 유지하면서 그 chain 안에 있는 모든 변수의 값을 유지하게 된다.
* Scope chain의 생성
: 단순히 보면 왜 그때그때 scope chain이 생성될까 이해하기 힘들지도 모르지만 속에 돌아가는 구조를 자세하게 살펴보면 이해를 할 수 있을 것이다. Closure에 대해서 더 깊어지기 전에 위의 현상에 대해서 이해하고 넘어가자. 위의 예에서 '익명 함수'를 이용하고 있다. 익명 함수는 함수의 이름을 지정 안하고 사용하는 경우를 뜻하고 위의 예에서는 다음의 부분이다.
: 이 부분에서 #2는 익명 함수가 선언되어 리턴되는 부분으로 내부에서는 이것은 새로운 Function 객체를 만들어 리턴을 하게 되는 과정과 같다. 다르게 표현하면 내부적으로는 아래와 '비슷한' 프로세스가 일어나게 된다.
return new Function("adder", "return inClosure + adder;");
: 자바스크립트에서 {}는 object literal, []는 array literal이라고 하고 위의 function () {} 는 function literal이라고 하는 것은 내부적으로 각각 {}는 Object 객체, []는 Array 객체, function () {}는 Function 객체를 만들기 때문이다. 이렇게 sum 함수를 호출함으로써 내부에서 #2를 거치게 될 때마다 매번 새로운 함수를 생성하여 리턴하는 것이라고 보면 되고, 이럴 때마다 각 함수의 scope chain은 새롭게 할당되어 저장하게 된다. 따라서 위의 예에서 fiveAdder = sum(5)를 호출 할 때 new Function과 비슷한 과정을 통해 함수 A가 생성되어 리턴되고, threeAdder = sum(3)을 호출 할 때 또 new Function을 통해 함수 B가 생성 된 것이다. 이렇게 보면 매번 sum을 호출할 때마다 새로운 함수와 그 함수의 scope chain이 따로따로 생성된 것을 이해할 수 있을 것이다. 여기서 재밌는 것은 fiveAdder와 threeAdder의 외부 표현식은 같다는 것이다. toString() 함수를 통하여 호출해보면 둘다 아래와 같이 나오게 된다.
: 하지만 fiveAdder !== threeAdder이다. 이렇게 똑같은 모양의 함수들이 매번 새롭게 나오고 있는 것이다. 이렇게 두 함수는 일치한 모양을 가지고 있지만 둘의 동작이 달라지는 것은 바로 숨겨져 있는 closure 때문이다.
- 덧: 위에서 new Function과 '비슷한' 프로세스가 일어난다고 말한 것은 new Function을 이용해서 생성한 함수는 로컬 변수만 이용 가능하지만 function literal로 생성한 함수는 Closure를 생성하여 외부의 scope에 있는 변수들을 접근 가능하다는 점이다. 따라서 closure를 생성할 때에는 new Function을 이용하면 안되고 function () {} 으로 함수를 생성해야한다.
- 덧2: 위의 그림에서 보면 base와 inClosure의 값은 항상 같고 같은 scope에 자리하고 있다. 따라서 inClosure 변수는 사실상 필요없지만 closure 내부의 로컬 변수도 유지 된다는 것을 보여주고자 추가적으로 선언했다.
* Closure를 쓰는 실제 예
: 이전 글에서 closure의 이용 방법에 대하여 몇가지를 이야기하기도 했지만 '어디서' '언제' 사용할지에 대해서는 감을 잡기가 어려울 것이다. closure를 가장 많이 사용하는 것은 이전 글에서 말했던 경우들, 라이브러리에서 private이나 나의 변수를 보호하고 싶을때라던가 self-defining function인 경우, static으로 변수를 이용하고 싶은 경우에도 있지만 가장 일상적으로는 closure를 활용하는 경우는 콜백함수에 추가적인 값들을 넘겨주거나 처음에 초기화 시켰던 값을 계속 유지하고 싶을 때일 것이다. 사실 이렇게 글로 실컷 읽어봤자 위의 fiveAdder 등과 같이 실용적이지도 않은 곳에만 쓰이는 탁상공론에 불과한 개념이라고 느낄 수 있을 것이다. 따라서 실제 상황에서도 사용할 수 있는 간단한 예를 한번 보자.
- 목적: 특정 div에 버튼1에 대한 콜백으로 div를 추가/버튼 2에 대한 콜백으로 img를 계속 추가
<div id="wrapper">
<button data-cb="1">Add div</button>
<button data-cb="2">Add img</button>
<button data-cb="delete">Clear</button>
아래에 추가<br/>
<div id="appendDiv"></div>
</div>
<script>
(function () {
var appendDiv = document.getElementById("appendDiv"); // #1
document.getElementById("wrapper").addEventListener("click", append);
function append(e) {
var target = e.target || e.srcElement || event.srcElement;
var callbackFunction = callback[target.getAttribute("data-cb")];
appendDiv.appendChild(callbackFunction());
};
var callback = {
"1":(function () {
var div = document.createElement("div"); // #2
div.innerHTML = "1번";
return function () {
return div.cloneNode(true); // #3
}
}()),
"2":(function () {
var img = document.createElement("img");
img.src = "http://www.google.co.kr/images/srpr/logo3w.png";
return function () {
return img.cloneNode(true);
}
}()),
"delete":function () {
appendDiv.innerHTML = "";
return document.createTextNode("Cleared");
}
};
}());
</script>
아래에 추가
: 여기서 Closure를 활용한 곳을 보면 크게 2가지로 볼 수 있다. 바로 #1 부분에서 전체 함수들이 공통적으로 접근하고자하는 변수(appendDiv)를 선언하여 한번의 초기화 만으로 이후에 함수들이(여기서는 append(e) 함수) 지속적으로 접근 가능하도록 한 부분과 #2에서 현재 화면에 안 보이는 가상의 노드를 만들어 보관하고 있는 #2 부분이다. 나중에 콜백 함수에서는 append 함수를 호출하여 클릭한 버튼에 따라 변수 callback에 선언되어있는 다른 내부 함수를 호출하게 되고, 이벤트가 발생하게 되면 해당 콜백 함수가 호출되어 #3에서 #2의 가상 노드의 복제 노드를 생성하여 리턴하여 appendDiv에 추가하게 된다. 이렇게 콜백 함수를 동적으로 생성할 때 초기화 되어있는 값들을 유지하는 것이 퍼포먼스상 유리한 경우, 특히 DOM을 생성하거나 탐색하여 가져오는 경우 한번 로드했던 DOM 객체를 보관하고 있는 것이 여러 모로 유리하기 때문에 이렇게 DOM을 적극적으로 활용할 때야말로 Closure를 진정으로 효과적으로 사용할 수 있을 것이다.
: 그렇다고 위의 예제에서 closure는 필수적인 요소가 아니다. 이렇게 closure는 언제든 사용할수도 있고 안 할수도 있지만, 위에서 말했듯 중복된 DOM 탐색이나 DOM 생성을 할 때에 효과적으로 closure를 이용한다면 월등한 퍼포먼스를 가져올 수 있을 것이다. 이렇게 실용에서 closure를 가장 많이 활용할 수 있는 부분을 살펴보면, 다음과 같이 말할 수 있을 것이다.
반복적으로 같은 작업을 할 때 같은 초기화 작업이 지속적으로 필요할 때, 콜백 함수에 동적인 데이터를 넘겨주고 싶을 때 Closure를 사용하자!
- 덧: 이 예제에서는 실제로 활용 가능한 다양한 예들이 같이 포함되어 있다.
closure로 appendDiv를 한 번만 검색하고 조회하여 초기화하고 계속 보관하는 활용 방법
div, img 등 가상 노드를 만들어놓고 필요할 때마다 복제하여 생성할 수 있는 활용 방법
appendDiv에만 이벤트 핸들러를 추가하여 관리할 수 있는 event delegation 활용 방법
이벤트가 발생한 target element를 크로스 브라우져에서 가져올 수 있는 방법
var callback = {...}를 활용하여 대상에 따라 동적으로 콜백 함수를 변화 시키는 활용 방법
HTML5의 스펙에 맞게 사용자 정의 속성을 "data-*" 여기서는 "data-cb"로 설정한 것
만약 callback 함수에 인자를 넣어주게 되면 div를 추가하되, 안의 내용 또한 동적으로 설정할 수 있는 위의 예 응용 방법 등
: 나중에 어느 정도 강좌를 진행하고 나면 이 예제를 다시 가져와서 더 자세하게 세부적으로 들어가서 정말로 실용에서 사용할 자바스크립트 개발 방법론에 대하여 공부해보도록 하자. 이 간단한 예제는 자바스크립트 개발에서 사용할 수 있는 기본적인 틀을 하나 제시하고 있고, 지금이라도 천천히 하나씩 뜯어보면 이해할 수 있는 자바스크립트만의 독특한 개발 방법들이다. 이 방법들을 제대로 이해하고 활용할 줄 안다면 자바스크립트를 더욱더 깊이 있게 다룰 수 있게 될 것이다.
* 정리
Closure는 function 안에 function이 있게 되면 기본적으로 생성된다.
Closure는 scope chain이 생성됐을 때의 변수 값들을 보존하고 기억하게 된다.
함수가 메모리에서 없어질 때까지 따라다니게 된다.
같은 모양의 함수이더라도 다른 closure를 가지고 있을 수 있다.
함수가 다른 곳에서 사용되더라도 처음에 생성되었던 scope chain이 끝가지 따라다니게 된다.
다른 곳에서 사용되는 대표적인 경우: 함수를 리턴하여 사용, 다른 함수의 인자로 넘겨줘서 사용, 콜백으로 사용
: 지금까지 틈틈히 자바스크립트만의 다른 개발 방법에 대해서 계속 간단하게 살펴봐왔었는데, 거의 모든 방법들에 이 closure가 연관되어있다. 이러한 closure는 functional language에서 이전 글들에서 closure가 사용되었던 예들을 다시 한번 살펴보자. 아래의 소스들은 지금까지 공부해왔던 내용 중에 나왔던 소스들이다. 앞 뒤의 소스들은 간단하게 생략한 것들도 있다.
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click",
function () { // #1
alert("You clicked div #" + index);
}, false);
}
for (i = 0; i < len; i++) {
document.getElementById("divScope" + i).addEventListener("click", (function (index) { // #1
return function () { // #2
alert("You clicked div #" + index);
};
}(i)), false); //#3
}
function bind(obj, func) {
return function () {
func.apply(obj, arguments);
};
}
document.getElementById("clickDiv3").addEventListener("click", bind(unikys, unikys.say));
: 위의 각 소스들은 지금까지 closure를 이용해왔던 예들이다. 위의 소스들의 공통점이 무엇인지 잘 살펴보면 closure는 어떻게 생성되는지 쉽게 이해할 수 있을 것이다. 그냥 훑어가며 봤을 때에는 약간 어려울지도 모르겠지만, 첫번째 특징은 closure는 function 안에 function이 선언될 때 생성된다는 것을 알 수 있다. 두번째 특징은 바로 function에 선언된 scope가 아닌 다른 scope에서 호출 될 때이다. 이는 비동기적으로 활용될수도 있고, function을 return 해서 사용할 때에도 적용이 가능한 것이고, 위의 이벤트 핸들러에서 활용하고 있는 것이 가장 대표적인 예이다. 그럼 먼저 closure의 특징을 살펴보자.
* Closure의 특징
: Closure가 나타나는 가장 기본적인 환경은 바로 함수 안에 함수가 다시 선언되어 호출되었을 때이다. 이는 가장 기본적인 예로 inner function과 outer function을 통해서 나타낼수 있다.
function outer () {
var count = 0; // #1
var inner = function () { // #2
return ++count;
};
return inner; // #3
}
var increase = outer(); // #4
increase(); // === 1 #5
increase(); // === 2
: 여기서 짧게 설명을 한다면 #1 count 변수는 outer의 local 변수이고 #2에서 outer의 로컬 함수로 inner 함수가 선언이 된다. 이때에 #2 안에서는 outer 함수의 count 변수가 접근 가능하다. 이러한 상태에서 inner 함수는 #3에서 outer 함수의 결과로 리턴이 된다. 이 때 #4에서 outer 함수가 호출이 되고, #3에서 리턴된 inner가 #4의 increase 변수에 저장이 되고 #5에서 리턴된 inner 함수가 호출이 되는 것이 순서다. 여기서 중요한 것은 바로 inner 함수가 리턴되면서 다른 곳에서 호출이 될 때에도 inner 함수가 선언되었던 당시에 접근 가능했던 변수 count가 계속 접근이 가능하다는 것이다. 그리고 scope의 개념으로 볼 때에 #4, #5 등 outer 함수 외부에서는 outer 함수의 local 변수인 #1의 count에 접근할 방법이 없게 된다. 즉, 자바스크립트에서도 일반적인 객체지향에서 말하는 private 개념이 적용이 가능한 것이다. 이것이 closure의 가장 기본적인 특징이고 개념이 되는 것이다. 조금 다른 예제를 살펴보자. 이번 예제는 라이브러리를 이용한다면 아주 자주 이용할 패턴이다.
function outer () {
var count = 0;
return { // #1
increase: function () {
return ++count;
},
decrease: function () {
return --count;
}
};
}
var counter = outer(); // #2
counter.increase(); // === 1
counter.increase(); // === 2
counter.decrease(); // === 1
var counter2 = outer();
counter.increase(); // === ? #3
: 이번에도 매우 비슷하다. 하지만 이번에는 이전에 함수를 바로 리턴하던것과는 다르게 #1에서 object 리터럴 {}를 이용해서 함수 2개를 묶어서 리턴하게 했다. #2에서 outer()함수를 호출 할 때에는 #1에서 선언한 object가 리턴이 되어 counter에 들어가게 된다. 이러한 경우에도 똑같이 closure가 생성되고, 이번에는 #1의 object 안에 있는 두개의 함수가 동일한 scope를 보게 되어 같은 작업을 할 수 있게 되었다. 그렇다면 outer() 함수를 한번 더 호출하게 되면 어떻게 될까? 개발자 콘솔에서 직접 실행해보면 아래와 같은 결과가 나온다.
: counter와 counter2는 서로 다른 scope를 생성하여 따로따로 저장하게 된다. 그렇다면 리턴하는 모든 함수들이 같은 값을 사용하도록 static하게 만드는 방법이 있을까?
* 즉시 호출 함수 (immediate function)
: 즉시 호출 함수라 하면 자바스크립트가 익숙하지 않은 사람은 가장 어색하고 당황할 함수 호출 방식이다. 하지만 이미 이전 글들에서도 종종 나왔었다. 맨 위에서 2번째 소스의 예가 바로 즉시 호출 함수의 예이다. 이 즉시 호출 함수를 이용하면 모든 함수들이 공통으로 사용하는 변수를 만들수 있게 된다.
var countFactory = (function () { // #1
var staticCount = 0; // #2
return function factory () { // #3
var localCount = 0; // #4
return { // #5
increase: function () {
return {
static: ++staticCount,
local: ++localCount
};
},
decrease: function () {
return {
static: --staticCount,
local: --localCount
};
}
};
};
}());
var counter = countFactory(), counter2 = countFactory();
counter.increase();
counter.increase();
counter2.decrease();
counter.increase();
: 그냥 보면 무언가 복잡해진것 같지만, 아주 쉽다. 그냥 위의 예를 하나의 함수로 더 덧씌우면서 closure 하나를 더 생성한 것이다. 즉, #1에서 closure를 생성하는 즉시 호출 함수를 선언하고, #2에서는 static으로 활용할 변수를 선언하고, #3 에서는 즉히 호출 함수에서 리턴 값으로 사용할 함수, 위의 예제에서 사용했던 함수를 리턴하게 되는 것이다. 즉, #1의 리턴 값은 #3이 되어 countFactory 변수에는 #3의 함수가 들어가게 된다. 나머지 #4와 #5는 위의 예제와 똑같고, 단지 local과 static의 차이를 나타내기 위하여 리턴 값에 그 두가지를 묶어서 리턴하도록 했다. 위의 실행 결과는 아래와 같이 나온다.
: 이렇게 즉시 호출 함수를 선언함으로써 closure를 하나 바로 생성하는 방법은 다양한 곳에서 활용될 수 있고, 이것은 기존의 웹 개발과는 확연하게 다른 자바스크립트만의 새로운 개발 방법론으로 자리 잡고 있으므로 반드시 이해하고 넘어가길 바란다. 이제 눈치가 좀 빠른 사람이라면 이렇게 closure를 사용하는 가장 간단한 방법이 바로 함수를 return하면서 사용하는 것이라는 것도 알게 되었을 것이다.
* 라이브러리에서의 활용
: 위와 같이 함수로 한번 둘러싸는 경우 가장 많이 사용하는 것이 바로 라이브러리일 것이다. 라이브러리일 때 뿐만아니라 사용자의 접근을 제한하고, 변수의 조작을 불가능하게 하기 위해서는 필수로 위와 같은 방법을 사용해야한다. 하지만 이것보다도 더 큰 이유는 바로 다른 라이브러리들과 함께 사용되는 경우 서로간 충돌을 없애기 위해 반드시 해야한다. 전역변수를 사용했다가 다른 라이브러리 가져왔는데 그 라이브러리에서 덮어씌워버린다면 이유도 모르고 멀쩡하던 웹 페이지에서 에러가 발생하게 될 것이다.
: 아주 간단한 예를 들어보면, 모두가 많이 사용하는 var xmlhttp를 전역변수로 사용했는데, 잘 못 만든 라이브러리 하나를 가져왔더니 거기서 XMLHttpRequest를 워낙에 빈번하게 사용하다보니 전역변수로 선언해서 사용하고 있는데, 그게 하필 같은 xmlhttp를 전역변수로 사용하기라도 한다면 이전에 선언되고 호출 되었던 부분들이 덮어씌워져 버릴 것이다. 그래서 라이브러리를 만들때에도, 활용할 때에도 위와 같이 자신의 중요한 변수들은 즉시 호출 함수로 감싸서 보호를 해주는 것이 마땅하고 전역변수로부터의 접근은 네임스페이스를 활용함으로써 그 가능성을 최소화 시키는 것이 필요하다. 때로는 전역변수의 사용이 불가피하다고 느낄때가 있겠지만, 거의 모든 상황에서 전역변수의 사용은 회피할 수 있다. 그 이유는 바로 closure로 인해 function을 인자로 넘겨줄 경우 function이 참조하고 있는 scope째로 왔다갔다 하게 되기 때문에, 이쪽의 scope와 다른 곳에서의 scope를 함수를 전달 시킴으로써 공유하게 만드는 것이다.
* Closure가 발생하는 또 다른 경우
: 위처럼 return을 통해서도 closure가 생성되기도 하지만 이외에도 많은 방법으로 생성할 수 있다. 위의 return으로 생성하는 방법은 자바스크립트에 매우 친숙한 사람이라면 다 해봤겠지만, 일반 프로그래머들도 많이 해봤을 경우가 있다. 하지만 아마 본인도 closure가 생성된다는 것 자체도 모르고 활용할 방법도 이해 못하고 있었을 확률이 높다. 첫번째 예는 이 글 맨 위의 첫 예이다.
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click",
function () { // #1
alert("You clicked div #" + index);
}, false);
}
: 일단 위의 Closure의 특징에서 말한 특징을 찾으면 같은 구조로 함수 안에 함수가 존재하는 것을 발견할 수 있다. 하지만 위의 특징에서와는 다르게 return을 하고 있지는 않다. 아래의 예도 closure가 존재한다는 것을 모르고 자주 사용했을 예이다. 이전에 팁으로 썼던 글 중에서 setInterval에 대한 글 중의 소스이다.
<script>
var pendingInterval = false;
function setPending() {
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
}
function startPending() {
var div = document.getElementById("pending");
if (div.innerHTML.length > 12) {
div.innerHTML = "Pending";
}
div.innerHTML += ".";
}
</script>
<button onclick="javascript:setPending();">Toggle Pending</button>
<div id="pending">Pending</div>
: 위의 소스를 보면 setInterval(startPending)을 하고 있다. 이것은 실제적으로 startPending의 scope를 그대로 옮겨와서 closure를 하나 생성하게 된다. 하지만 전역벽수를 이용하고 있기 때문에 만약 사용자가 pendingInterval = true;를 시켜버리면 라이브러리의 동작을 수정하여 어떠한 일이 일어날지 예측하지 못할지도 모른다. 위와 같이 간단한 예에서는 미치는 영향이 적을지 모르지만 고객 정보를 다루거나 응모 이벤트 같은 곳에서 잘못 짜여진 웹페이지를 사용자가 건드리는건 아주 식은죽 먹기이다. 따라서 위의 라이브러리에서의 closure 활용처럼 오류를 방지하도록 수정해보고 덤으로 한번 퍼포먼스를 향상시켜보자.
<button onclick="javascript:setPending2();">Toggle Pending</button>
<div id="pending2">Pending2</div>
<script>
var setPending2 = (function () {
var pendingInterval = false, div = document.getElementById("pending2"); // #1
function startPending() { // #2
if (div.innerHTML.length > 13) {
div.innerHTML = "Pending2";
}
div.innerHTML += ".";
};
return function () { // #3
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
};
}());
</script>
Pending2
: Closure를 이용하는 아주 간단한 예이고 라이브러리의 기본 형태와 비슷하다. setPending2 변수에는 #3에 선언된 함수가 리턴되면서 설정하게 되고, #1과 #2에 있는 변수와 함수는 closure 내부에서만 접근할 수 있는 private 변수와 함수 같이 된 것이다. 여기서 퍼포먼스를 향상 시켰다는 말은 어디로부터 올 수 있었는지 살펴보면, 바로 매번 startPending 함수가 호출될때마다 div = getElementById()를 하던 것을 한번만 하도록 closure에 변수로 저장해둔 것이다. 이것은 전역변수가 아니라서 전역변수를 싫어하는 개발자들의 마음을 아프게하지도 않는다. 이전까지 전역변수가 난무하던 자바스크립트의 개발 방법이 closure에 대한 이해가 늘어나면서 이러한 식으로 encapsulate하고 자기의 변수를 보호하는 방식의 개발 방법론으로 개선되고 있는 것이다. 여기서 중요한 것은 closure를 이용했기 때문에 전역변수를 이용하지 않고도 위의 setPending2 함수는 인자로 보내든, 다른 라이브러리에서 사용하든 어디서든 똑같은 동작을 하게 되는 것이다. 하지만 위의 구현 방법이 만능은 아니다. 아래와 같은 단점들을 대표적으로 꼽을 수 있다.
즉시 호출 함수는 소스가 로드되면 바로 실행을 하게 되므로 소스의 로딩시간이 길어진다.
소스가 바로 실행되므로 html 소스보다 아래에 있어야한다.
: 이러한 단점들은 물론 극복 가능하거나 다른 방법으로 고민을 해볼 수 있다. 일단 두 번째 단점 때문에 대표적인 해결 방법이 즉시 호출 함수를 window.onload 이벤트에 넣는 방법이 있다. 이렇게 되면 html 소스 등 웹페이지의 로딩이 다 끝나고난 뒤에 함수를 호출하게 되므로 소스가 어디에 있든 상관없게 된다. 이렇게 즉시 호출 함수가 많아짐에 따라 window.onload의 활용은 이전보다 확연하게 많아진 것이다. 첫 번째 단점은 여러 모로 조금 고민을 해봐야한다. 다음 중에서 가장 중요한 것을 고민해볼 수 있을 것이다.
페이지의 첫 로딩시간은 조금 느리지만 사용자의 지속적인 인터렉션 반응 속도의 단축
사용자의 첫 클릭에서의 반응 속도는 느리지만 지속적인 반응 속도 단축
조금 느리지만 사용자의 꾸준하게 동일한 반응 속도
: 위의 3가지 중에서 첫번째는 위의 예처럼 즉시 호출 함수를 이용하는 방법이다. 처음에 로딩하면서 div를 로딩해두는 것이다. 그 다음 3번째는 맨 처음 구현했던 방법이다. 매번 div를 DOM 트리에서 가져오기 때문에 지속적으로 조금은 느린 사용자 반응속도가 일어나게 되는 것이다. 그럼 2번째는 어떠한 경우인지 살펴보자.
* 덧
: 자바스크립트의 퍼포먼스를 저해하는 가장 큰 요소는 'DOM을 탐색하여 접근'하는 것이다. getElementById이든 jquery의 $이든 상관없다. 따라서 위처럼 한번 접근하고 나서 다시 접근을 자주 할 것 같을 때 변수에 저장해두고 접근을 하는 것이 퍼포먼스를 향상 시킬 수 있는 큰 방법이다.
* 자기를 덮어쓰는 함수 (self-defining function)
: 이 방법은 사용자들에게나 라이브러리를 이용하는 개발자들에게는 전혀 나타나지 않고 다른 점을 못느끼겠지만 이 자체를 개발하는 개발자라면 매우 뿌듯함(?)을 느낄 수 있는, 만약에 이전에 이러한 설계를 한번도 보지 못했다면 아주 기가막힌 설계 디자인이다. 간단하게 요약하자면 바로 초기화를 호출 단계에서 하고 자기 자신을 그 초기화된 정보들을 포함하는 closure가 있는 함수로 덮어씌우는 것이다. 위의 예를 다시 한번 사용해보자.
<button onclick="javascript:setPending3();">Toggle Pending</button>
<div id="pending3">Pending3</div>
<script>
var setPending3 = function () {
var pendingInterval = false, div = document.getElementById("pending3"); // #1
function startPending() { // #2
if (div.innerHTML.length > 13) {
div.innerHTML = "Pending";
}
div.innerHTML += ".";
};
setPending3 = function () { // #3
if (!pendingInterval) {
pendingInterval = setInterval(startPending, 500);
} else {
clearInterval(pendingInterval);
pendingInterval = false;
}
};
setPending3();
};
</script>
Pending3
: 이번은 2번째 예와 비슷하지만 다른 점이 있다면 setPending3는 처음에 호출되었던 함수를 그 안에서 다시 덮어씌워서 다른 함수 #3으로 만들어버린다는 것이다. #3에서는 맨 처음의 setPending3 함수의 #1과 #2에서 가지고 있었던 변수와 함수들에 대하여 접근할 수 있는 closure가 생성되어 유지된다. 다른 점이 있다면 2번째는 소스가 로딩됨과 동시에 호출이 되어 초기화 작업이 이루어졌다면, 이번에는 함수 호출이 일어날 때, 즉 사용자가 처음으로 버튼을 클릭해서 처음으로 호출이 될 때 초기화를 하게 된다. 이게 뭐가 좋은지 잘 모를수도 있지만, 매번 호출될 때마다 초기화가 되었는지 if-else를 해볼 필요없이 그냥 함수 자체를 다시 선언해주는 것이라고 생각하면 이후에 호출될 때마다 성능상으로 충분한 메리트가 있다고 생각할 수 있다. 이렇게 사용자가 처음에 클릭했을 때 초기화하는 것은 아주 사소한 차이지만 프로그램이 커졌을 경우나 DOM을 자바스크립트로 대량으로 다루게 될 때에는 UX가 초기화가 로딩 때 일어나느냐 처음으로 눌렀을 때냐 등의 차이에 따라 꽤나 크게 다가올지도 모른다. 즉, 서로 다른 기능의 특징에 따라 서로 다른 초기화 방법을 사용하면 된다. 이것은 각자 생각하는 가치관에 따라 다르기 때문에 대충 한번 간단하게 어떻게 구현하느냐에 따른 기준을 적어보자면 아래와 같다.
사용자가 페이지에 접속하자마자 자주 사용하는 기능, 이 페이지에 들어와서 반드시 한번은 사용하게 되는 기능이라면 로드하면서 초기화
사용자가 페이지에 들어와서 한참 후에 사용하겠지만 한번 쓰고나서 자주(혹은 이따금씩) 이용하게 되는 기능은 처음 호출 때 초기화
사용자가 기능을 건드리지 않고 나갈 가능성이 크고 자주 이용하지도 않는 기능은 초기화 단계 없이 그냥 그때그때 사용
: 이것이 자바스크립트 초기화에 대한 기본 이해가 될 것이다. 그렇다면 이번에는 closure를 응용하는 한가지 예를 살펴보자.
* 오버로딩
: 객체 지향 개발자라면 아주 반가운 단어일 것이다. 자바스크립트에서는 유동적으로 인자의 수를 받아들이기 때문에사실 오버로딩을 지원하지 않는다. 하지만 arguments와 closure를 이용한다면 이러한 오버로딩 개념도 나름 구현할 수 있게 된다. 간단하게 작성해보면 아래와 같다.
function overload(object, functionName, fn, fnMap) {
var previousFunction = object[functionName];
object[functionName] = function () {
if (fn.length === arguments.length) {
if (fnMap && fnMap.length === arguments.length) {
for (var i = 0; i < arguments.length; i++) {
if (fnMap[i] === typeof arguments[i]) {
return previousFunction.apply(this, arguments);
}
}
return fn.apply(this, arguments);
}
return previousFunction.apply(this, arguments);
} else if (typeof previousFunction === "function") {
return previousFunction.apply(this, arguments);
};
};
}
: 복잡한듯 하지만 함수를 호출하게 되면 객체에 저장되어있는 함수를 previousFunction으로 closure에 저장해두고, 인자의 갯수와 인자의 형을 비교해서 모든게 일치하다면 인자로 넘어왔던 fn을 호출하고, 아니라면 이전에 설정되었던 다른 함수 previousFunction을 호출하게 되는 것이다. 간단한 활용 예를 들면 아래와 같다.
var MyFile = {};
overload(MyFile.prototype, init, function () {
console.log("init as empty file");
});
overload(MyFile.prototype, init, function (fileName) {
console.log("init with file name");
}, ['string']);
overload(MyFile.prototype, init, function (file) {
console.log("init with file object");
}, ['object']);
var file = new MyFile();
file.init();
file.init("myfile.txt");
file.init(fileObject);
: 이러한 활용 예를 들 수 있겠다. 위의 예는 아주 간단하게 오버로딩을 구현한 기법이고, 에러 체크라던가 기본 호출 함수 설정 등 구미에 맞게 바꿔서 구현하면 될 것이다.
* closure 단점
: 이렇게 편리한 closure라도 만능은 아니다. 엄연히 단점이 있기 때문에 항상 사용하기 보다는 정말로 필요할 때, 구현에 있어서 급진적으로 개발이 편해질 때 사용하면 좋을 것이다. 이러한 closure의 단점은 크게 2가지이라고 볼 수 있고 부수적인 단점이 한가지 더 있다. 일단 큰 단점 2개는 다음과 같다.
메모리를 소모한다.
Scope 생성에 따른 퍼포먼스 손해가 있다.
: 이들 2가지는 어떻게 극복할수 없는 단점들이다. Closure를 정말로 필요한 곳에 요긴하게 사용하는 수 밖에 없다. 특히, 메모리의 소모는 리턴하거나 timer, 콜백 등으로 등록했던 함수들이 메모리에 계속 남아있게 되면 해당하는 closure도 같이 메모리에 계속 남아있게 되는 것이기 때문에, 지속적으로 루프를 돌면서 closure 생성하는 것은 지양해야할 설계가 될지도 모른다. 최신 버전에서는 해결되었지만, 구 버전의 IE 같은 경우에는 DOM의 콜백함수로 등록을 하고 콜백함수의 해제 없이 바로 DOM을 삭제해버리면 메모리 누수가 생기는 단점도 있었던 점만 봐도 closure의 메모리 누수와 누적에 대한 고민을 해야한다는 것을 깨달을 수 있다. Closure는 또한 하나의 새로운 Scope를 생성하여 내부의 함수에 링크를 시키기 때문에 이에 따른 퍼포먼스 손해도 감수해야한다. 잦은 함수 호출이 퍼포먼스상 안 좋듯 만약 굳이 함수나 closure를 사용하지 않아도 되는 간단한 일이라면 굳이 함수로 분류를 하지 않아도 될 것이다.
: 그렇다면 위의 핵심적인 단점들 이외의 부수적인 단점은 무엇일까? 다른 언어의 개발 경험이 많았던 사람이라면 조금은 느꼈을지도 모른다. 바로 이해하기가 어렵다는 것이다. Closure는 개발자 본인이 사용할 때에는 나비처럼 날고 벌처럼 쏘는 핵심 기능으로 자라나지만 다른 사람들이 보면 이것이 무엇인지, 어디서 closure가 생성되었고 거기에는 어떠한 정보가 있는지 불분명하게 되는 경우가 많기 때문에 협업을 하게 될 때에는 명확한 주석과 문서화가 필요로 있어야할 것이다. 그리고 무엇보다도 다른 언어 개발자들은 closure가 돌아가는 방식을 이해하지 못하는 경우가 많기 때문에 그들에게 설명을 해줘야할 시간을 투자해야 한다는 점과 이 개념을 같은 웹개발자라도 제대로 이해하고 있지 않는 사람이 정말 많다는 것에 놀라 마음의 충격을 받는다는 점이다.
* 단점에도 불구하고..
: 이러한 단점들은 정말 어떻게 다른 방법으로 극복이 불가능한 단점들이다. 하지만 자바스크립트에서 closure를 빼면 그것은 진정한 자바스크립트가 아니다. 정말로 단순한 '스크립트 언어'에 머물던 5년전 closure를 배제한 개발 방식이야말로 자바스크립트에 잠재되어있는 무한한 가능성을 없애는, 정말로 너무나 놀라운 언어를 그냥 '스트립트언어'로, 그냥 보조적인 언어로 만들어 버리는 것이다. 위에서 단점을 쓴 것은 단점이 많아서라던가 치명적이어서가 아니라 장점이 훨씬 더 많지만 적어도 어떠한 단점들이 있는지 알고 사용해야 더욱더 잘 사용할 수 있기 때문이다. 이 closure를 마음껏 쓰다가 다른 언어를 사용하게 되면 closure가 없다는 것이 엄청 아쉬울 때가 많아질 만큼 closure는 자바스크립트의 핵심이자 특징이라고 볼 수 있다.
* 정리
- Closure는 function 안에 function이 있을 때 생성된다.
- Closure는 함수가 정의된 scope 이외의 곳에서 사용될 때 private 저장소처럼 활용 가능하다. 이러한 경우가 발생하는 대표적인 경우는 아래와 같다.
내부의 function을 리턴하여 다른 곳에서 사용 (바로 호출하던지 인자로 넘겨주는 등)
setTimeout, setInterval 등과 같이 비동기적으로 호출 되는 경우
이벤트 콜백 함수로 활용 되는 경우 (addEventListener 라던가, xmlhttprequest.onreadystatechange 등)
* C나 자바를 접하던 사람들이 처음으로 자바스크립트를 접하면 혼란스러워하는 것이 바로 scope와 this의 상이함일 것이다. 처음에 접할 때에는 객체지향 언어에서는 이해할 수 없는 동작들을 하고 있기 때문에 이것이 뭔가 싶다가도 자바스크립트가 이상하다고 스스로 판정을 내리게 된다. 하지만 이것들은 자바스크립트의 원리만 이해하면 아주아주 쉽고, 오히려 객체지향 언어보다 놀라운 유연함에 감탄을 하게 될 것이고, 자바스크립트를 하다가 다시 C나 자바를 하게 되면, '자바스크립트라면 쉽게 해결할 수 있는데..'라며 자바스크립트를 아쉬워하게 될 것이다. 그럼 이번에는 일단 자바스크립트의 가장 '기본'인 scope와 closure에 대해서 알아보자.
: Scope라 하면, 현재 접근 가능한 변수들이 결정되는 방법이다. 영어의 뜻을 가져와서 설명해보면, 현재 자신의 위치에서 볼 수 있는 변수들을 결정하는 방법인 것이다. 자신의 scope 안에 있다면 접근이 가능하여 변수를 읽거나 쓸 수 있는 것이고, scope 밖에라면 해당하는 변수는 접근이 불가능한 것이다. 간단하게 생각한다면 너무나 쉬운 내용이지만 왜 기존의 '능수능란한' 프로그래머들도 쉽게 이해하지 못할 함정에 빠지게 되는 것일까?
: 아래는 정확하게 Scope 때문에 일어난 일은 아니지만, C와 자바의 프로그래머들이 어떻게 해결해야할지 가장 이해하지 못하는 상황이다. <div>가 0,1,2가 있고, 각 div에 클릭 이벤트를 넣어서 0,1,2를 출력하는 아주 간단한 프로그램을 짜고자 한다. div를 쉽게 늘릴 수 있게 해주기 위해 능숙하게 for loop을 이용해서 이벤트를 부여해줘야겠다고 생각한다. 그래서 아래와 같이 짰다.
<div id="div0">Click me! DIV 0</div>
<div id="div1">Click me! DIV 1</div>
<div id="div2">Click me! DIV 2</div>
<script>
var i, len = 3;
for (i = 0; i < len; i++) { //#1
document.getElementById("div" + i).addEventListener("click", function () { //#2
alert("You clicked div #" + i); //#3
}, false);
}
</script>
: 그 구현 결과는 아래와 같다. DIV 0, DIV 1, DIV 2를 눌러보자.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: 위와 같이 능숙하게 짰건만 구현하고나서 위의 DIV들을 각각 눌러보면 이상하게 "You clicked div #3"만 나온다. 왜 이렇게 나오는 것일까? 문제는, scope가 어떻게 결정되느냐가 아니라, scope가 생성되는 방법과 scope가 유지되는 방법인 것이다. click 이벤트에 대하여 콜백 함수를 작성하여 alert를 시켜주는 함수의 scope는 콜백함수로 선언될 때 #2에서 생성되며, 그 scope는 #1의 변수들에 대하여 접근 가능하기 때문에 해당하는 scope를 유지하게 된다. 나중에 div위에 click 이벤트가 발생해서 실제로 #3이 호출 될때 i는 여전히 #1에 대한 scope가 그대로 살아있어서 클릭된 시점의 i 값을 가져오게 된다. 간단하게 그림으로 설명하면 아래와 같다.
: 위의 for루프를 돌 때에는 scope가 생성되지 않고 i는 기본적으로 global scope에 존재하게 되고, addEventListener에 함수를 첨부할 때 익명 함수가 선언이 될 때 scope가 생성되어 참고를 하게 된다. 그리고 2번째, 3번째 루프를 돌면서 div2의 클릭 이벤트의 콜백함수를 설정하고 나면 scope는 아래와 같이 된다.
: 각 div의 click 이벤트에 부착되었던 콜백 함수들을 모두다 같은 scope의 변수 i를 보게 되었고, 3번째 루프를 다 돌고나서 마지막에 i++이 되고나면 최종적으로 위의 함수들은 공통적으로 global scope에 있는 i=3의 i를 보게 되는 것이다. 그리고 나중에 클릭 이벤트가 일어나서 클릭을 하게 된다면 모두다 똑같이 "You clicked div #3"을 출력하게 된다.
: 이러한 현상은 자바스크립트에서 scope가 함수로 인해 생성되고 함수가 호출될 때에도 계속 지속되는 특성에 의해 생긴 문제이다. 자바스크립트에서 이는 자주 발생하는 문제로 이 개념만 쉽게 이해하고 있다면 기본은 끝냈다고 생각해도 된다. 그럼 위의 문제를 해결하기 위해 먼저 scope가 생성되는 방법에 대해 살펴보자.
* scope의 생성
: scope의 생성은 특정 구문이 실행될 때나 객체들이 생성될 때 새롭게 scope가 하나 생성하게 된다. 그 구문들은 다음과 같다.
function
with
catch
: 이들이 scope를 생성하게 되는 방법은 각각 다르지만, 중요한 것은 이런 객체들이 생성될 때에만 scope가 생성되고 {} 의 블럭이 생성된다고해서 scope이 하나 생성되는 것이 아니다. for 루프를 적용해보면 바로 차이를 느낄 수 있을 것이다. 아래의 소스코드는 실생활에서 사용할 일이 없지만 scope를 이해하는데 약간은 도움이 될 것이다.
* 목적: 수를 0~9까지 더하다가 총합이 16를 넘는 숫자를 구하고 싶다.
for(var i = 0; i < 10; i++) {
var total = (total || 0) + i; // #1
var last = i; // #2
if (total > 16) {
break;
}
}
alert(total + " , " + last); // #3
: 위의 소스코드에서 #1은 자바스크립트에서 자주 사용하는 표현에 한정됐지만, #2의 last는 C나 자바에서도 자주 사용하는 표현일 것이다. 특정 객체의 index를 찾는 등을 할 때 자주 이용하는데 for 루프 안에 var last로 선언되어있다. C나 자바에서라면 #3에서 당연히 에러가 나야겠지만, 자바스크립트에서는 잘 돌아간다. 이것은 블럭{}을 사용할 때에는 scope이 생성되지 않는다는 뜻이다. 반면 function의 안에 있는 값들은 접근이 불가능하다.
function foo() {
var b = "access me";
}
typeof b === 'undefined';
: 이렇게 scope가 생기는 것은 다른 언어와 같기 때문에 당연한 것이다. 하지만 with와 catch는 조금 다르다. function은 블럭{} 안에 있는 모든 내용이 새로운 내부 scope에 포함되지만, with와 catch는 괄호() 안에 인자로 받는 변수들만 새로운 내부 scope에 포함되어 이후에 따르는 블럭{}에서만 접근이 가능하다.
try {
throw new exception("fake exception");
} 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');
: 위와 같은 경우 외부에서 test는 일반 블럭처럼 접근이 가능하지만 catch의 인자로 들어온 err에는 접근을 할 수가 없다. with도 마찬가지이다.
with ({test: "You can't see me"}) {
var notScope = "but you can see me";
console.log("Inside: " + (test === "You can't see me"));
}
console.log(typeof test === 'undefined');
console.log(notScope === "but you can see me");
: function과는 또 다른 독특한 scope를 가지고 있다. 그렇다면 이것만 가지고 위의 클릭 이벤트에서 일어나는 오류를 해결할 수 있을까?
: 잠시 다른 이야기지만, 자바스크립트를 오래한 사람들은 머리에 수없이 박히게 들어왔을, 왠만해서는 정말 어쩔 수 없는 경우가 아니라면사용하지 말아야할 2가지 함수가 있다.
eval
with
: 자바스크립트의 활성화에 가장 큰 기여를 한 Douglas Crockford가 말한 명언이 있다.
"eval is evil"
: 이미 eval을 사용하는 것은 JSON.parse() 기능이 나오고난 이후에 퍼포먼스상, 보안상 단점만 있는 기능이 되어버렸다. 그리고 그와 동급으로 with는 처음부터 없는 듯이 사는 것이 좋을 때도 있다고 언급하고 있다. 이미 자바스크립트를 많이 해왔던 사람들 중에서도 with가 무엇을 하는 구문인지 모르는 사람들도 많겠지만 위의 클릭 이벤트 문제에서는 이렇게 천대받던 with도 뭔가 한가지 역할을 찾을 수 있게 된다. 만약 with를 쓴다면 이것이 with문을 거의 유일하게 효율적으로 이용할 수 있는 방법이 아닐까 싶다.
<div id="divWith0">Click me! DIV 0</div>
<div id="divWith1">Click me! DIV 1</div>
<div id="divWith2">Click me! DIV 2</div>
<script>
var i, len = 3;
for (i = 0; i < len; i++) {
with ({num: i}) {
document.getElementById("divWith" + num).addEventListener("click", function () {
alert("You clicked div #" + num);
}, false);
}
}
</script>
: 위의 구현 결과는 아래와 같다.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: with를 하나 추가해줬을 뿐인데 뭐가 달라진 것일까? 이건 단순히 scope가 하나 생겼다고해서 이해되는 현상이 아니고 자바스크립트의 비동기적인 콜백 함수의 특성과 scope의 지속성이 합쳐진 결과이다. 이렇게 with를 이용해서 해결할 수도 있지만 with는 변수 사용에 있어서 혼란을 가져오고 있기 때문에 그래도 사용을 비추천한다. 다른 해결 방법은 아래에서 설명하고 그래도 with로 문제가 해결은 되었기 때문에 일단 위의 소스에서 with가 어떻게 문제를 해결하는지 살펴보자.
: 먼저 with는 괄호() 안에 새로운 scope를 만들게 된다. 그리고 num의 값에 i의 값을 부여하고 click 이벤트 콜백 함수를 선언한다. 이제 click 이벤트 콜백 함수는 with의 num을 보며 고정된 num의 값 0을 보게 되는 것이다. 이어서 다음 루프에서 scope가 생성되는 것을 다시 살펴보면 아래와 같다. 이전에 div0의 click 이벤트 콜백 함수가 보는 scope는 유지되고, 새롭게 with의 scope가 생성되면서 num을 i의 값인 1로 초기화를 시킨다. div1의 click 이벤트의 콜백함수는 이 scope의 num을 이용하게 되는 것이고, 3번째 루프에서도 똑같은 방식으로 div2의 click 이벤트 콜백도 만들어지게 된다. 결국 오른쪽 그림과 같이 scope가 형성되어 div에서 click 이벤트가 일어나 콜백 함수를 호출 할 때에는 맞게 값이 출력하게 된다.
: 위와 같이 scope가 생성되는 것 뿐만 아니라 함께 scope에서 변수값을 지속시키고 유지 시키는 특성이 이러한 문제를 해결할 수 있게 도와준 것이다. 그럼 scope의 지속성에 대해서 조금 더 공부해보자.
* scope의 지속성
: 사실 scope의 생성되는 방식이 기존의 언어와 가장 다른 점은 아니다. 하지만 scope가 지속되는 것은 다른 언어와는 다른 자바스크립트만의 강점 중 하나이다. 이러한 지속성이 자바스크립트에서 필요한 이유는 바로 자바스크립트에서 새로운 scope가 생성되는 '함수'를 변수에 넣을수도 있고, 다른 함수의 인자로 넘겨줄수도 있으며, 함수의 return 값으로도 활용할 수 있기 때문에 필요했던 개념이다. 즉, 지금 함수가 선언된 곳이 아닌 전혀 다른 곳에서도 함수가 호출될 수 있기 때문에, 그 함수의 scope는 지속될 필요가 있었던 것이다. 그럼 간단하게 그 지속성을 이해하기 위해 위의 클릭 이벤트 문제를 또 다른 방식으로 해결해보겠다.
<div id="divScope0">Click me! DIV 0</div>
<div id="divScope1">Click me! DIV 1</div>
<div id="divScope2">Click me! DIV 2</div>
<script>
function setDivClick(index) {
document.getElementById("divScope" + index).addEventListener("click", function () { // #1
alert("You clicked div #" + index);
}, false);
}
var i, len = 3;
for (i = 0; i < len; i++) {
setDivClick(i);
}
</script>
: 이번에는 굳이 자바스크립트 언어가 아니더래도 이해하기 쉬울 것이다. 하지만 비동기적으로 호출되는 자바스크립트의 특성에서는 중요하게 생각해야할 개념이다. 일단 위의 구현 결과를 아래에서 살펴보자. 각 DIV를 누르면 누른 번호가 맞게 출력된다.
Click me! DIV 0
Click me! DIV 1
Click me! DIV 2
: 위의 with와는 똑같은 개념으로 scope가 생성되고 지속되기 때문에 그림은 비슷하게 나온다. 하지만 다른점이 있다면, with는 with의 내부에서 with의 특성을 따라가게 되고 scope가 완전히 분리된것이 아닌 global scope와 반쯤 섞여있는 형태를 취하고 있는 반면, function으로 구현한 경우 정말로 별도의 scope를 생성하게 된다.
: 소스상 with를 쓰는 것이 간편하기 때문에 이렇게 간단한 경우에'만' 사용할 것이고, 그 외에 조금이라도 복잡해지거나 다른 사람들과 협업을 한다면 function으로 분리할 것을 추천한다. 왜냐하면 자바스크립트를 자주 사용하는 사람이더래도 with가 들어가게 되면 그 용법에 대해서 다시 한번 고민을 하고 검색까지 해봐야할지도 모르기 때문에 모두가 이해할 수 있는 쉬운 용법이 있으니 협업에서는 그것을 활용하는 것이 맞는 것이다. 하지만 위와 같이 함수를 따로 뽑아내는것도 귀찮고 일이다. 그럼 이것을 위의 with처럼 간단하게 처리하고 싶을 때에는 closure와 익명 함수의 조합을 이용하면 된다.
<div id="divScope0">Click me! DIV 0</div>
<div id="divScope1">Click me! DIV 1</div>
<div id="divScope2">Click me! DIV 2</div>
<script>
var i, len = 3;
for (i = 0; i < len; i++) {
document.getElementById("divScope" + i).addEventListener("click", (function (index) { // #1
return function () { // #2
alert("You clicked div #" + index);
};
}(i)), false); //#3
}
</script>
: 위의 소스가 이해된다면 자바스크립트를 어느 정도 봐온 사람일 것이다. 일단 with랑 비슷하게 생각하면 되지만, with가 아닌 새로운 function을 통해 새로운 scope를 생성하는 것이다. 이러한 기법은 자바스크립트에서 아주 많이 사용되고 있고, 응용 범위도 아주 넓어 익혀두면 유용한 기법이다. 일단 어떻게 돌아가는 것인지 간단하게 설명을 한다면, #1에 선언된 익명 함수는 인자를 index를 받는것인데, 이 인자의 값은 #3에 있는 (i)의 값이 index로 들어오게 된다. 이는 함수가 변수임을 다시 생각하고 보면 쉽다. 즉 아래와 비슷하게 생각하면 된다.
var func = function (index) { /* 생략 */}; //#4
var returnValue = func(i); //#5
returnValue = (function (index){ /* 생략 */}(i)); //#6
: 위의 #4와 #5를 하나로 합치게 되면 #6이 되는 것이다. 이것을 위의 #1~#3까지 표현한것이라고 보면 된다. 이것에 대한 것은 기본적으로 이해하고 넘어가길 바란다. 자바스크립트 라이브러리의 가장 기본적인 활용 패턴 중 하나이기 때문이고, 이 개념을 이해하고 응용할 줄 아는 것은 자바스크립트 프로그래머의 기본이다.
: 이제 내부에서 #2는 현재 호출된 함수의 return값으로 익명 함수를 하나 또 return한다. 이건 또 무슨 뜻인지 처음에 보면 헷갈리겠지만, 위의 #6번에서 returnValue의 안에 함수가 들어가는 것으로 이해하면 된다. 즉, 익명 함수의 리턴값으로 함수가 반환되고 그 반환된 함수가 addEventListener의 2번째 인자로서 들어가게 되는 것이다. 이 개념 또한 아주 기본적인 개념이니까 이해하고 넘어가길 바란다.
: 이렇게 간단하게 closure를 이용하는 방법이 있는 반면, closure자체는 엄청난 응용이 가능하기 때문에 closure에 대한 내용은 나중에 closure의 활용 방법에 대해서 자세하게 다시 한번 더 다룰 것이다.
* 정리
- scope는 다음의 명령어들을 호출할 때 새로 생성하게 되고, 일반적인 for, switch 등의 블럭{}에 의해서 구분되지 않는다.
function
with
catch
- scope는 비동기 함수가 호출될 때까지 계속해서 지속되어 참고된다. 이를 새로운 scope를 생성함으로써 비동기적으로 호출 될 때의 scope를 조율할 수 있다.
* 덧
: 위에서 언급했지만 여기서 잠깐 다룬 scope와 closure는 자바스크립트에서 가장 기본이 되는 내용이다. 이 내용을 모르고는 자바스크립트를 자바스크립트답게 사용할 수 없기 때문에 위의 간단한 예들이 어떻게 돌아가는 것인지 천천히 이해하고 넘어가는 것이 좋다. 많은 강좌들에서는 문법들을 다루고 closure를 가장 뒤에서 소개하고 있지만, 이 내용은 진정 자바스크립트의 가장 기초가 되는 개념이고 이것을 이해해야 다양한 자바스크립트만의 기법들이 활용될 수 있기 때문에 기본이라고 말한 것이다.
: 그럼 다음에는 this가 결정되는 방법과 function에 대해서 공부해보도록 하자. 자바스크립트를 자바스크립트답게 만드는 것이 바로 이 function이므로 이에 대해서도 가장 먼저, 사실 scope보다도 먼저, 공부를 해야할 필요가 있다.
이전글에서 소개한 function에 대한 글의 연속으로 이번글에서는 function 이 가진 여러가지 특성들에 대하여 알아보려고 한다. 이번 글에서 소개하는 function 의 특성은 Scope, Scope Chain 그리고 arguments 객체 이다. 이들은 Execution Context라는 개념에서 함수객체가 만들어지면서 생성되는 것들로서 이글 이후에 소개될 Execution Context에 대한 이해를 위해 필요로하는 녀석들이다. 그러므로 이번글에서 소개되는 내용을 통해 Scope, Scope Chain, arguments 에 대한 기본적인 이해에 도움이 되었으면 한다.
함수(function)의 특징
함수는 전역변수와 지역변수를 활용할 수 있다.
함수는 자신의 statement 안에서 변수를 선언하여 지역변수로 사용할 수 있다.
이때 선언된 지역변수는 해당 함수안에서만 접근가능한private variable 이 된다. 즉 함수 코드블럭을 벗어나게 되면 해당 변수는 메모리에서 사라지게 된다.
하지만 global 영역에 선언된 전역변수는 자바스크립트 코드 어디에서든 사용할 수 있기에 *함수안에서도 전역변수를 이용할 수 있다 *
var x= 1; // global 영역에 선언된 전역변수functionfoo(){
return x; // 전역변수 x를 return
}console.log(foo());
> 1
console.log(x); // 어디서든 참조가능한 전역변수
> 1
함수에서 전역변수와 지역변수사용함에 있어 재미있는 현상이 하나 있다. 바로 중첩된 이름의 변수 사용이다. 자바스크립트는 global 영역에 선언된 변수와 동일한 이름의 지역변수를 사용할수 있다. 이로 인해 디버깅 과정중에 많은 혼란을 초래하기도 한다.
var x='hello'; // 전역변수 x 에 hello 할당.functionfoo() {var x='world'; // 지역변수 x 에 world할당.return x;
}
console.log(foo());
>world // 함수실행에 따라 지역변수 x에 할당된 world 가 출력된다.
console.log(x);
>hello // 전역변수 x에 할당된 hello 가 출력된다.
전역변수와 지역변수를 동일한 이름으로 사용했을경우 지역변수에서 다른 값을 할당하여도 전역변수에는 아무런 변화가 없다.
즉 전역변수와 지역변수를 동일한 이름으로 사용하여도 각각 다른 변수객체가 만들어진다는 것을 알수 있다. 이 현상에 대해 정확히 이해하기 위해서는 Scope 와 Scope Chain 이란 녀석들에 대해 이해하고 있어야 한다.
함수객체의 Scope & Scope Chain
Scope & Scope Chain
ECMA-262 3 Edition 에서는 함수객체는 [[scope]] 라는 프로퍼티를 가지며 [[scope]]는 함수객체가 생성되는 시점과 관련된 Object Reference 정보를 가지고 있으며 이 Object Reference 정보들을 Scope Chain 이라고 한다고 정의한다. 즉 scope는 함수객체가 접근가능한 Valiable Object의 유효범위이며 이 Valiable Object들의 집함을 Scope Chain 이라고 한다.
함수객체는 생성과정에서 함수구문 내에서 선언된 지역변수들을 Valiable Object 라는 객체에 저장한다. 그리고 전역객체(Global Object)의 Valiable Object 와 연결된다. 전역객체의 Valiable Object는 전역객체 자신이다. 이때 이 Valiable Object들에 대한 연결들을 Scope Chain 으로 관리하고 [[scope]]를 통해 Scope Chain을 참조하여 함수객체가 가지는 유효범위를 설정하게 되는 것이다.
var x = 1;
functionfoo(){
var y = 10;
return a+x;
}console.log(z);
>ReferenceError;
위 예제코드의 foo() 함수객체가 생성될 때의 [[Scope]]와 Scope Chain 에 대한 연결은 아래 다이어그램을 보면 쉽게 이해할수 있다.
변수의 검색은 이 Scope Chain 에서만 탐색 된다. **Scope Chain의 하위에서부터 상위로 등록된 변수가 있는지 찾아가며 가장 처음 탐색되는 scope에 등록된 변수를 이용*한다. 즉 *Scope Chain 에의해 탐색되지 않는 변수는 undefined **이며 그런 이유로 예제의 console.log(z); 는 foo함수가 가진 유효범위에서 찾을 수 없는 변수 z 에 대한 접근을 하기 때문에 ReferenceError 가 되는 것이다. 처음의 의문으로 돌아가서 전역변수 x와 지역변수 x가 동일한 이름으로 사용이 가능한 이유는 이처럼 각각 x에 접근하기 위해 참조하는 Valiable Object가 다르기 때문인 것이다.
3. 변수선언에 사용되는 var 키워드
자바스크립트에서 변수선언은 반드시 var 키워드를 사용하여야 한다는 착각을 하고 있는 입문자들 여럿보았다. 하지만 이는 잘못된 생각이다. var 키워드의 사용은 해당 변수의 scope를 설정하는 역활만을 한다. var 키워드를 사용한 변수선언은 현재 Scope에 등록되며 var 없는 변수 선언은 Global Scopet에 등록되는 것이다. 이런 이유로 함수안에서 var 없는 변수를 사용하면 함수외부에서도 사용가능 하다.
x=1; //전역변수로 등록된다. functionsum() {
y=2; //전역변수로 등록된다.
var z = 3; //var 키워드 사용으로 현재 scope에 등록된다. return x+y+z;
}
console.log(sum());
> 6console.log(x+y);
> 3//x,y 는 전역변수로 등록되기 때문에 정상 수행된다. console.log(x+y+z);
> ReferenceError // z는 함수실행이 종료되면 메모리에서 비워진다.
위 예제를 보면 알수 있듯이 y 변수는 함수내부에 선언되었지만 var 없이 선언 되었기 때문에 전역객체에 등록되는 전역변수가 된다. var 를 사용한 z는 현재 스코프 즉 현재 실행되는 코드 블럭의 주인읜 객체의 Valiable Object 에 등록되기 때문에 함수가 호출되어 실행되고 있는 시점에서의 런타임 주체인 sum 함수의 Valiable Object에 등록되었다가 함수 실행이 종료되면 메모리에서 비워진다.
하지만 이런 코드패턴은 그다지 좋은 방법이 아니다. var 없는 변수는 전역객체에 등록이 됨으로 항시 메모리를 차지하고 있을뿐 아니라 변수에 값이 할당되는 것은 런타임에서 일어나는 일이기에 해당 변수는 함수객체가 생성되기 이전 즉 함수호출되기 이전에는 global scope 에 등록되지 않는다.
또 한가지 var 없는 변수선언을 지양하는 이유는 복잡하고 길어진 코드내에서 var 없이 사용된 변수는 어디에서 어떻게 바뀌는지 추적하기 힘들기 때문이기도 하다. var 없이 선언된 변수는 함수내에서 수정된 값이 전역 scope에 직접적으로 영향을 끼치게 됨으로 본인도 모르는 사이 엄청난 버그를 양산해 낼수 있기 때문이다.
함수는 0개 이상의 매개변수(parameter)를 가질 수 있다.
매개변수(Parameter) 란?
매개변수란 함수호출시 함수객체로 넘겨질 인자값들을 담기 위해 함수명세에서 선언된 변수들을 뜻한다.
함수는 내부에서 선언된 지역변수와 global 영역에 선언된 전역변수를 사용하는 방법말고 변수를 활용하는 한가지 방법이 더 있다. 바로 함수호출시 인자로 전달받은 값을 저장할 변수를 미리 함수명세에 선언해 두는 것이다.
이런 매개변수는 ,(comma)를 기준으로 기분하여 선언하면 한개 이상의 매개변수도 사용할 수 있으며 해당 함수의 scope에 등록되어 함수내부 어디에서든 별다른 선언없이 함수호출시 넘어온 인자값을 할당받아 바로 사용할 수 있다.
위 예제에서 알수 있듯이 명세에 선언된 매개변수보다 많은 매개변수가 전달될 경우 넘어온 순서대로 매개변수로 할당하고 남은 인자값은 버려진다. 재미있는 사실은 넘어온 값이 매개변수보다 적을 경우에 실행 된다는 것이다. 흔히 함수호출시 선언된 매개변수에 대한 인자를 넘기지 않아 생기는 undefined 를 syntax error 라고 생각한다.
하지만 이는 함수객체 생성과정에서 매개변수로 선언된 변수를 Valiable Object에 추가하면서 기본적으로 undefined 를 할당하며 생긴 결과이지 syntax error 가 아니다.
이렇게 함수호출시 전달된 인자값들은 함수객체가 가진 arguments 객체에 저장된다.
arguments 객체
arguments 객체
자바스크립트의 모든 함수객체 scope는 arguments 라는 프로퍼티를 가지고 있다. 이 arguments는 Function에 넘겨진 모든 인자에 대한 정보가 담겨 있는 arguments Object 라는 특별한 객체를 가리키고 있다.
정의에서 말했듯이 함수객체의 Valiable Object의 프로퍼티로 추가되는 arguments는 arguments Object를 가리킨다. 이 arguments Object 는 넘어온 인자의 순서에 따른 인덱스를 가진 유사배열객체이다. 유사배열객체라는 것은 매우 중요한 특성이다.
arguments Object에 저장된 인자값들에 대한 접근은 배열과 동일한 [] 를 사용하지만 **arguments Object는 Array Prototype Object를 상속하는 객체가 아니다.**그러므로 arguments Object 객체에는 push(), pop(), slice()등이 메소드를 가 존재하지 않는다.
functionsum(x,y,z) {
var total =arguments[0] +arguments[1]+arguments[2];
return total;
}
console.log(sum(1,2,3));
> 6
위 코드의 scope에 대한 다이어그램을 보도록 하자
다이어그램에서 보면 알수 있듯 arguments Object는 넘어온 인자값을 저장하며** .length와 .callee 라는 프로퍼티를 가지고 있다.** .callee 프로퍼티는 현재 실행되고 있는 function 을 가리키는 프로퍼티 이다. sum() 함수객체의 .length 프로퍼티와 arguments 객체가 가진 .length 프로퍼티는 서로 다른 의미를 갖는다. 함수객체의 .length 는 함수명세에 선언된 매개변수의 수를 arguments 의 .length는 함수호출시 넘어온 인자값의 수를 나타내는 프로퍼티들이다. 함수객체와 arguments 객체가 가진 .length 프로퍼티를 이용하면 함수명세에 선언된 매개변수와 동일한 수의 인자값이 넘어왔는지 체크할수 있다.
이번글에서 알아본 내용들은 후에 있을 Closure와 Execution Context 에 대한 설명에서 나오는 용어들의 풀이정도라고 생각하면 될것이다.
즉 이번 글에서 해당 용어들에 대한 정의를 제대로 이해하지 못한다면 앞으로 연재될 글을 제대로 이해하지 못할수도 있다. 그럼 언제나 똑같은 멘트이지만 이글을 통해서도 해당 내용들에 대하여 자세히 이해할 수 없다면 당연히 다른 레퍼런스를 참조해서 이들에 대한 정확한 이해를 해야할것이다.
이는 비단 다음연재될 글들뿐만이 아닌 자바스크립트 Core 에 대한 이해에 필수적인 내용이기 때문에 반드시 이해하고 있길 바란다.
프로그래밍 언어를 처음 배울때 가장 먼저 다루는 부분이 바로 그 언어의 데이터타입 이다. 대부분의 기초 프로그래밍 서적들도 첫장에서 데이터 타입에 대해 설명을 하고 있으며 데이터 타입을 숙지 하지 않고서 해당 언어를 다룬다는 것은 "스펠링을 모르면서 작문을 하겠다는 것과 같다" 라고 생각한다. 그런 이유로 자바스크립트 기초의 가장 첫번째 포스틩이 될 이 글에서는 자바스크립트에서 사용되는 데이터 타입에는 어떤 것들이 있으며 그 특징은 무엇인지 다루어 보고자 한다.
자바스크립트에서 사용되는 데이터 타입
자바스크립트에는 Number, String, Boolean, Function, Object, Null, undefined,Array 등의 데이터 타입이 존재한다. 하지만 Array,Function,Date,RegExp 와 같은 데이터는 엄밀히 따지면 Object이다. 자바스크립트를 객제기반 언어라고 하는 이유이기도 하다. 실직적으로 자바스크립트에서 사용되는 대부분의 데이터 타입은 객체로 존재하며 그에 따른 사용또한 객체기반이 될수 밖에 없다. 그래서 자바스크립트 기술 문서들은 다음과 같이 데이터 타입을 분류 한다.
수 (Number)
문자열 (String)
부울,불린 (Boolean)
객체 (Object)
함수 (Function)
배열 (Array)
날짜 (Date)
정규식 (RegExp)
널 (Null)
정의되지않음 (Undefined)
Number
숫자를 표현하거나 산술 연산을 하는데 사용되는 데이터 타입이다. 기본적으로 +, -, *, / 등의 산술연산이 가능하며 Math 라는 내장객체를 이용하여 수학함수를 이용한 결과를 얻을 수도 있다. 명세에 따르면 자바스크립트의 Number는 "64비트 형식 IEEE 754 값" 으로 정의 된다. 이 때문에 자바스크립트에서 간혹 의도하지 않은 어이없는 결과로 인하여 개발자를 열받게 하기도 한다
console.log(0.1+0.2);
> 0.30000000000000004// ????????? 이런 미친 결과가 나오니 주의해야 한다.
Number Casting :: parseInt(), parseFloat()
자바스크립트는 변수선언시 데이터 타입을 명시하지 않는다. 그래서 숫자형 데이터가 아닌 문자열 데이터로 숫자를 표현하는 일도 많다. 그런 이유로 자바스크립트 코딩을 하다보면 데이터형 변환(casting)을 해야하는 경우가 종종 발생한다. 자바스크립트의 캐스팅을 이용한 데이터형 변환에 자주 사용되는 내장객체 중 parseInt()를 이용하면 문자열을 정수로, parseFloat()를 사용하면 실수로 치환 할수 있다.
console.log(parseInt("010", 10));
> 10
parseInt() 의 두번째 인자는 치환하고자하는 문자열이 몇진수의 숫자를 표현한 문자열인가를 나타낸다 . 위 예제는 "010" 을 10진수 표현의 문자열이라는 의미다. 이와 같은 원리로 두번째 인자를 2로 변경 하여 2진수를 나타낸 문자열이라고 한다면 그 결과는 위 예제와 다른 값이 출력된다.
console.log(parseInt("010", 2));
> 3// 010 을 2진수로 판단하여 10진수 정수로 변환한 값을 출력해준다.
재미있는 사실 하나는 위 예제들에서 두번째 인자를 전달하지 않으면 원하는 결과와 전혀 다른 값이 출력된다.
console.log(parseInt("010"));
> 8// ??? 뜬금없이 8 이 출력된다.
이런 어이없는 결과가 나오는 이유는 간단하다. parseInt() 는 두번째 인자인 진법을 지정하지 않으면 0x로 시작하면 16진수, 0으로 시작하면 8진수로 인식하여 그 결과를 출력하기 때문에 "010"을 8진수로 인식하여 8이라는 결과를 출력한 것이다. 그러니 정확한 결과값을 원한다면 두번째 인자를 반드시 전달해주는 것이 좋다. 또한가지 주의 해야 할 것은 parseInt()는 소수점을 과감하게 버림한다. 그러니 소수점을 표현하고자 한다면 parseFloat()를 사용하여야 한다.
parseFloat()를 사용하면 parseInt()에서 나왔던 두번째 인자 미전달로 인한 황당한 결과의 초래도 막을 수 있다.
위에서 설명한 문자열에서 숫자형으로 데이터형변환시 대상이 되는 문자열이 숫자를 표현한 문자열표현이 아닌경우 자바스크립트는 Not a Number의 약자인 NaN 을 리턴 하며 말그대로 해당 하는 값이 숫자가 아니란 뜻이다. 이 NaN은 자바스크립트를 다루면서 반드시 인지하고 있어야 하는 결과 중 하나 이기도 하다.
console.log(parseInt("hello world", 10));
> NaN
자바스크립트에서 해당 데이터가 NaN 인지 검사해주는 isNaN() 이라는 내장 객체가 존재하는데 이 isNaN() 객체는 상당히 많은 활용도를 가지는 내장객체이다. isNan() 은 NaN 인지 여부를 검사하는 함수임으로 NaN 일때 true 를 반환한다.
Number의 무한대의 표현식 :: Infinity , -Infinity 과 isFinite()
자바스크립트의 특별한 표현식중 하나가 바로 이 무한대를 나타내는 Infinity 이다. 0보다 큰 양의 무한대는 Infinity, 0보다 적은 음의 무한대는 -Infinity로 표현 되는데 이는 산술연산의 결과 혹은 데이터 값이 무한대값을 가지는 경우 자바스크립트가 출력하는 결과값이다.
String은 문자열을 표현하는데 사용되는 데이터 타입이다. 자바스크립트의 문자열은 16비트 유니코드 문자들의 연결구조 이기도 하다. 즉 문자열이라 함은 문자 하나하나가 연결되어 하나의 표현을 이루는 데이터를 말하는 것이다.
String property :: length
이글 초반에 언급했듯이 자바스크립트의 대부분의 데이터타입은 객체로 존재하고 있다. 문자열의 typeof는 당연히 String 이다. 하지만 문자열또한 객체로서 활용가능한 property와 method를 가지고 있는 특이한 형태의 데이터 타입이다. 그러한 이유로 php 에서 문자열의 길이를 구하기위한 strlen() 함수와 같이 별도의 내장함수를 사용하지 않고도 문자열이 가지고 있는 property인 length 만으로도 그 길이를 알수 있다.
문자열은 객체에 의존적으로서 객체의 수행을 담당한다는 메소드(함수와 메소드의 차이는 객체를 다룰때 자세히 정의 하겠다.) 를 가지고 있다. 이들 메소드들은 문자열을 여러가지 형태로 변환하거나 문자열 정의에서 밝힌 문자 하나하나의 연결중 특정 위치의 값을 가져오는데 활용할 수 있다.
console.log("hello world".charAt(0));
> h // charAt() 를 활용하여 문자열의 특정 자리에 위치한 문자를 반환.````
console.log("hello world".replace("hello","goodbye"));
> goodbye world // replace() 를 활용하여 문자열 치환을 수행.
console.log("hello world".toUpperCase());
> HELLO WORLD //toUpperCase()를 이용하여 문자열을 대문자로 치환.
console.log("1+2+3+4+5".split("+"));
> ["1","2","3","4","5"] // split() 를 활용하여 "+" 를 기준으로 문자열을 나누어 배열로 반환한다.
Null (값 없음) 과 undefined
자바스크립트는 값이 없음을 나타내는 null 과 초기화(선언) 되지 않았거나 값이 할당되지 않았음을 나타내는 undefined라는 특별한 데이터 타입이 존재한다. 자바스크립트를 활용함에 있어서 null 과 undefined의 차이를 정확히 이해하지 않는다면 훗날 큰 곤욕을 치를 지도 모른다. **null 은 자바스크립트 개발자가 의도적으로 비어있는 값을 부여한 것이고 undefined 는 **애당초 어떤한 값도 할당되지 않은 것이다. 자바스크립트는 변수의 선언과 초기화를 동시에 하지 않아도 된다. 이는 다른 언어에서도 마찬가지로 활용되지만 자바스크립트에서는 특이하게 선언만 하고 초기화 되지 않은 변수는 초기화되지 않았거나 값이 할당되지 않았음을 표현하는 undefined 라는 값을 할당받는다.
var a; // 선언만 되고 초기화 되지 않은 변수;
console.log(a);
> undefined// 초기화가 이루어지지 않았음으로 java-script 엔진이 undefined를 강제적로 할당한다.
하지만 유념해야하는 것이 있다. 위 예제의 결과는 undefined 지만 아래의 예제를 보면 null과 undefined가 굉장히 햇갈리게 된다.
var a;
console.log(typeof a);
> undefined// 초기화가 이루어지지 않았음으로 당연히 undefined
console.log(a==undefined);
> true// 당연히 a==undefined 는 true를 반환 한다.
console.log(a==null);
> true//typeof 는 undefined 이지만 null 인지 검증하면 true를 반환 한다.
console.log(null==undefined);
> true//null==undefined 검증은 true를 반환한다.
위 예제를 보면 null과 undefined는 그닥 별반 차이가 없어 보인다. 검증에 따른 결과가 둘다 true 이기 때문이다. 하지만 선언조차 하지 않은 변수를 활용한다면 이야기가 틀려진다.
console.log(typeof a);
> undefined// 선언되지 않은 변수를 사용함에 따라 자바스크립트 엔진이 강제적으로 undefined를 할당한다.
console.log(a==undefined);
> // error. 선언되지 않은 변수이기에 undefined의 데이터타입이지만 사용할 수는 없는 문법 오류이다.
그럼 정확하게 null과 undefined를 구별하기 위해서는 어떻게 해야하느냐? 간단하다 **null은 변수가 참조하는 객체가 없음(null)을 나타내고, **undefined는 그 변수가 참조하는 객체를 아직 지정하지 않았음(not initialized)을 뜻한다. 따라서 undefined 값을 가지는 변수는 할당을 통해 값을 가지며 이 값(객체)을 해제할때 null 이 되는 것이다.
var a;
console.log(a);
> undefined // 아직 값이 할당되지 않은 undefined
a = "hello world";
console.log(a);
> hello world // a는 "hello world"의 문자열 객체를 할당 받았음으로 null,undefined가 아닌 hello world 이다.
a=null;
console.log(a);
> null // a가 참조하는 hello world 라는 문자열 객체를 해제하였기에 undefined가 아닌 null 이다.
Boolean (부울,불린)
자바스크립트도 부울 혹은 불린으로 불리우는 이 데이터 타입은 true,false값을 가지는 논리 데이터 타입이다 . 자바스크립트뿐만 아니라 대다수의 프로그래밍 언어에서 가장 많이 사용되는 이 Boolean 데이터 타입은 Boolean() 함수를 이용하여 검증을 수행할수 있다.
흔히 가장 많이 활용되는 if(a) 라는 형태의 제어문은 if(Boolean(a)) 의 약식 표현인 것이다.
갈무리
이번 포스팅에서는 자바스크립트에서 사용되는 데이터 타입에 대해 다루었다. 하지만 글을 차분히 잘 읽은 사람이라면 이미 깨닿고 있을 Object에 대해서는 이번장에서 다루지 않았다. 그 이유는 Object에 대해 논하려면 이글의 스크롤바는 밑도 끝도 없는 해저 깊숙히 박혀 있어야 할듯 하기에 데이터 타입에 대한 글에서 독립시켜 별도로 다룰 예정이기 때문이다.
어쨋든간에 이번장에서는 자바스크립트를 다루면서 반듯이 이해하고 있어야할 기본중에 기본인 데이터 타입에 대해 다루었으며 이런 기초들 하나하나가 곧 front-end 개발자로서 성장해 나가는 밑거름이며 앞으로 자바스크립트를 다루면서 애매한 상황에서 햇갈리지 않고 자신이 원하는 프로그램코드를 작성하는데 큰 도움이 될것이다. 그럼 이번장은 여기서 마무리하고 다음 포스팅이 될 Object에 대해 정리는 훼이크고 사실 지금 사무실에서 혼자 뻘짓중이기때문에 밤이 늦은 관계로 이젠 슬슬 집에 들어가야겠다.