[JS][함수 vs 블록 스코프] 일반 스코프에 숨기
일반 스코프에 숨기
함수에 대한 전통적인 개념은 이렇다.
- 함수를 선언하고 그 안에 코드를 넣는다. 바꿔 생각해보는 것도 꽤 유용하다.
- 작성한 코드에서 임의 부분을 함수 선언문으로 감싼다. 이는 해당 코드를 '숨기는'효과를 낸다.
이렇게 하면 해당 코드 주위에 새로운 스코프 버블이 생ㅅ어된다. 즉, 감싸진 코드 안에 있는 '모든 변수' 또는 '함수 선언문'은 이전 코드에 포함됐던 스코프가 아니라 새ㅇ로이 코드를 감싼 함수의 스코프에 묶인다. 달리 말하면, 함수의 스코프로 둘러싸서 변수와함수를 '숨길' 수 있다는 말이다. 그렇다면 코드를 '숨기는'테크닉은 어디에 유용할까?
스코프를 이용해 숨기는 방식을 사용하는 이유는 여러 가지가 있는데, 소프트웨어 디자인 원칙인 '최소 권한의 원칙'과 관련이 있다. 이 원칙큰 모듈/객체의 API와 같은 소프트웨어를 설계할 때 필요한 것만 최소한으로 남기고 나머지는 '숨겨야'한다는 것이다.
이 원칙은 어떤 스코프가 변수와 함수를 포함하는지에 관한 문제와도 관련이 있다. 모든 변수와 함수가 글로벌 스코프에 존재한다면 어느 중첩된 하위 스코프에서도 이들에 접근할 수 있다. 이는 '최소'의 원칙을 어기는 것이고, 코드를 적절하게 사용했을 때 접근할 필요가 없어서 비공개로 남겨둬야할 많은 변수나 함수를 노출시키게 된다.
function doSomething(a) {
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
function doSomethingElse(a) {
return a - 1;
}
var b;
doSomething(2); // 15
이 코드에서 변수 b와 함수 doSomethingElse()
는 doSomething()
이 어떤 작업을 하는지 보여주는 '비공개' 부분이라고 할 수 있다. 변수 b와 doSomethingElse()
에 '접근'할 수 있도록 내버려 두는 것은 불필요할 뿐 아니라 '위험'할 수 있다. 접근 가능한 확인자는 의도적이든 아니든 생각지못한 방식으로 사용될 수 있으며, doSomething()
의 실행에 전제되는 가정들이 훼손될 수 있다.
더 '적절하게' 설계하려면 다음과 같이 앞의 비공개 부분은 doSomething()
스코프 내부에 숨겨야 한다.
function doSomething(a) {
function doSomethingElse(a) {
return a - 1;
}
var b;
b = a + doSomethingElse(a * 2);
console.log(b * 3);
}
doSomething(2); // 15
이제 b와 doSomethingElse()
는 외부에서 접근할 수 없어서 더는 바깥의 영향을 받지 않고, 오직 doSomething()
만이 이들을 통제한다. 기능과 최종 결과는 달라지지 않았지만, 변경된 디자인은 비공개로 해야 할 내용을 확실하게 비공개로 둔다. 일반적으로 이것이 더 나은 코드라고 본다.
충돌 회피
변수와 함수를 스코프 안에 '숨기는 것'의 또다른 장점은 같은 이름을 가졌지만 다른 용도를 가진 두 확인자가 충돌하는 것을 피할 수 있다는 점이다. 이런 충돌은 종종 예상하지 못한 변숫값의 겹쳐 쓰기를 초래한다.
function foo() {
function bar(a) {
i = 3;
console.log(a + i);
}
for (var i = 0; i < 10; i++) {
bar(i * 2);
}
}
foo();
bar()
내부의 대입문 i = 3
은 예기치 않게 foo()
에서 for반복문을 위해 선언된 변수 i의 값을 변경한다. 그 결과, 이 코드는 무한 반복에 빠진다. 변수 i의 값이 3으로 고정되어 영원히 i < 10
인 상태로 머물기 때문이다.
bar()
내부의 대입문은 어떤 확인자 일므을 고르든 지역 변수로 선언해서 사용해야 한다. var i = 3
으로 변경하면 문제를 해결할 수 있다. 이를 대체할 수는 없지만, 추가적인 선택 안은 var j = 3
과 같이 완전히 다른 확인자 이름을 고르는 것이다. 그러나 소프트웨어 설계를 하다 보면 자연스럽게 같으 확인자 이름을 사용하게 되므로 스코프를 이용해서 내부에 선언문을 '숨기는' 것이 가장 좋은 선택지ㅏ.
글로벌 '네임스페이스'
글로벌 스코프에서 변수 충돌이 특히 일어나기 쉬운 경우에 대해 알아보자. 내부/비공개 함수와 변수가 적절하게 숨겨져 있찌 않은 여러 라이브러리를 한 프로그램에서 불러오면 라이브러리들은 서로 쉽게 충돌한다.
이런 라이브러리는 일반적으로 글로벌 스코프에 하나의 고유 이름을 가지는 객체 선언문을 생성한다. 이후 객체는 해당 라이브러리의 '네임스페이스'로 이용된다. 네임스페이스를 통해 최상위 스코프의 확인자가 아니라 속성 형태로 라이브러리의 모든 기능이 노출된다.
var MyReallyCoolLibarary = {
awesome: "stuff",
doSomething: function () {
// ...
},
doAnotherThing: function () {
// ..
},
};
모듈 관리
좀 더 현대적인 충돌 방지 옵션으로는 다양한 의존성 관리자를 이용한 '모듈'접근법이 잇다. 이도구를 사용하면 어떤 라이브러리도 확인자를 글로벌 스코프에 추가할 필요 없이, 특정 스코프로부터 의존성 관리자를 이용한 다양한 명시적인 방법으로 확인자를 가져와 사용할 수 있다.
기억할 것은 이런 도구를 사용한다고 '마법'처럼 렉시컬 스코프 규릭에서 벗어날 수 있는 것이 아니라는 점이다. 의존성 관리자는 그저 여기서 설명한 스코프 규칙을 적용해 모든 확인자가 공유 스코프에 누출되는 것을방지하고, 우발적인 스코프 충돌을 예방하기 위해 충돌 위험이 없는 비공개 스코프에 확인자를 보관한다.
물론 하려고 한다면 방어적으로 코딩하여 실제 의존성 관리자를 사용하지 않고도 사용한 것과 같은 결과를 얻을 수 있다.
참고
- You Don't Know JS - 타입과 문법, 스코프와 클로저( 한빛 미디어 )