javascript 12강 스코프


JavaScript 12강 예습

  • 12강 : 스코프
    1. 스코프란?
      • 비순수 함수
    2. 스코프 위치에 따른 값
      • 코드의 문맥과 환경
      • var 키워드로 선언한 변수의 중복 선언
      • let, const
    3. 스코프의 종류
      • 전역과 전역 스코프
      • 지역과 지역 스코프
    4. 스코프 체인
      • 스코프 체인에 의한 변수 검색
      • 스코프 체인에 의한 함수 검색
    5. 함수 레벨 스코프
    6. 렉시컬 스코프
    7. 암묵적 전역 변수


12강

스코프


스코프란?

유효범위를 뜻하는 스코프는 JS를 포함한 모든 프로그래밍 언어즤 기본적이면 중요한 개념이다.

*모든 식별자(변수 이름, 함수 이름, 클래스 이름 등)는 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. *

*이를 스코프(Scope, 유효범위)라 한다. *

즉, 스코프는 식별자가 유효한 범위를 말한다.

1
2
3
4
5
6
7
8
var num = 0; // 현재 카운트를 나타내는 상태: increase 함수에 의해 변화한다.

function increase() {
return ++num; // 외부 상태를 변경한다.
}

console.log(increase()); // 1
console.log(increase()); // 2

num은 전역 변수이기 때문에 increase 함수를 통해 값이 늘어난다.

비순수 함수

​ increase 함수처럼 함수 내부의 상태(함수 내부에서 선언한 변수 또는 원시 타입의 매개 변수)만이 아니라 함수 외부 상태(num)까지 변경하는 함수를 비순수 함수(impure function)라고 한다.

함수가 외부 상태를 변경하면 상태 변화를 추적하기 어려워진다. 따라서 함수 외부 상태의 변경을 지양하는 순수 함수(pure function)를 사용하는 것이 좋다.


스코프 위치에 따른 값

1
2
3
4
5
6
7
8
9
10
var x = 'global';

function foo() {
var x = 'local';
console.log(x); // ① local
}

foo();

console.log(x); // ② global

위 예제에서 JS엔진은 스코프를 통해 어떤 변수를 참조하고 출력해야할지 결정한다.

여기서 스코프란, JS엔진이 식별자를 검색할 때 사용하는 규칙이라고 할 수 있다.

네임스페이스

코드의 문맥과 환경

“코드가 어디서 실행되며 주변에 어떤 코드들이 있는지”를 환경(Environment)이라고 부른다.

즉, 코드의 문맥(Context)은 환경으로 이루어진다. 이를 구현한 것이 “실행 컨텍스트(Execution context)”이며 모든 코드는 실행 컨텍스트에서 평가되고 실행된다. 스코프는 실행 컨텍스트와 깊은 관련이 있다.


만약 스코프란 개념이 없다면 같은 네이밍을 가진 변수들끼리 충동을 일으키기 때문에 모든 변수는 고유한 네이밍을 가져야 할것이다.

만약 그럴경우, 소스가 길어지게 될 경우 변수 네이밍을 전부다 기억해야되는 불상사가 생길수도 있다.

따라서, 스코프를 이용하여 이러한 충돌을 방지하고 같은 네이밍을 쓸 수 있다.


var 키워드로 선언한 변수의 중복 선언

var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언이 허용된다.

이는 의도치 않게 변수값이 변경되는 부작용을 발생시킨다.

1
2
3
4
5
6
7
8
function foo() {
var x = 1;
// var 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용한다.
// 아래 변수 선언문은 자바스크립트 엔진에 의해 var 키워드가 없는 것처럼 동작한다.
var x = 2;
console.log(x); // 2
}
foo();

let , const

하지만 let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다.

1
2
3
4
5
6
function bar() {
let x = 1;
// let이나 const 키워드로 선언된 변수는 같은 스코프 내에서 중복 선언을 허용하지 않는다.
let x = 2; // SyntaxError: Identifier 'x' has already been declared
}
bar();

스코프의 종류

스코프는 전역(global)과 지역(local)으로 나뉜다.

구분 설명 스코프 변수
전역 코드의 가장 바깥 영역 전역 스코프 전역 변수
지역 함수 몸체 내부 지역 스코프 지역 변수

전역과 전역 스코프

전역 : 코드의 가장 바깥 영역을 의미. 전역은 전역 스코프를 만든다.

전역 변수는 어디서든지 참조할 수 있다.


지역과 지역 스코프

지역 : 함수 몸체 내부를 의미. 지역은 지역 스코프를 만든다.

지역 변수는 자신이 선언된 지역과 하위 지역(중첩 함수)에서만 참조가 가능하다.


스코프 체인

​ 함수 몸체 내부에서 정의한 함수를 ‘중첩 함수(nested function)’, 중첩 함수를 포함하는 함수를 ‘외부 함수(outer function)’라고 부른다.

​ 여기서 생기는 함수들의 중첩은 곧 함수의 지역 스코프도 중첩이 될 수 있다는 것을 의미하며,
스코프는 곧 함수의 중첩에 의해 계층적 구조를 지닌다는 것을 알 수 있다.

또한 모든 지역 스코프의 최상위 스코프는 전역 스코프이다.

이러한 스코프들간의 계층적인 연결을 스코프 체인이라 부른다.

변수를 참조할 때, 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색한다.


스코프 체인에 의한 변수 검색

상위 스코프에서 유효한 변수는 하위 스코프에서 자유롭게 참조할 수 있지만 하위 스코프에서 유효한 변수를 상위 스코프에서 참조할 수 없다.

스코프 체인으로 연결된 스코프의 계층적 구조는 부자 관계로 이루어진 상속(Inheritance)과 유사하다. 상속을 통해 부모의 자산을 자식이 자유롭게 사용할 수 있지만 자식의 자산을 부모가 사용할 수는 없다. 스코프 체인도 마찬가지 개념이다.


스코프 체인에 의한 함수 검색

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 전역 함수
function foo() {
console.log('global function foo');
}

function bar() {
// 중첩 함수
function foo() {
console.log('local function foo');
}

foo(); // ①
}

bar();

자바스크립트 엔진은 함수 이름과 동일한 이름의 변수를 암묵적으로 선언하고 생성된 함수 객체를 할당한다.

따라서 위 예제의 모든 함수는 자바스크립트 엔진에 의해 암묵적으로 선언된 함수 이름과 동일한 이름의 변수에 할당된다.

이처럼 함수도 변수에 할당되기 때문에 스코프를 갖는다.

사실 함수는 변수에 함수 객체가 할당된 것 외에는 일반 변수와 다를 바가 없다.

따라서 스코프를 “변수를 검색할 때 사용하는 규칙”이라고 표현하기 보다는 “식별자를 검색하는 규칙”이라고 표현하는 것이 보다 적합하다.


함수 레벨 스코프

var 키워드로 선언된 변수는 오로지 함수의 코드 블록 만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프(Function level scope)라 한다.

let , const 키워드는 블록 레벨 스코프(함수 몸체 만이 아니라 모든 코드 블록(if, for, while, try/catch 등)이 지역 스코프를 만든 것)를 지원한다.

1
2
3
4
5
6
7
8
9
var i = 10;

// for문에서 선언한 i는 전역 변수이다. 이미 선언된 전역 변수 i가 있으므로 중복 선언된다.
for (var i = 0; i < 5; i++) {
console.log(i); // 0 1 2 3 4
}

// 의도치 않게 변수의 값이 변경되었다.
console.log(i); // 5

렉시컬 스코프

1
2
3
4
5
6
7
8
9
10
11
12
13
var x = 1;

function foo() {
var x = 10;
bar();
}

function bar() {
console.log(x);
}

foo(); // ?
bar(); // ?

상위 스코프를 결정 짓는 방식은 2가지가 있다.

  1. 동적 스코프 : 함수를 어디서 호출했는지에 따라 함수의 상위 스코프를 결정한다.

  2. 렉시컬(정적) 스코프 : 함수를 어디서 정의했는지에 따라 함수의 상위 스코프를 결정한다.

JS는 렉시컬 스코프를 따른다.

위 예제에서 bar( ) 함수의 정의는 전역이다. 따라서 bar( )의 상위 스코프는 전역으로 결정이된다.

전역 변수 x의 값이 1이므로 foo( ) 와 bar( ) 모두 정답은 1이다.

렉시컬 스코프클로저 와 관계 깊다. (추후 학습)


암묵적 전역 변수

1
2
3
4
5
6
7
8
function foo() {
// 선언하지 않은 변수에 값을 할당하면 암묵적 전역 변수가 된다.
x = 10;
}

foo();

console.log(x); // 10

선언하지 않은 변수에 값을 할당하면 자바스크립트 엔진은 아무런 에러없이 암묵적으로 전역 변수를 선언하고 값을 할당한다.

위는 소스에서 JS엔진은 다음과 같이 동작한다.

  1. 함수 foo의 스코프에서 변수 x에 관한 선언을 찾는다.
  2. 못찾을 경우, 상위 스코프(전역) 으로 이동해 변수 x에 관한 선언을 찾는다.
  3. 역시 못찾을 경우, 참조 에러를 발생 시켜야 겠지만, JS엔진은 암묵적으로 변수 x를 선언한다.

이러한 변수를 암묵적 전역 변수라 한다.


1
2
3
4
5
// x.js
function foo() {
i = 0; // 암묵적 전역 변수
// ...
}
1
2
3
4
5
6
7
8
// y.js
// var 키워드로 선언된 변수는 함수의 코드 블록 만을 지역 스코프로 인정한다.
// 따라서 for문에서 선언한 i는 전역 변수이다.
// 이미 암묵적 전역 변수 i가 있으므로 변수 i는 중복 선언된다.
for (var i = 0; i < 5; i++) {
foo();
console.log(i);
}

서로 분리된 x.jsy.js 라는 서로 다른 파일이 존재한다고 가정하자.

여기서 HTML이 위 두 파일을 다음처럼 로드할 경우,

1
2
3
4
5
6
7
<!DOCTYPE html>
<html>
<body>
<script src='x.js'></script>
<script src='y.js'></script>
</body>
</htm

HTML에서 로드한 자바스크립트 파일은 여러 개로 분리되어 있다해도 하나의 전역 스코프를 공유한다. 다시 말해 자바스크립트는 파일마다 독립적인 파일 스코프를 갖지 않는다.

따라서 자바스크립트 파일을 여러 개로 분리하여도 결국 하나의 자바스크립트 파일로 통합된 것처럼 동작한다. 이러한 특징은 자바스크립트에 모듈이라는 개념이 없기 때문이다.

위 예제에서는 결국 나중에 선언된 y.js의 i값이 x.js의 i값을 덮어씌우게 된다.

결국, JS는 변수의 중복 선언을 허용하기 때문에 어떠한 에러도 발생시키지 않고 무한 반복 상태에 빠져들게 될 것이다.

Share