스코프는 컨테이너 역할을 하는 버블이고 변수나 함수같은 식별자가 그 안에서 선언된다.
그렇다면 정확히 어떤 것이 새로운 버블을 만들까? 함수만 버블을 만들까? 자바스크립트의 다른 자료 구조는 스코프 버블을 생성하지 못할까?
이에 대한 가장 일반적인 답변은 자바스크립트가 함수 기반 스코프를 사용하기 때문에 각각 선언된 함수는 저마다의 스코프를 가지지만 다른 어떤 자료구조도 자체적인 스코프를 생성하지 않는다는 것이다.
하나의 함수를 보자.
function foo(a) {
var b = 2;
// some code
function bar() {
// ...
}
//more code
var c = 3;
}
foo()의 스코프 버블은 식별자 a / b / c 와 bar를 포함한다.
(선언문이 스코프의 어디에 있는지는 중요하지 않다. 스코프 안에 있는 모든 변수와 함수는 그 버블에 속한다.)
bar()도 스코프 버블을 가지고 있다.
글로벌 스코프 역시 스코프 버블을 가지고 있다. -> foo()
a / b / c / bar 모두 foo의 스코프 버블에 속하므로 foo 바깥에서는 이들에게 접근할 수 없다. 따라서 다음과 같은 코드는 호출된 식별자가 글로벌 스코프 내에 없기 때문에 Reference Error를 발생시킨다.
bar();
console.log(a, b, c);
반대로 이 모든 확인자(a / b / c / bar)는 foo안에서 접근할 수 있고 bar안에서도 접근할 수 있다.
즉, 안에서 바깥으로 접근할 수 있지만 바깥에서 안으로 접근할 수는 없다.
함수 스코프는 모든 변수가 함수에 속하고 함수 전체에 걸쳐 사용되며 재사용된다는 개념을 가지고 있다.
요약 -> 이러한 방법은 자바스크립트의 동적 특성을 살려 완전히 다른 타입의 값을 필요에 따라 가져올 수 있지만 스코프 전체에서 변수가 살아있다는 점 때문에 예상치 못한 문제를 가져올 수도 있다.
스코프 전체에서 변수가 살아있다는 점 때문에 우리는 예상치 못한 충돌을 가져올 수 있다.
이것을 피하는 방법은 작성해놓은 코드의 임의의 부분을 함수 선언문으로 감싸는 것이다. -> 이는 해당 코드를 숨기는
역할을 한다.
이렇게 코드의 일부를 함수로 선언하면 새로운 스코프 버블이 생성되고 감싸진 코드 안에 있는 변수와 함수 선언문은 더 이상 이전 스코프가 아니라 새로 만들어진 스코프 버블에 묶이게 된다.
달리 말하면 새로운 스코프로 둘러싸는 것은 변수와 함수를 숨길 수 있다는 말이 된다.
스코프를 사용해 숨기는 방식을 사용하는 이유 중 하나는 디자인 원칙 중 하나인 최소 권한의 원칙 를 지키기 위함이다.
이 원칙은 모듈/객체의 API 같은 소프트웨어를 설계할 때 필요한 것만 최소한으로 남기고 나머지는 숨겨야 한다는 것이다.
만약 모든 변수와 함수가 글로벌 스코프에 존재한다면 어느 중첩된 하위 스코프에서도 이들을 사용할 수 있다. 하지만 이는 접근할 필요가 없는 많은 변수나 함수들을 노출시키게 된다.
EX)
function doSomething(a) {
b = a + doSomethingElse(a*2);
console.log(b*3);
}
function doSomethingElse(a) {
return a - 1;
}
var b = 1;
doSomething(2); // 15
위 코드는 최소 권한의 원칙
을 지키지 않으며 글로벌 스코프 내에서 모두 접근이 가능하다.
이것을 아래와 같이 최소 권한의 원칙
을 준수하는 코드로 변경한다.
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a*2);
console.log(b*3);
}
doSomething(2); // 15
같은 결과를 불러오지만 doSomethingElse스코프가 doSomething스코프 내부에 적절히 숨겨지면서 doSomethingElse는 더 이상 외부에서 접근할 수 없게 되었다.
때문에 doSomething만이 doSomethingElse를 통제할 수 있게 되었다.
변수와 함수를 스코프 안에 숨기는 것의 또 다른 장점은 다른 용도로 사용되는 같은 이름의 식별자가 충돌하는 것을 피할 수 있다는 것이다.
물론 겹치는 식별자가 있다면 서로 다른 이름의 식별자로 만들어주는 것도 선택지이지만 프로그램이 커진다면 식별자 하나하나 관리를 하며 충돌이 일어나지 않게 한다는 것을 매우 어려운 일이며 자기도 모르게 같은 식별자 이름을 여러개 만들 상황이 생기기도 할 것이다.
때문에 가장 좋은 방법은 스코프를 사용해 내부에 선언문을 숨기는 것이 가장 좋은 방법이라고 할 수 있다.
글로벌 스코프에서 일어나는 2가지 충돌과 해결법을 알아보자.
-
글로벌 네임스페이스
내부 비공개 함수와 변수가 적절하게 숨겨지지 않은 라이브러리를 한 프로그램에 여러 개 불러오면 라이브러리들은 쉽게 충돌한다.
이러한 라이브러리는 일반적으로 글로벌 스코프에 하나의 고유 이름을 가지는 객체 선언문을 선언한다.
이후 객체는 해당 라이브러리의네임스페이스
로 이용된다.
네임스페이스를 통해 최상위 스코프의 확인자가 아니라 속성 형태로 라이브러리의 모든 기능들이 노출된다. -
모듈 관리
좀 더 현대적인 충돌 방지 옵션으로는 다양한 의존성 관리를 이용한모듈 접근
방법이 있다.
이 도구를 이용하면 어떤 라이브러리들도 식별자를 글로벌 스코프에 추가할 필요없이 특정 스코프로부터 의존성 관리자를 이용해 식별자를 사용할 수 있다.
중요한 것은 이러한 것들이 렉시컬 스코프 규칙에서 벗어날 수 있게 하는 것이 아니라 충돌을 방지할 수 있도록 도와주는 것이다.
지금까지 코드를 함수로 감싸 변수나 함수 선언문을 바깥 스코프로부터 숨기는 것을 알아보았다.
다시 코드를 한 번 보자.
1번째 코드
var = 2;
function foo() {
var a = 3;
console.log(a); // 3
} // <-- and this
foo();
console.log(a);
이 방식은 작동은 하지만 결코 이상적인 방법은 아니다.
-
foo()라는 이름의 함수를 선언해야 한다.
이렇게 되면 foo()라는 함수 식별자를 가진 외부 스코프(이 경우에는 글로벌 스코프)가 오염되며 함수를 직접 이름으로 호출해야만 실제 감싼 코드를 실행할 수 있다.
함수를 이름없이(또는 그 이름이 둘러싸인 스코프를 오염시키지 않고) 선언하고 자동으로 실행될 수 있다면 그 방법이 가장 좋을 것이다.
자바스크립트에서는 이것을 해결할 2가지 방법이 있다.
2번째 코드
var = 2;
(function foo() {
var a = 3;
console.log(a); // 3
})(); // <-- and this
console.log(a);
첫번째 코드와 같아보이지만 두번째 코드와 차이점이 있다.
두번째 코드는 function...이 아닌(function... 처럼 감싼 코드로 되어있다. 이렇게 함수를 ()으로 감싸게 되는 순간 함수 선언문이 아닌 표현식이 된다.
(선언문과 표현식을 구분하는 가장 쉬운 방법은 function이라는 단어가 구문의 어디에 위치하는지 확인하는가를 살펴보자. function이 구문의 시작 위치에 있다면 선언문이고 다른 경우는 표현식이다.)
(function foo(){...})라는 표현식에서 식별자 foo는 오직 ...이 가리키는 스코프 내에서만 찾을 수 있고 바깥 스코프에서는 발견되지 않는다.
때문에 함수를 감싸게 되면 바깥 스코프를 오염시키지 않고 자신만의 스코프를 가질 수 있게 되는 것이다.
()로 함수를 감싸면 표현식으로 바뀌며 (function foo(){...})()처럼 마지막에 ()를 붙이면 함수를 실행할 수 있다.
첫번째 ()는 함수를 표현식으로 바꾸며 두번째 ()는 함수를 실행시킨다.
이러한 패턴을 현재는 즉시호출함수(IIFE) 라고 부르고 있다.
IIFE는 익명함수 표현식으로 가장 흔하게 사용된다.
(익명과 가명의 차이는 함수의 식별자가 존재하는지 아닌지의 차이이다. 익명함수를 사용하는 것은 편리하지만 디버깅 또는 유지보수를 위해 함수의 이름을 기명하는 것은 좋은 습관이다.)
다른 IIFE를 살펴보자.
var a = 2;
(function IIFE(global)){
var a = 3;
console.log(a); // 3
console.log(global.a); // 2
}(window);
console.log(a); // 2
IIFE에 매개변수로 window객체를 넘겨주었다. 이렇게 스코프에 무엇이든 매개변수로 넘길 수 있고 넘긴 window객체는 해당 스코프에서 global이라는 식별자로 받아서 사용할 수 있게 된다.
다른 방식의 IIFE도 있는데 UMD(범용 모듈 정의)프로젝트에서 사용한다.
var a = 2;
(function IIFE(def){
def(window);
})(function def(global){
var a = 2;
console.log(a); // 3
console.log(global.a); // 2
})
console.log(a); // 2
def함수 자체를 매개변수로 주었고 IIFE함수에서 매개변수로 넘겨진다. IIFE에서 받은 매개변수 def가 호출되고 window객체가 global 매개변수로 넘겨진다.
자바스크립트는 함수가 가장 일반적인 스코프 단위인 함수 기반 스코프이며 현재 가장 널리 알려진 디자인 접근법이지만 다른 스코프 범위도 존재하며 알아두는 것이 좋다.
대표적인 것 중 하나가 블록 스코프이며 자바스크립트를 제외한 많은 언어들은 사실 블록 스코프를 지원하기도 한다.
아래는 블록 스코프 중 한 예이다.
for (var i=0; i<10; i++) {
console.log(i);
}
이것은 function으로 시작하는 함수가 아니다. for로 시작하며 {}(블록)으로 구성되어 있는 반복문으로 변수 i를 오직 for 반복문에서만 사용하기 위함이다.
변수 i는 실제로 둘러싼(함수 또는 글로벌) 스코프에 포함된다는 사실을 무시한다. 이것이 바로 블록 스코프의 목적이다. 변수를 최대한 사용처 가까이에서 최대한 작은 유효 범위를 가지도록 선언하기 위함이다.
오직 for 반복문 안에서만 사용할 변수 i를 함수 스코프 전체에 오염시킬 필요가 없다.
또한 개발자들은 의도치 않게 변수가 원래 용도 이외의 곳에서 재사용되어있는지 점검해야 할 필요가 있다.
블록 스코프를 사용한다면 변수 i는 오직 for문에서만 존재할 것이고 이외 함수 어느곳에서 접근하더라도 오류가 발생할 것이다. 이는 변수가 어려운 방식으로 재사용되는 것을 막는 좋은 방법이다.
하지만 자바스크립트는 외견상으로 블록 스코프를 지원하지 않는다. 따라서 블록 스코프를 사용하기 위해 다음과 같은 방법을 사용하기도 한다.
-
with
with는 블록 스코프의 형태를 보여주는 한 예로 바깥 스코프에 영향을 주는 일 없이 with문이 끝날때까지만 존재한다.
하지만 with는 지양하는 것이 좋다.
-
try/catch
try/catch문에서 catch부분에서 선언된 변수는 catch블록 스코프에 속한다.
EX)catch 매개변수인 error를 catch 블록 외에서 사용하려고 하면 에러가 발생한다.
-
let
let은 var과 같이 변수를 선언하는 방식이다.
키워드 let은 선언된 변수를 둘러싼 아무블록({...})의 스코프에 붙인다.
아래의 코드를 보자.
var foo = true; if (foo) { let bar = foo * 2; bar = something(bar); console.log(bar); } console.log(bar);
let을 이용해 변수를 현재 블록에 붙이는 것은 비명시적이다.
코드를 작성하다 보면 블록이 왔다갔다 하기도 하고 다른 블록으로 감싸기도 하는데 이럴때 주의하지 않으면 변수가 어느 블록 스코프에 속한 것인지 착각하기 쉽다.
블록 스코프에 사용하는 블록을 명시적으로 생성하면 이런 문제를 해결하기 쉽다. 변수가 어느 블록에 속해있는지 알기 쉽게 말이다.
아래는 명시적으로 블록을 생성한 것이다.
var foo = true; if (foo) { { let bar = foo * 2; bar = something(bar); console.log(bar); } } console.log(bar);
if문 안에 명시적인 블록을 만들었다. 이렇게 하면 나중에 if문의 위치나 의미를 변화시키지 않아도 전체 블록을 옮기기가 쉬워진다.
나중에 호이스팅이라는 개념을 배울 것인데 호이스팅 은 어디에서 선언됐든 속하는 스코프 전체에서 존재하는 것처럼 취급되는 작용을 말한다.
let을 사용한 선언문은 호이스팅 효과를 받지 않아 let으로 선언된 변수는 실제 선언문 전에는 명백하게 존재하지 않는다.
-
const
키워드 const 역시 let과 같이 블록 스코프를 생성하지만 한번 선언된 값은 고정된다는 차이점이 있다.
선언된 후 const 값을 변경하려고 하면 오류가 발생한다.
블록 스코프는 함수 스코프를 완전히 대체할 수 없다. 두 기술은 공존하고 개발자들은 함수 스코프와 블록 스코프 두 기술을 같이 사용할 수 있어야 더 읽기 쉽고 유지보수가 가능한 코드를 만들 수 있을 것이다.
출처: You don't know JS