스코프를 제대로 이해해 보자
스코프란?
“스코프는 범위다”
스코프는 생각보다 직관적인 의미를 갖는다. 프로그래밍에서 또한 “하나의 범위”를 의미하는 개념이기 때문이다. 이러한 스코프는 자바스크립트에서 뿐만이 아니라, 프로그래밍 자체에서 굉장히 중요한 의미를 가지며, 기본 중 기본이라고 할 수 있다. 따라서 스코프를 확실하게 이해해야 호이스팅, this, 변수활용 등에 있어서도 명확하게 이해할 수 있을 것이다.
스코프 : 식별자가 참조될 수 있는 범위
스코프를 조금 더 “프로그래밍스럽게” 설명을 해보자면, 식별자가 참조될 수 있는 범위라고 할 수 있을 것이다.
갑자기 ‘식별자(identifier)’라는 말이 나와서 어렵게 느껴질 수도 있지만, 굉장히 간단하게 이해할 수 있는 개념이다. 식별자는 곧 함수 또는 변수 등의 이름을 의미하기 때문이다. 다시 말해, 스코프는 “변수 또는 함수가 참조될 수 있는 범위”인 것이다. 아래의 코드를 살펴봐보자.
1 | var name = 'Hoon'; |
위의 경우는 각각 어떤 결과가 반환될까?
사실 스코프의 개념이 제대로 잡혀있지 않으면 명확하게 답변하기 어려울 것이다. 정답부터 말하자면, sayName()
은 ‘John Doe’를, console.log
문은 ‘Hoon’을 출력한다. 이처럼, “같은 식별자(변수)가 중복될 경우 무엇이 출력되는가?”에 대한 답변을 위해 필요한 것이 스코프라고 할 수 있다.
즉, 스코프는 식별자가 참조될 수 있는 범위이자, 그 범위를 정하는 규칙이다.
스코프의 종류
스코프의 종류는 크게 두 가지로 분류된다.
종류 | 설명 |
---|---|
전역 스코프(Global Scope) | 어디서든 참조할 수 있는 스코프 |
지역 스코프(Local Scope, Function-Level-Scope) | 특정 블록 내에서만 참조가 가능한 스코프 (함수 자신 or 자신의 하위 함수) |
위의 표와 같이 스코프는 전역 스코프 or 지역 스코프로 나뉜다. 전역 스코프는 말 그대로, 코드 전역에서 참조할 수 있다. 이와 다르게 지역 스코프는 특정한 지역(블록) 내에서만 참조가 가능하다는 특징을 갖는다.
블록(Block)
근데… 위에서 말한 블록이란 무엇을 의미하는 걸까?
1 | 프로그램 코드에서 블록(block)이란 마치 한 문단처럼 보이는, 코드의 한 부분을 뜻하며, |
즉, 중괄호({}
)에 둘러싸인 범위라고 생각하면 된다. 따라서 if문, 반복문, 객체 등은 모두 하나의 고유한 블록을 갖는다고 볼 수 있을 것이다.
블록레벨 스코프 vs 함수레벨 스코프
이제 블록과 스코프가 무엇인지 알았으니, 대부분의 언어에서 따르고 있는 블록레벨 스코프에 대해서 짚고 넘어갈 차례다.
C언어, Java는 기본적으로 블록레벨 스코프를 따른다. 즉, C나 JAVA에서는 기본적으로 if문에서 쓰인 변수는 if문 안에서만 쓰일 수 있다는 것이다. 하지만, 자바스크립트는 함수레벨 스코프를 기반으로 한다. 따라서 블록 밖에서 쓰였더라도, 하나의 함수 안에 선언된 변수라면 참조할 수 있다는 독특한 특징을 갖는다.
1 | function scope() { |
그럼에도 불구하고, 우리 자바스크립트는 블록레벨 스코프처럼 사용될 수도 있다. ES6부터 도입된 const
,let
은 블록레벨 스코프를 따르기 때문이다.
1 | function scope() { |
물론 var
는 함수레벨 스코프를 따르기 때문에 아래와 같은 경우는 오류가 날 것이다.
1 | function scope() { |
이처럼, 자바스크립트는 함수레벨 스코프로도, 블록레벨 스코프로도 사용할 수 있는 참 특별(?)한 언어다.
함수레벨 스코프는 좀 유연하다
함수레벨 스코프의 특징이 하나 더 있는데, 내부 함수의 경우 상위 함수의 변수를 참조 가능하다는 것이다.
바로 코드를 통해 직관적으로 이해해보도록 하자.
1 | const name = 'John'; |
위의 코드 예시처럼, 함수레벨 스코프는 조금 유연한 면이 있다. 전역변수에 간섭받지 않고 함수 내의 독자적인 중복된 변수를 선언하여 활용할 수 있기 때문이다. 뿐만 아니라, 내부 함수는 자신의 상위 함수의 변수를 참조 및 수정까지 가능한 모습이다. 물론 함수 안에서 쓰인 변수가 const
로 선언되었다면 재할당이 불가능했겠지만, let
으로 선언된 name
이 내부함수에서 ‘James’로 수정된 모습을 확인할 수 있다.
렉시컬 스코프
렉시컬 스코프는 함수를 어디서 “호출”했는지가 아닌, 어디에서 “선언”하였는지에 기준을 둔다.
자바스크립트를 포함한 여러 언어들은 대부분 이 렉시컬 스코프를 기반으로 한다.
바로 아래의 코드 예시를 봐보도록 하자. 함수 foo
는 전역에서 호출됐고, bar
는 foo
와 전역에서 호출된 모습이다. 따라서 렉시컬 스코프를 따르지 않는다면 bar의 스코프는 foo와 전역이 될 것이다.
1 | const country = 'Korea'; |
하지만, 자바스크립트는 렉시컬 스코프를 따르기 때문에, 변수가 어디서 “선언” 됐는지를 기준으로 삼는다. 이에 따라 두 함수 모두 전역에서 선언됐기에, bar
는 전역 스코프를 따를 것이다. 이러한 이유로 foo
에서 호출된 bar
도 Korea
를, 전역에서 호출된 bar
도 Korea
를 출력한 것이다.
전역 스코프
근데 그럼.. 함수나 블록에 종속되지 않고(지역스코프), 코드 전역에서 참조될 수 있는 전역 스코프는 어떤 개념이며, 어떤 모습일까?
자바스크립트에서는 전역 스코프를 사용하는 방법이 굉~~장히 간단하다. C언어의 경우에는 main()
밖에 변수를 선언하며 전역 변수를 만들지만, 자바스크립트는 그냥 함수 밖 어디든지 선언해주면 그게 전역변수가 된다.
1 | var global = 'hi'; |
그럼 이 경우는 어떨까?
1 | if (true) { |
위에서도 언급했듯, 자바스크립트의 var
는 블록레벨 스코프가 아니다. 따라서 함수 밖의 블록에서 변수가 선언되면, 이는 전역 변수로써 선언된다. 이 때문에 함수 안에서도 isGlobal
이라는 변수를 참조할 수 있었던 것이다.
물론, 변수 선언을 let
이나 const
로 했으면 블록레벨 스코프를 따르기 때문에 if문 블록 안에서만 사용할 수 있다. 사실, 요즘에는 var
의 사용은 거의 이루어지지 않고, let
과 const
를 대부분 사용하기 때문에 블록레벨 스코프에 대한 이해를 조금 더 신중하게 해야 할 필요가 있다..!! (물론, let
과 const
또한 첫 번째 예와 같이 전역변수로써 선언되면 전역변수로써 활용될 수 있다)
암묵적 전역
그렇다면, 아예
let
,const
,var
에 의해 선언되지 않은 식별자는 어떻게 처리될까?
1 | function foo() { |
사실 스코프를 제대로 공부하기 전까진, 당연히 오류가 날 것이라고 생각했었다. 하지만, 신기하게도 자바스크립트 엔진은 foo()
함수가 호출됨과 동시에 trick
을 탐색한다. 하지만 어디에도 선언된 trick
이 없기 때문에 이 trick
을 window.trick
으로 인식하며 window
프로퍼티 자체에 trick
이라는 프로퍼티를 저장하게 되는 것이다. 이를 ‘암묵적 전역’이라고 하고, 이는 변수가 아닌, 하나의 프로퍼티로써 정의된다.
암묵적 전역에 의해 생성된 프로퍼티는 변수가 아닌 ‘프로퍼티’로 정의되기에, 프로퍼티와 동일하게 활용될 수 있다. (ex. delete trick
과 같이 프로퍼티에만 사용할 수 있는 메소드 사용 가능)
전역 변수 사용을 최대한 지양하기 위한 방법
공통의 ‘전역변수용 객체’를 생성하는 법, 즉시실행함수(IIFE)를 사용하는 방법 등이 있다.
공통의 전역변수용 객체를 생성하는 방법은, 우리가 프론트엔드 개발을 할 때 자주 사용하는 consts 유틸 활용 방법과 동일하다. 그냥 객체를 하나 만들고, 이를 통해 해당 값에 접근하는 것이다.
1 | const GLOBAL = {}; |
마지막으로 즉시 실행 함수는 아래와 같이 활용할 수 있다.
1 | (function iife() { |
이런식으로 즉시실행 함수 안에서 전역변수를 사용하게 되면, 즉시실행 함수가 즉시 실행되고, 실행 후 바로 사라지므로 그 안에서 쓰인 변수들은 즉시실행 함수 밖에서 사용될 수 없다.
스코프를 제대로 이해해 보자