스코프를 제대로 이해해 보자

스코프란?

“스코프는 범위다”

사전적 의미

스코프는 생각보다 직관적인 의미를 갖는다. 프로그래밍에서 또한 “하나의 범위”를 의미하는 개념이기 때문이다. 이러한 스코프는 자바스크립트에서 뿐만이 아니라, 프로그래밍 자체에서 굉장히 중요한 의미를 가지며, 기본 중 기본이라고 할 수 있다. 따라서 스코프를 확실하게 이해해야 호이스팅, this, 변수활용 등에 있어서도 명확하게 이해할 수 있을 것이다.

스코프 : 식별자가 참조될 수 있는 범위

스코프를 조금 더 “프로그래밍스럽게” 설명을 해보자면, 식별자가 참조될 수 있는 범위라고 할 수 있을 것이다.

식별자는 곧 이름

갑자기 ‘식별자(identifier)’라는 말이 나와서 어렵게 느껴질 수도 있지만, 굉장히 간단하게 이해할 수 있는 개념이다. 식별자는 곧 함수 또는 변수 등의 이름을 의미하기 때문이다. 다시 말해, 스코프는 “변수 또는 함수가 참조될 수 있는 범위”인 것이다. 아래의 코드를 살펴봐보자.

1
2
3
4
5
6
7
8
9
var name = 'Hoon';

function sayName() {
var name = 'John Doe';
console.log(name);
}

sayName();
console.log(name);

위의 경우는 각각 어떤 결과가 반환될까?

사실 스코프의 개념이 제대로 잡혀있지 않으면 명확하게 답변하기 어려울 것이다. 정답부터 말하자면, sayName()은 ‘John Doe’를, console.log문은 ‘Hoon’을 출력한다. 이처럼, “같은 식별자(변수)가 중복될 경우 무엇이 출력되는가?”에 대한 답변을 위해 필요한 것이 스코프라고 할 수 있다.

즉, 스코프는 식별자가 참조될 수 있는 범위이자, 그 범위를 정하는 규칙이다.

스코프는 ‘이름’이 참조(사용)될 수 있는 범위라고 할 수 있다.


스코프의 종류

스코프의 종류는 크게 두 가지로 분류된다.

종류 설명
전역 스코프(Global Scope) 어디서든 참조할 수 있는 스코프
지역 스코프(Local Scope, Function-Level-Scope) 특정 블록 내에서만 참조가 가능한 스코프 (함수 자신 or 자신의 하위 함수)

위의 표와 같이 스코프는 전역 스코프 or 지역 스코프로 나뉜다. 전역 스코프는 말 그대로, 코드 전역에서 참조할 수 있다. 이와 다르게 지역 스코프는 특정한 지역(블록) 내에서만 참조가 가능하다는 특징을 갖는다.

블록(Block)

근데… 위에서 말한 블록이란 무엇을 의미하는 걸까?

1
2
3
4
프로그램 코드에서 블록(block)이란 마치 한 문단처럼 보이는, 코드의 한 부분을 뜻하며,
중괄호로 묶여 있는 경우가 많다.
보통 1개 이상의 명령어를 가지고 있으나, 주석으로 이루어진 블록이나, 아무 내용도 없는 빈 블록도 가능하다.
- wikipedia -

즉, 중괄호({})에 둘러싸인 범위라고 생각하면 된다. 따라서 if문, 반복문, 객체 등은 모두 하나의 고유한 블록을 갖는다고 볼 수 있을 것이다.


블록레벨 스코프 vs 함수레벨 스코프

이제 블록과 스코프가 무엇인지 알았으니, 대부분의 언어에서 따르고 있는 블록레벨 스코프에 대해서 짚고 넘어갈 차례다.

C언어, Java는 기본적으로 블록레벨 스코프를 따른다. 즉, C나 JAVA에서는 기본적으로 if문에서 쓰인 변수는 if문 안에서만 쓰일 수 있다는 것이다. 하지만, 자바스크립트는 함수레벨 스코프를 기반으로 한다. 따라서 블록 밖에서 쓰였더라도, 하나의 함수 안에 선언된 변수라면 참조할 수 있다는 독특한 특징을 갖는다.

1
2
3
4
5
6
7
function scope() {
if (true) {
// if 블록 안에서 선언된 foo
var foo = 'bar';
}
console.log(foo); // if 블록 밖에서도 참조가 되는 모습
}

그럼에도 불구하고, 우리 자바스크립트는 블록레벨 스코프처럼 사용될 수도 있다. ES6부터 도입된 const,let은 블록레벨 스코프를 따르기 때문이다.

1
2
3
4
5
6
function scope() {
if (true) {
let foo = 'bar';
}
console.log(foo); // ReferenceError: foo is not defined
}

물론 var는 함수레벨 스코프를 따르기 때문에 아래와 같은 경우는 오류가 날 것이다.

1
2
3
4
5
6
7
function scope() {
if (true) {
var foo = 'bar';
}
}
// 함수 scope라는 함수 밖에서 참조 시도
console.log(foo); // ReferenceError: foo is not defined (함수레벨 스코프인데 함수 밖에서 참조를 시도했기 때문)

이처럼, 자바스크립트는 함수레벨 스코프로도, 블록레벨 스코프로도 사용할 수 있는 참 특별(?)한 언어다.

함수레벨 스코프는 좀 유연하다

함수레벨 스코프의 특징이 하나 더 있는데, 내부 함수의 경우 상위 함수의 변수를 참조 가능하다는 것이다.

바로 코드를 통해 직관적으로 이해해보도록 하자.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const name = 'John';

function foo() {
let name = 'Doe';
console.log(name); // Doe

function bar() {
// 내부함수 bar
name = 'James';
console.log(name); // James
}
bar();
}
foo();
console.log(name); // John

위의 코드 예시처럼, 함수레벨 스코프는 조금 유연한 면이 있다. 전역변수에 간섭받지 않고 함수 내의 독자적인 중복된 변수를 선언하여 활용할 수 있기 때문이다. 뿐만 아니라, 내부 함수는 자신의 상위 함수의 변수를 참조 및 수정까지 가능한 모습이다. 물론 함수 안에서 쓰인 변수가 const로 선언되었다면 재할당이 불가능했겠지만, let으로 선언된 name이 내부함수에서 ‘James’로 수정된 모습을 확인할 수 있다.


렉시컬 스코프

렉시컬 스코프는 함수를 어디서 “호출”했는지가 아닌, 어디에서 “선언”하였는지에 기준을 둔다.

자바스크립트를 포함한 여러 언어들은 대부분 이 렉시컬 스코프를 기반으로 한다.

바로 아래의 코드 예시를 봐보도록 하자. 함수 foo는 전역에서 호출됐고, barfoo와 전역에서 호출된 모습이다. 따라서 렉시컬 스코프를 따르지 않는다면 bar의 스코프는 foo와 전역이 될 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
const country = 'Korea';

function foo() {
const country = 'USA';
bar();
}

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

foo(); // Korea
bar(); // Korea

하지만, 자바스크립트는 렉시컬 스코프를 따르기 때문에, 변수가 어디서 “선언” 됐는지를 기준으로 삼는다. 이에 따라 두 함수 모두 전역에서 선언됐기에, bar는 전역 스코프를 따를 것이다. 이러한 이유로 foo에서 호출된 barKorea를, 전역에서 호출된 barKorea를 출력한 것이다.


전역 스코프

근데 그럼.. 함수나 블록에 종속되지 않고(지역스코프), 코드 전역에서 참조될 수 있는 전역 스코프는 어떤 개념이며, 어떤 모습일까?

자바스크립트에서는 전역 스코프를 사용하는 방법이 굉~~장히 간단하다. C언어의 경우에는 main() 밖에 변수를 선언하며 전역 변수를 만들지만, 자바스크립트는 그냥 함수 밖 어디든지 선언해주면 그게 전역변수가 된다.

1
2
3
4
5
6
7
var global = 'hi';

function scope() {
console.log(global); // hi
}

console.log(global); // hi

그럼 이 경우는 어떨까?

1
2
3
4
5
6
7
8
9
10
11
if (true) {
// if 블록 내에서 isGlobal이 선언
var isGlobal = 'yeah';
}
// 블록 밖에서 참조
console.log(isGlobal);

function scope() {
// 블록 밖 함수 내에서 참조
console.log(isGlobal);
}

위에서도 언급했듯, 자바스크립트의 var는 블록레벨 스코프가 아니다. 따라서 함수 밖의 블록에서 변수가 선언되면, 이는 전역 변수로써 선언된다. 이 때문에 함수 안에서도 isGlobal이라는 변수를 참조할 수 있었던 것이다.

물론, 변수 선언을 let이나 const로 했으면 블록레벨 스코프를 따르기 때문에 if문 블록 안에서만 사용할 수 있다. 사실, 요즘에는 var의 사용은 거의 이루어지지 않고, letconst를 대부분 사용하기 때문에 블록레벨 스코프에 대한 이해를 조금 더 신중하게 해야 할 필요가 있다..!! (물론, letconst 또한 첫 번째 예와 같이 전역변수로써 선언되면 전역변수로써 활용될 수 있다)

암묵적 전역

그렇다면, 아예 let, const, var에 의해 선언되지 않은 식별자는 어떻게 처리될까?

1
2
3
4
5
6
7
function foo() {
// 선언하지 않은 식별자 y
trick = 'Hello';
console.log(trick);
}

foo(); // Hello

사실 스코프를 제대로 공부하기 전까진, 당연히 오류가 날 것이라고 생각했었다. 하지만, 신기하게도 자바스크립트 엔진은 foo() 함수가 호출됨과 동시에 trick을 탐색한다. 하지만 어디에도 선언된 trick이 없기 때문에 이 trickwindow.trick으로 인식하며 window 프로퍼티 자체에 trick이라는 프로퍼티를 저장하게 되는 것이다. 이를 ‘암묵적 전역’이라고 하고, 이는 변수가 아닌, 하나의 프로퍼티로써 정의된다.

암묵적 전역에 의해 생성된 프로퍼티는 변수가 아닌 ‘프로퍼티’로 정의되기에, 프로퍼티와 동일하게 활용될 수 있다. (ex. delete trick과 같이 프로퍼티에만 사용할 수 있는 메소드 사용 가능)

전역 변수 사용을 최대한 지양하기 위한 방법

공통의 ‘전역변수용 객체’를 생성하는 법, 즉시실행함수(IIFE)를 사용하는 방법 등이 있다.

공통의 전역변수용 객체를 생성하는 방법은, 우리가 프론트엔드 개발을 할 때 자주 사용하는 consts 유틸 활용 방법과 동일하다. 그냥 객체를 하나 만들고, 이를 통해 해당 값에 접근하는 것이다.

1
2
3
4
5
6
7
8
const GLOBAL = {};

GLOBAL.colors = {
white: '#ffffff',
black: '#000000',
};

console.log(MYAPP.student.name);

마지막으로 즉시 실행 함수는 아래와 같이 활용할 수 있다.

1
2
3
4
5
6
7
8
9
10
(function iife() {
const GLOBAL = {};

GLOBAL.colors = {
white: '#ffffff',
black: '#000000',
};

console.log(GLOBAL.colors.white);
})();

이런식으로 즉시실행 함수 안에서 전역변수를 사용하게 되면, 즉시실행 함수가 즉시 실행되고, 실행 후 바로 사라지므로 그 안에서 쓰인 변수들은 즉시실행 함수 밖에서 사용될 수 없다.


스코프를 제대로 이해해 보자

https://hoonjoo-park.github.io/javascript/base/scope/

Author

Hoonjoo

Posted on

2022-04-05

Updated on

2022-04-05

Licensed under

Comments