스코프(scope)란 참조 대상 식별자(identifier, 변수, 함수의 이름과 같이 어떤 대상을 다른 대상과 구분하여 식별할 수 있는 유일한 이름)를 찾아내기 위한 규칙이다. 자바스크립트는 이 규칙대로 식별자를 찾는다.
- 모던 자바스크립트 Deep Dive -
아래 예시를 보자.
var x = "global";
function foo() {
var x = "function scope";
console.log(x);
}
foo(); // function scope
console.log(x); // global
전역에 선언된 변수 x
는 어디에든 참조할 수 있다. 하지만 함수 foo
내에서 선언된 변수 x
는 함수 foo
내부에서만 참조할 수 있고 함수 외부에서는 참조할 수 없다. 이러한 규칙을 스코프라고 한다.
스코프라는 개념이 없다면,
같은 식별자 이름은 충돌을 일으키므로 프로그램 전체에서 하나밖에 사용할 수 없다.
스코프는 크게 둘로 나눌 수 있다.
전역 스코프(Global scope)
지역 스코프(Local scope or Function-level scope)
변수 관점에는 다음과 같이 해석 할 수 있겠다.
보통 코딩 공부 처음 시작할 때 많이 들었던 내용일 것이다.(필자는 대학 1학년에 그랬던 것 같다)
전역 변수(Global variable)
지역 변수(Local variable)
즉, 전역
에서 선언된 변수는 전역 스코프
를 가지는 전역 변수
라는 것이고,
지역
에서 선언된 변수는 지역 스코프
를 가지는 지역 변수
라는 것이다.
자바스크립트에서는
함수 레벨 스코프(Function-level scope)
를 따른다.
함수 레벨 스코프
란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다(참조할 수 없다)는 것이다.
- 모던 자바스크립트 Deep Dive -
아까 살펴봤던 예시를 다시 끌어와보자.
var x = "global";
function foo() {
var x = "function scope";
console.log(x);
}
foo(); // function scope
console.log(x); // global
함수 레벨 스코프
를 따르기 때문에, 함수 foo
내에서의 x
는 foo
내에서만 유효한 것을 볼 수 있다.
그러나, 여기선 var
가 사용되고 있다.
필자는 let
이 익숙하므로, let
에 대해서도 살펴보자.
var x = 0;
{
var x = 1;
console.log(x); // 1
}
console.log(x); // 1
let y = 0;
{
let y = 1;
console.log(y); // 1
}
console.log(y); // 0
엥? 뭔가 다르다.
var
의 경우에는 변경된 값이 그대로 블록 밖으로 빠져나오는 반면 let
은 블록 내에서만 유효하다.
이는 let
이 블록 레벨 스코프(Block-level scope)
를 따르기 때문이다.
블록 레벨 스코프
란 코드 블록({…})내에서 유효한 스코프를 의미한다. 여기서 “유효하다”라는 것은 “참조(접근)할 수 있다”라는 뜻이다.
- 모던 자바스크립트 Deep Dive -
지금까지 본 함수 레벨 스코프
와 블록 레벨 스코프
에서의 예시를 보면 사실 약간 헷갈릴 수 있다.
아까는 함수 밖에서는 접근 안된다고 하지않았었나? 왜 여기서는 변경된 값이 전역에 반영된거지?
이건 함수 레벨 스코프
에 대한 설명을 보면 좀 더 명확하게 이해할 수 있다.
함수 레벨 스코프
는 위 설명에 적혀있듯 함수 코드 블록 내에서 선언된 변수에 대한 것이다.
두 예시를 보면 하나는 함수에서, 다른 하나는 일반 블록에서 선언된다.
이 차이로 인해 변경의 반영 여부가 달라지는 것이다.
개념에 대한 설명을 잘 읽어보자!(필자는 헷갈렸다 ㅠ)
전역
에 변수를 선언하면 이 변수는 어디서든지 참조할 수 있는 전역 스코프
를 갖는전역 변수
가 된다.
var global = "global";
function foo() {
var local = "local";
console.log(global);
console.log(local);
}
foo();
console.log(global);
console.log(local); // Uncaught ReferenceError: local is not defined
변수 global
의 경우에는 전역
에 선언되었기 때문에,
함수 foo
내부에서도, 밖에서도 전부 접근이 가능하다.
그러나 변수 local
의 경우에는 지역
에 선언되었기 때문에,
함수 foo
내부에서만 접근이 가능하다.
즉, 변수 global
은 전역 스코프
를 갖는 전역 변수
라는 것을 알 수 있다.
참고로, var
키워드로 선언한 변수는 전역 객체(Global object)
인 window
의 프로퍼티가 된다.
이 내용은 다른 게시글에서 더 살펴볼 예정이다.
자바스크립트에서는 특별한 시작점(Entry point)가 없어서 아무데서나 변수를 선언 할 수 있는데,
전역 변수
의 사용은 변수 이름이 중복될 수 있고, 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만드므로 사용을 억제하여야 한다.
- 모던 자바스크립트 Deep Dive -
그렇다. 아무데서나 막 선언하면 코드를 살펴보는 개발자 입장에서는 독이 된다. 주의하도록 하자.
if (true) {
var x = 5;
}
console.log(x); // 5
앞서 말했듯, 자바스크립트는 함수 레벨 스코프
를 따른다.
그렇지만, 위 예시는 함수 내에서 선언된 것이 아니다.
따라서, 함수 밖에서 선언된 변수는 코드 블록 내에서 선언되었다할지라도 모두 전역 스코프
를 갖게된다. 따라서 변수 x
는 전역 변수
이다.
위에서 헷갈렸다고 말한 부분이 바로 이 부분인데, 이제 명확하게 이해가 된다.
var a = 10; // 전역변수
function foo() {
var b = 20; // 지역변수
}
console.log(a); // 10
console.log(b); // "b" is not defined
다시 한번 말하지만, 자바스크립트는 함수 레벨 스코프
를 따른다.
함수 내에서 선언된 매개변수와 변수는 함수 외부에서는 유효하지 않다.
따라서 변수 b
는 지역 변수
이다.
그런데...아까 살펴봤던 예시들 중 아래와 같은게 있었다.
var x = "global";
function foo() {
var x = "local";
console.log(x);
}
foo(); // local
console.log(x); // global
이렇게 중복되는 선언이 있을 경우에는 어떻게 될까?
전역 영역
에서는 전역변수
만이 참조 가능하고 함수 내 지역 영역
에서는 전역과 지역 변수
모두 참조 가능하나 위 예제와 같이 변수명이 중복된 경우, 지역변수
를 우선하여 참조한다.
함수 내에 있는 함수에 대해서도 살펴보자.
var x = "global";
function foo() {
var x = "local";
console.log(x); // local
function bar() {
console.log(x); // local
}
bar();
}
foo();
console.log(x); // global
내부함수는 자신을 포함하고 있는 외부함수의 변수에 접근할 수 있다.
따라서 함수 bar
는 함수 foo
에서 선언된 변수 x
인 local
을 출력한다.
이유를 더 명확하게 설명하자면 다음과 같다.
실행 컨텍스트
의 스코프 체인
에 의해 참조 순위에서 전역변수 x
가 뒤로 밀렸기 때문이다.
이 부분도 다른 게시글에서 다뤄볼 예정이다.
만약 전역 변수
를 함수 내부에서 수정하게 된다면 어떻게 될까?
var x = 10;
function foo() {
x = 100;
console.log(x); // 100
}
foo();
console.log(x); // 100
변경이 적용되는 것을 볼 수 있다.
이는 함수(지역) 영역
에서 전역변수
를 참조할 수 있으므로 전역변수
의 값도 변경할 수 있기 때문이다.
내부 함수의 경우, 전역변수는 물론 상위 함수에서 선언한 변수에 접근/변경이 가능하다.
var x = 10;
function foo() {
var x = 100;
console.log(x); // 100
function bar() {
x = 1000;
console.log(x); // 1000
}
bar();
}
foo();
console.log(x); // 10
내부 함수에서 수정된 전역 변수
가 반영되어 출력이 된 것을 확인 할 수 있다.
그런데 직전 예시와 약간 다른 점은 var
를 사용해서 재선언을 했다는 점이다.
따라서 함수 foo
내에서는 var x = 100
을 사용하고,
함수 foo
밖에서는 var x = 10
을 사용한다.
이처럼 중첩 스코프는 가장 인접한 지역을 우선하여 참조한다.
아래 예시를 추가로 살펴보자.
var foo = function () {
var a = 3;
var b = 5;
var bar = function () {
var b = 7;
var c = 11;
// 이 시점에서 a는 3, b는 7, c는 11
a += b + c;
// 이 시점에서 a는 21, b는 7, c는 11
};
// 이 시점에서 a는 3, b는 5, c는 not defined
bar();
// 이 시점에서 a는 21, b는 5
};
변수 b
가 재선언이 되는 것을 볼 수 있다.
각 위치에서의 변수 상태를 보면 중첩 스코프가 가장 인접한 지역을 우선하여 참조한다는 것을 알 수 있다.
만약, 함수 내에서 var
로 재선언을 하는게 아니라, 그냥 재할당만 하는 경우는 아래와 같다.
var x = 10;
function foo() {
x = 100;
console.log(x); // 100
function bar() {
x = 1000;
console.log(x); // 1000
}
bar();
}
foo();
console.log(x); // 1000
아래 예시가 어떻게 동작할지 예측해보자.
var x = 1;
function foo() {
var x = 10;
bar();
}
function bar() {
console.log(x);
}
foo(); // 1
bar(); // 1
예상한대로 결과가 나왔나요? ㅎㅎ
아마 예상 결과는 두 가지의 경우로 나뉠 것 같다.
첫 번째는 함수를 어디서 호출하였는지에 따라 상위 스코프를 결정하는 것이고,
두 번째는 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정하는 것이다.
즉, 첫 번째 방식으로는 함수 bar
의 상위 스코프는 함수 foo
와 전역
일 것이고,
두 번째 방식으로는 함수 bar
의 스코프는 전역
일 것이다
프로그래밍 언어는 이 두 가지 방식 중 하나의 방식으로 함수의 상위 스코프를 결정한다.
첫 번째 방식은 동적 스코프(Dynamic scope)
라고 하며,
두 번째는 방식은 렉시컬 스코프(Lexical scope) 또는 정적 스코프(Static scope)
라고 한다.
자바스크립트는 렉시컬 스코프
를 따른다.
즉, 함수를 어디서 선언하였는지에 따라 상위 스코프를 결정한다는 것이다.
따라서, 함수 bar
는 전역
에서 선언되었으므로 1
을 출력하는 것이다.
아래 코드를 보자.
var x = 10; // 전역 변수
function foo() {
y = 20; // 선언하지 않은 변수
console.log(x + y);
}
foo(); // 30
변수 y
가 선언되지 않았다. 그러나 이는 에러를 발생시키지 않는다.
이는 선언하지 않은 식별자
에 값을 할당하면 전역 객체의 프로퍼티
가 되기 때문이다.
함수 foo
가 호출되면 자바스크립트 엔진은 변수 y
에 값을 할당하기 위해 먼저 스코프 체인
을 통해 선언된 변수인지 확인한다.
이때 foo
함수의 스코프와 전역 스코프
어디에서도 변수 y
의 선언을 찾을 수 없다.
참조 에러가 발생해야 하지만 자바스크립트 엔진은 y = 20
을 window.y = 20
으로 해석하여 프로퍼티를 동적 생성
한다.
결국 y
는 전역 객체의 프로퍼티
가 되어 마치 전역 변수
처럼 동작한다.
이러한 현상을 암묵적 전역(implicit global)
이라 한다.
하지만 y
는 변수 선언없이 단지 전역 객체의 프로퍼티로 추가되었을 뿐이다. 따라서 y
는 변수가 아니다.
따라서 변수가 아닌 y
는 변수 호이스팅
이 발생하지 않는다.
console.log(x); // undefined
console.log(y); // ReferenceError: y is not defined
var x = 10; // 전역 변수
function foo() {
y = 20; // 선언하지 않은 변수
console.log(x + y);
}
foo(); // 30
콘솔을 찍어보면 x
는 호이스팅이 된 것에 반해, y
는 호이스팅이 되지 않았다.
호이스팅
에 대해서도 다른 게시글에서 다뤄볼 예정이다.
y
는 변수가 아니라 단순 프로퍼티이기 때문에 delete
를 통해 삭제도 가능하다.
아래 예시를 통해 확인해보자.
var x = 10; // 전역 변수
function foo() {
y = 20; // 선언하지 않은 변수
console.log(x + y);
}
foo(); // 30
console.log(window.x); // 10
console.log(window.y); // 20
delete x; // 전역 변수는 삭제되지 않는다.
delete y; // 프로퍼티는 삭제된다.
console.log(window.x); // 10
console.log(window.y); // undefined
스코프 체인은 Lexical Environment
와 같이 보면 이해가 잘된다.
이 부분은 Sukhjinder Arora님 블로그의 글을 의역하였습니다
자바스크립트에서 변수가 사용될 때, 자바스크립트 엔진은 현재 스코프
에서 변수를 찾을 것이다.
만약 변수를 찾지 못하면, 엔진은 바깥 스코프
를 찾으며,
결국에는 변수를 찾아내거나, 전역 스코프(global scope)
에 도달할 것이다.
만약, 여전히 변수를 찾지못했다면,
엔진은 암묵적으로 전역 스코프
에 변수를 선언하거나(엄격 모드(strict mode)가 아닌 경우)
에러를 반환한다.
아래 예시를 통해 살펴보자.
let foo = "foo";
function bar() {
let baz = "baz";
console.log(baz); // baz
console.log(foo); // foo
number = 42;
console.log(number); // 42
}
bar();
함수 bar()
가 실행될 때, 자바스크립트 엔진은 변수 baz
를 현재 스코프
에서 찾는다.
다음으로, 변수 foo
를 현재 스코프에서 찾고, 거기에 없으면, 바깥 스코프
에서 변수를 찾는다.
예시에서는 바깥 스코프
가 전역 스코프
이다.
그 이후, 변수 number
에 42
를 할당한다.
따라서, 자바스크립트 엔진은 변수 number
를 현재 스코프
에서 찾아본 후, 바깥 스코프
에서도 찾는다.
만약, 스크립트가 엄격 모드(strict mode)가 아니라면,
엔진은 number
라는 이름의 새로운 변수를 생성하고 42
를 할당한다.
혹은 에러를 반환한다(엄격 모드에서)
결국, 변수가 사용될 때, 엔진은 변수를 찾을 때까지 스코프 체인을 횡단(traverse)할 것이다.
여기서부터는 Lexical Environment
에 대한 내용이 섞여서 나옵니다.
혹시나 해당 개념에 대해 궁금하신 분은 필자의 실행 컨텍스트에 대한 게시글을 참고해주세요
이제 자바스크립트가 스코프와 스코프 체인을 정의하기 위해 어떻게 Lexical Environment
를 사용하는지 알아보자.
아래 예시를 보자.
let greeting = "Hello";
function greet() {
let name = "Peter";
console.log(`${greeting} ${name}`);
}
greet(); // Hello Peter
{
let greeting = "Hello World!";
console.log(greeting); // Hello World!
}
위의 스크립트가 불러와졌을 때(loads),
전역 스코프
내에 정의된 변수와 함수를 포함하는 global Lexical Environment
가 생성된다.
예를들어, 다음과 같이 말이다.
globalLexicalEnvironment = {
greeting: 'Hello'
greet: <ref. to greet function>
outer: <null>
}
외부(outer) Lexical Environment
이 null
로 세팅되어 있다.
전역 스코프
후에 더 이상 바깥 스코프
가 없기 때문이다.
이 이후, 함수 greet()
의 호출을 만난다.
따라서, 함수 greet()
를 위해 새로운 Lexical Environment
가 생성된다.
아래와 같이 말이다.
functionLexicalEnvironment = {
name: 'Peter'
outer: <globalLexicalEnvironment>
}
이번에는 외부 Lexcial Environment
가 globalLexicalEnvironment
로 되어있다.
왜냐면 greet()
의 외부 스코프
가 전역 스코프
이기 때문이다.
이 이후, 자바스크립트 엔진은
console.log(`${greeting} ${name}`);
위 구문을 실행한다.
자바스크립트 엔진은 함수의 Lexical Environment
내에서 변수 greeting
,name
을 찾으려고 한다.
현재 Lexical Environment
에서 변수 name
은 찾았지만, greeting
은 찾지 못했다.
따라서, 엔진은 외부 Lexcial Environment
(외부 속성에 의해 정의된, 즉, 전역 Lexical Environment) 내에서 변수 greeting
을 찾아낸다.
그 다음, 자바스크립트 엔진은 블록 내에서 코드를 실행한다.
따라서, 엔진은 블록을 위해 새로운 Lexical Environment
를 생성한다.
아래와 같이 말이다.
blockLexicalEnvironment = {
greeting: 'Hello World',
outer: <globalLexicalEnvironment>
}
그 다음, console.log(greeting)
구문이 실행되고,
자바스크립트 엔진은 현재 스코프
에서 변수를 찾고 사용한다.
따라서, 엔진은 변수를 위해서 외부 Lexical Environment
(전역 Lexcial Environment)를 보지않는다.
참고로, 새로운 Lexical Environment
는 오직 let
과 const
의 선언을 위해서만 생성된다.
var
에 대해서는 생성되지 않는다.
var
선언은 현재 Lexical Environment
(전역 혹은 함수 Lexical Environment)에 추가된다.
이 부분은 실행 컨텍스트에 대한 내용을 보고 오시면 더 이해가 잘 됩니다 :)
따라서, 프로그램에서 변수가 사용될 때,
자바스크립트 엔진은 현재 Lexical Environment
에서 변수를 찾으려고 하며,
만약 변수가 그 곳에 없는 경우, 엔진은 외부 Lexical Environment
에서 변수를 찾는다.
이게 바로 자바스크립트 엔진이 변수를 찾아내는 방법이다.
var MYAPP = {};
MYAPP.student = {
name: "Lee",
gender: "male",
};
console.log(MYAPP.student.name); // Lee
전역 변수
사용을 최소화하는 방법 중 하나는 애플리케이션에서 전역 변수
사용을 위해
전역 변수 객체
하나를 만들어 사용하는 것이다.
(function () {
var MYAPP = {};
MYAPP.student = {
name: "Lee",
gender: "male",
};
console.log(MYAPP.student.name); // Lee
})();
console.log(MYAPP.student.name); // ReferenceError: MYAPP is not defined
전역 변수
사용을 억제하기 위해,
즉시 실행 함수(IIFE, Immediately-Invoked Function Expression)
를 사용할 수도 있다.
이 방법을 사용하면 전역 변수
를 만들지 않으므로 라이브러리 등에 자주 사용된다.
즉시 실행 함수
는 즉시 실행되고 그 후 전역에서 바로 사라진다.
모던 자바스크립트 Deep Dive 페이지
doozi0316님 블로그
Sukhjinder Arora님 블로그