<한쪽에서 보이는 취조실 유리, 스코프의 접근과 비슷하다>
javascript에서 Scope(유효범위)
라는 개념은 너무 굉장히 중요하다.
그렇다면 왜 중요할까?
Scope(유효범위)
란 어떤 변수들에게 접근할 수 있는지를 정의하고 javascript 뿐만 아니라 모든 프로그래밍 언어 코드의 가장 기본적인 개념의 하나이다.
javascript의 Scope(유효범위)
는 다른 언어의 유효범위와는 다르다.
Scope(유효범위)
개념은 쉬운 내용이면서 쉽게 이해하지 못할 함정에 빠지기도 한다.
기본이 탄탄해야 된다는 말이 있듯이 기본이 되야하는 Scope(유효범위)
에 대해 알아보도록 하자.
Scope를 직역하면 영역,범위
라는 뜻이다. 하지만 프로그래밍 언어측면에서 유효범위는
어느 범위까지 참고하는지, 변수와 매개변수(parameter)의 접근성과 생존기간을 뜻한다.
따라서 유효범위를 잘 숙지하고 있으면 변수와 매개변수의 접근성과 생존기간을 잘 제어할 수 있다.
javascript에선 두 가지의 종류의 스코프가 있다.
변수가 함수 바깥이나 중괄호({}
) 바깥에 선언되었다면, 전역 스코프에 정의된다고 한다.
const globalVariable = "hello world"
전역 스코프를 선언하면, 코드 모든 곳에서 해당 변수를 참조할 수 있다.(함수에서도 포함)
const fruit = 'apple'
function delicious(){
console.log(fruit)
}
console.log(fruit) // 'apple'
console.log(delicious) // 'apple'
전역 스코프에 변수를 선언할 수 있어도, 가급적이면 그러지 않는 것이 좋다.
왜냐하면, 두개 이상의 변수 이름이 중복되어 충돌될 수 있기 때문이다.
만약 변수에 const
,let
을 사용하여 선언하였다면, 이름에 충돌이 발생할 때마다 에러가 발생한다.
//이렇게 하면 안돼요!!
let a = 'something'
let a = 'something else' //Error, a has already been declared
만약 var
을 사용하였다면 두번째 변수가 첫번째 변수를 덮어쓰게 된다.
이러면 디버깅이 어려워지기 때문이 이렇게 사용하면 안된다.(ES6에선 var
를 사용하지 않는다.)
//이렇게 하면 안돼요!!
var a = 'something'
var a = 'something else'
console.log(a); // 'something else'
그래서 우리는 전역변수가 아닌, 지역변수로 변수를 선언해야 한다.
우리 코드의 특정 부분에서만 사용할 수 있는 변수는 지역 스코프에 있다. 그리고 이런 변수들은 지역 변수라고 불린다.
javascript에서는 두가지 지역 변수가 존재한다.
먼저 함수 스코프(function Scope)에 대해 알아보자.
우리가 함수 내부에서 변수를 선언하게 되면, 그 변수는 함수내에서만 접근이 가능하다.
함수 바깥에서는 해당 변수에 접근할 수 없다.
function sayHello(){
const hello = 'hello world'
console.log(hello);
}
sayHello() // 'hello world'
console.log(hello) // Error, hello is not defined
위 예제를 살펴보면 변수 hello는 sayHello의 스코프 내에 존재한다는 것을 알 수 있다.
중괄호({}
) 내부에서 const
,let
변수를 선언하면, 그 변수들은 블록 내부에서만 접근이 가능하다.
{
const hello = 'Hello world'
console.log(hello) // 'Hello world'
}
console.log(hello) // Error, hello is not defined
위 예제에서 볼 수 있듯이 변수 hello는 중괄호 내부의 스코프에 존재한다.
함수가 함수 선언식(Function Declaration)
으로 선언되면, 현재 스코프의 최상단으로 호이스팅 된다.
다음 예제에서는 두 가지 경우가 같은 결과를 보인다.
// 아래 두 가지 경우는 같은 결과를 보인다.
sayHello()
function sayHello () {
console.log('Hello world');
}
// 위 코드와 동일하다.
function sayHello () {
console.log('Hello world');
}
sayHello()
반면 함수 표현식(function expression)
으로 선언될 경우 함수는 최상단으로 호이스팅 되지 않는다.
sayHello() // Error, sayHello is not defined
const sayHello = function () {
console.log('Hello world')
}
이렇게 함수 선언문과 함수 표현식을 알아보았고, 두 가지의 방식이 다르기 때문에 함수 호이스팅에 혼란스러 울 수 있어 아무렇게나 사용하면 안된다.
언제나 함수를 호출하기 전에 선언해 놓아야 한다.
함수들이 각각 선언되었을 때, 서로의 스코프에는 접근할 수 없다.
아래의 예제에서, 함수 second는 변수 firstFunction에 접근할 수 없습니다.
function first () {
const firstFunction = 'hello world'
}
function second () {
first()
console.log(firstFunction) // Error, firstFunction is not defined
}
함수가 다른 함수 내부에서 정의 되었다면 내부 함수는 외부 함수의 변수에 접근할 수 있다.
이런 행동을 렉시컬 스코핑(lexical scoping)이라고 부른다.
function outerFunction () {
const outer = 'I’m the outer function!'
function innerFunction() {
const inner = 'I’m the inner function!'
console.log(outer) // I’m the outer function!
}
console.log(inner) // Error, inner is not defined
}
맨 처음에 취조실 사진을 보았을것이다. 스코프가 어떻게 동작하는지 그림을 그려보자면, 취조실 특수 유리(단방향 투과성 유리)를 상상하면 된다.
만약 스코프 내부에 스코프가 있다면, 여러장의 취조실 유리가 겹쳐진 것처럼 그림을 그릴 수 있다.
우리가 바깥을 바라볼 수는 있지만, 바깥에 있는 사람들은 우리를 볼 수 없다.
이제 스코프에 대한 이해가 끝났으니, 클로저에 대해 공부 해보도록 하자.
MDN에서 클로저는 아래와 같이 정의된다.
클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.
그치만 필자인 나도 이렇게 보기만 해선 너무 추상적이라 더 내용을 찾아보았다.
아래는 실제 V8엔진의 클로저 테스트코드와 사람들이 말하는 클로저의 의미이다.
종합해보면, 함수가 무엇을 기억하고 다시 사용한다고 알 수 있는데 여전히 애매모호하다.
클로저를 현대 프로그래밍에서 다음과 같이 해석하여 정의할 수 있다.
클로저 = 함수 + 함수를 둘러싼 환경(Lexical environment)
함수를 둘러싼 환경이라는 것은 앞에서 설명한 렉시컬 스코프이다.
함수를 만들고 그 함수 내부의 코드가 탐색하는 스코프를 함수 생성 당시의 렉시컬 스코프로 고정하면 바로 클로저가 되는 것이다.
렉시컬 스코핑은 위에서 정리해보았다.
javascript에서 클로저는 함수가 생성되는 시점에 생성된다. = 함수가 생성될때, 그 함수의 렉시컬 환경을 포섭(closure)하여 실행될 때 이용한다.
실제로 javascript에서 모든 함수는 클로저이지만, 실제로 우리는 모든 함수를 전부 클로저라고 부르지 않는다.
예제를 보며 클로저의 의미를 조금 더 정확하게 파악해보자.
function foo() {
const color = 'blue';
function bar() {
console.log(color);
}
bar();
}
foo();
bar
함수는 우리가 부르는 클로저일까?
bar
는 foo
안에 속하기 때문에 foo
를 외부 스코프(outer lexical environment) 참조로 저장한다.
그리고 bar
는 foo
의 color
를 정확히 참조 할 것이다.
그렇다면 bar
는 우리가 흔히 부르는 클로저 일까?
정답은 'No'이다.
bar
는 foo
안에 정의되고 실행되었을 뿐, foo
밖으로 나오지 않았기 때문에 클로저라 부를수 없다.
그러면 클로저라 부르는 코드 예제를 보기로하자.
const color = 'red';
function foo() {
const color = 'blue'; // 2
function bar() {
console.log(color); // 1
}
return bar;
}
const baz = foo(); // 3
baz(); // 4
bar
는color
를 찾아 출력하는 함수로 정의되었다.bar
는 외부 스코프의 참조로 foo
의 스코프인 color
를 참조하였다.bar
를 global
의 baz
라는 값으로 할당했다.global
에서 baz(=bar)
를 호출했다.baz(=bar)
는 자신의 스코프에서 color
를 찾는다.foo
의 스코프를 뒤진다.color
를 찾았다. 값은 blue
이다. 따라서 blue
가 출력된다.그렇다면 첫번째의 코드와 현재 코드의 차이점을 비교해보자.
유심있게 보아야 할 부분은 2~4번
,7번
이다.
bar
는 자신이 생성된 렉시컬 스코프에서 벗어나 global
에서 baz
라는 이름으로 호출이 되었고, 스코프 탐색은 현재와 관련 없는 foo
를 탐색한다.
baz
를 bar
로 초기화 할때는 이미 bar
의 외부 스코프(outer lexical environment) 참조
를 foo
로 결정한 이후이다.
때문에 global
에서 아무리 bar
를 호출해도 여전히 foo
에서 color
를 찾는것이다.
이런 baz(=bar)
함수를 '클로저'라고 부른다.
마지막으로 클로저의 문제중 많은 사람들을 헷갈리게 했던 유명한 문제가 있다.
아래를 보도록 하자.
function count() {
var i;
for (i = 1; i < 10; i++) {
setTimeout(function timer() {
console.log(i);
}, i*100);
}
}
count();
우리의 목표는 1,2,3...9를 0.1초마다 출력하는 것이 목표였는데, 막상 실행시켜보면 10
이 9번 출력 되었다.
무엇이 문제일까?
위 코드의 실행순서를 나열해 보자.
timer
함수는 상위 스코프인 count
에게 i
를 요청한다.timer
는 0.1초 뒤에 호출된다.i
는 10
이 되었다.timer
는 0.1초 주기로 호출될 때마다 count
에서 i
를 찾는다.timer
는 이미 10
이 되어 버린 i
만 출력하게 된다.그렇다면 의도대로 1~9까지 순서대로 출력하고 싶으면 어떻게 해야할까?
function count() {
var i;
for (i = 1; i < 10; i += 1) {
(function(countingNumber){
setTimeout(function timer() {
console.log(countingNumber);
}, i*100);
})(i);
}
}
count();
중간에 IIFE(즉시실행함수)
를 덧붙여 setTimeout()
에 걸린 익명함수를 클로저로 만들었다.
앞서 말한대로 클로저는 만들어진 환경을 기억한다.
이 코드에서 i
는 1부터 시작해서 IIFE(즉시실행함수)
내에 countingNumber
라는 형태로 주입되어 반복문이 실행되고,
클로저에 의해 각기 다른 환경속에 포함된다.
반복문은 10회 반복되므로 10개의 환경이 생길 것이고, 10개의 서로 다른 환경에 10개의 서로 다른 countingNumber
가 생긴다.
ES6이후론 이 예제가 문제가 되지 않는다.
function count() {
'use strict';
for (let i = 1; i < 10; i += 1) {
setTimeout(function timer() {
console.log(i);
}, i * 100);
}
}
count();
ES6이후론 이 예제가 문제가 되지 않는다.
let과 const를 사용하면 위와 같은 문제가 발생하지 않는다.
와 깔끔하게 정리 잘하셨네요 유용하게 참고하고 갑니다 댓글 꾹