본문 바로가기
코드잇 스프린트 6기/스터디 모임

[JS Q.R 스터디] 변수 / 스코프 / 전역 변수 / let, const, 블록레벨 스코프

by 학습하는 청년 2024. 4. 15.

최종 수정 : 2024-04-14

4장. 변수

1. 변수란 무엇이고, 왜 필요한가?

변수는 프로그래밍 언어에서 데이터를 관리하기 위한 핵심 개념이다. 이를 설명하기 위해 변수가 어떤 역할을 수행하는지에 대한 원리를 알아볼 필요가 있다.

 

컴퓨터는 연산과 기억을 수행하는 부품이 나눠져 있다. 컴퓨터는 CPU를 사용해 연산하고, 메모리를 사용해 데이터를 기억한다. 메모리(memory)는 데이터를 저장할 수 있는 메모리 셀(memory cell)의 집합체이다. 하나의 크기는 1바이트(8비트)이며, 컴퓨터는 1바이트 단위로 데이터를 저장하거나 읽어들인다. 또한 각 셀은 고유의 메모리 주소(memory address)를 갖는다. 이 메모리 주소는 메모리 공간의 위치를 나타낸다.

 

컴퓨터는 모든 데이터를 2진수로 처리한다. 따라서 메모리에 저장되는 데이터는 종류와 상관없이 모두 2진수로 저장된다. 이 메모리 주소를 통해 값에 직접 접근하는 것은 치명적인 오류를 발생시킬 가능성이 매우 높다. 또한 동일한 컴퓨터에서 동일한 코드를 실행해도 코드가 실행될 때마다 값이 저장될 메모리 주소는 변경된다. 이처럼 코드가 실행되기 이전에는 값이 저장된 메모리 주소를 알 수 없으며, 알려 주지도 않는다. 따라서 메모리 주소를 통해 값에 직접 접근하려는 시도는 올바른 방법이 아니다.

 

프로그래밍 언어는 기억하고 싶은 값을 메모리에 저장하고, 저장된 값을 읽어 들여 재사용하기 위해 변수라는 메커니즘을 제공한다. 변수(variable)은 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 공간을 식별하기 위해 붙인 이름을 뜻한다. 변수는 프로그래밍 언어의 컴파일러 또는 인터프리터에 의해 값이 저장된 메모리 공간의 주소로 치환되어 실행된다. 메모리 공간에 저장된 값을 식별할 수 있는 고유한 이름을 변수 이름(변수명), 변수에 저장된 값을 변수 값이라고 한다. 변수에 값을 저장하는 것을 할당(assingment, 또는 대입, 저장))이라 하고, 변수에 저장된 값을 읽어 들이는 것을 참조(reference)라 한다.

 

2. 식별자

변수 이름을 식별자(identifier)이라고도 한다. 어떤 값을 구별해서 식별할 수 있는 고유한 이름을 뜻한다. 다시 상기해야 할 점은 식별자는 값이 아니라 메모리 주소를 기억하고 있다는 사실이다. 식별자라는 용어는 변수 이름에만 국한되지 않고, 변수, 함수, 클래스 등의 이름은 모두 식별자라고 부른다.

 

식별자는 네이밍 규칙을 준수해야 하며, 선언(declaration)에 의해 자바스크립트 엔진에 식별자의 존재를 알린다.

 

3. 변수 선언(variable declaration)

변수를 사용하기 위해 생성하는 것을 말한다. 좀 더 풀자면, 값을 저장하기 위한 메모리 공간을 확보(allocate)하고 변수 이름과 확보된 메모리 공간의 주소를 연결(name binding)해서 값을 저장할 수 있게 준비하는 것이다. 변수를 사용하려면 var, let, const 키워드를 사용해 선언해야 한다.

키워드

자바스크립트 코드를 해석하고 실행하는 자바스크립트 엔진이 수행할 동작을 규정한 일종의 명령어

 

자바스크립트 엔진은 변수 선언을 2단계에 거쳐 수행한다.

  1. 선언 단계: 변수 이름을 등록해서 자바스크립트 엔젠에 변수의 존재를 알린다.
  2. 초기화 단계: 값을 저장하기 위한 메모리 공간을 확보하고 암묵적으로 undefined를 할당해 초기화 한다.

변수 이름은 어디에 등록되는가?

모든 식별자는 실행 컨텍스트에 등록된다. 자바스크립트 엔진이 소스코드를 평가하고 실행하기 위해 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다. 자바스크립트 엔진은 실행 컨텍스트를 통해 식별자와 스코프를 관리한다.

변수 이름과 변수 값은 실행 컨텍스트 내에 키(key)/값(value) 형식인 객체로 등록되어 관리된다.

 

초기화(initialization)란 변수가 선언된 이후 최초로 값을 할당하는 것을 말한다. 만약 초기화 단계를 거치지 않으면 확보된 메모리 공간에는 이전에 다른 애플리케이션이 사용했던 값이 남아 있을 수 있다. 이러한 값을 쓰레기 값(garbage value)라 한다. 그러나 자바스크립트의 var 키워드는 암묵적으로 undefined값을 할당하여 초기화를 수행하므로 이러한 위험으로부터 안전하다.

 

4. 변수 선언의 실행 시점과 변수 호이스팅

console.log(score); // undefined

var score; // 변수 선언문

(var는 암묵적으로 undefined를 할당해준다.)

 

위의 결과가 나타난 이유는 변수 선언이 소스코드가 한 줄씩 순차적으로 실행되는 시점, 즉 런타임(runtime)이 아니라 그 이전 단계에서 먼저 실행되기 때문이다. 자바스크립트 엔진은 소스코드를 한 줄씩 순차적으로 실행하기 앞서 먼저 소스코드의 평가 과정을 거치면서 소스코드를 실행하기 위한 준비를 한다. 변수 선언을 포함한 모든 선언문을 소스코드를 찾아내 먼저 실행한다. 덕분에 어디서든지 변수를 참조할 수 있다. 이처럼 변수 선운문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트의 고유의 특징을 변수 호이스팅(variable hoisting)이라 한다.

 

변수 선언뿐 아니라 var, let, const, function, function*, class 키워드를 사용하여 선언하는 모든 식별자는 호이스팅된다. 모든 선언문은 런타임 이전 단계에서 먼저 실행되기 때문이다.

 

5. 값의 할당

변수 값을 할당할 때는 할당 연산자( = )를 사용한다. 이때 유념해야 할 점은, 자바스크립트 엔진은 변수 선언과 값의 할당을 하나의 문으로 단축 표현해도 변수 선언과 값의 할당을 2개의 문으로 나누어 각각 실행한다는 사실이다. 다시 말해, 변수 선언은 소스코드가 순차적으로 실행되는 시점인 런타임 이전에 먼저 실행되지만 값의 할당은 소스코드가 순차적으로 실행되는 시점인 런타임에 실행된다.

 

6. 값의 재할당

재할당이란 이미 값이 할당되어 있는 변수에 새로운 값을 또다시 할당하는 것을 말한다. 이때, 생각할 점은 var 키워드로 선언한 변수는 선언과 동시에 undefined로 초기화되기 때문에 엄밀히 말해서는 재할당이라고 봐야 한다. 재할당됨으로써 이전에 사용했던 값은 필요가 없어진다. 이런 값들은 가비지 콜렉터(garbage collector)에 의해 메모리에서 자동 해제된다. 하지만, 메모리에서 언제 해제될지는 예측할 수 없다.

 

가비지 콜렉터(garbage collector)

애플리케이션이 할당(allocate)한 메모리 공간을 주기적으로 검사하여 더 이상 사용되지 않는 메모리를 해제(release)하는 기능을 말한다. 자바스크립트는 가비지 콜렉터를 내장하고 있는 매니지드 언어로서 가바지 콜렉터를 통해 메모리 누수(memory leak)를 방지한다.

 

만약 값을 재할당할 수 없다면, 그것을 상수(constant)라 한다.

 

7. 식별자 네이밍 규칙

식별자(identifier)는 어떤 값을 구별해서 식별해낼 수 있는 고유한 이름을 말한다. 또한 네이밍 규칙을 준수해야 한다.

  • 특수문자를 제외한 문자, 숫자, 언더스코어(_), 달러 기호($)를 포함할 수 있다.
  • 특수문자를 제외한 문자, 언더스코어(_), 달러 기호($)로 시작해야 한다.
  • 예약어는 식별자로 사용할 수 없다.

변수 이름은 변수의 존재 목적을 쉽게 이해할 수 있도록 의미를 명확회 표현해야 한다. 좋은 변수 이름은 코드의 가독성을 높이고, 주석을 작성하지 않게 만든다.

 

네이밍 컨벤션(naming conventrion)

하나 이상의 영어 단어로 구서된 식별자를 만들 때, 가독성 좋게 단어를 눈에 띄게 구분하기 위해 규정한 명명 규칙을 말한다. 자바스크립트에서는 일반적으로 변수나 함수의 이름에는 카멜 케이스를, 생성자 함수나 클래스의 이름에는 파스칼 케이스를 사용한다.

// 카멜 케이스(camelCase)
var firstName;

// 스네이크 케이스(snake_case)
var first_name;

// 파스칼 케이스(PascalCase)
var FirstName;

13장. 스코프

1. 스코프란?

스코프(scope, 유효범위)는 자바스크립트를 포함한 모든 프로그래밍 언어의 기본적이며 중요한 개념이다. 더욱이 자바스크립트의 스코프는 다른 언어와 구별되는 특징이 있으므로 주의할 필요가 있다. 스코프는 변수 그리고 함수와 깊은 관련이 있다.

 

변수는 자신이 선언된 위치에 의해 자신이 유효한 범위, 즉 다른 코드가 변수 자신을 참조할 수 있는 범위가 결정된다. 모든 실벽자로 동일하다. 자신이 선언된 위치에 의해 다른 코드가 식별자 자신을 참조할 수 있는 유효 범위가 결정된다. 즉, 스코프는 식별자가 유효한 범위를 말한다.

 

자바스크립트 엔진은 스코프를 통해 어떤 변수를 참조해야 할지를 결정한다. 이를 식별자 결정(identifier resolution)이라 한다. 따라서, 스코프란 자바스크립트 엔진이 식별자를 검색할 때 사용하는 규칙이라 할 수 있다. 다시 말해, 코드를 실행할 때 코드의 문맥(context)을 고려한다.

 

코드의 문맥과 환경

코드가 어디서 실행되며 주변에 어떤 코드가 있는지를 '렉시컬 환경(lexical environment)'이라고 부른다. 즉, 코드의 문맥(context)은 렉시컬 환경으로 이뤄진다. 이를 구현한 것이 '실행 컨텍스트(execution context)'이며, 모든 코드는 실행 컨텍스트에서 평가되고 실행된다. 스코프는 실행 컨텕스트와 깊은 관련이 있을 수밖에 없다.

 

네임 스페이스

같은 스코프 내에서 식별자는 유일해야 하지만 다른 스코프에는 같은 이름의 식별자를 사용해도 아무런 문제가 없다. 즉, 스코프는 '네임 스페이스'다.

 

2. 스코포의 종류

코드는 전역(gloval)과 지역(local)으로 구분할 수 있다. 이때 변수는 자신이 선언된 위치에 의해 자신이 유요한 범위인 스코프가 결정된다.

  • 전역이란 코드의 가장 바깥 영역을 말한다. 전역 스코프(global scope)를 만든다. 이렇게 만들어진 전역 변수는 어디서도 참조할 수 있으므로 함수 내부에서도 참조할 수 있다. 즉, 지역 변수에 동일한 식별자가 존재하면 값이 재할당되는 경우도 생길 수 있다.
  • 지역이란 함수 몸체 내부를 만든다. 그리고 지역 스코프(local scope)를 만든다. 지역 변수는 자신의 지역 스코프와 하위 지역 스코프에서 유효하다.

3. 스코프 체인

함수는 전역에서 정의할 수도 있고, 함수 몸체 내부에서 정의할 수도 있다. 즉, 함수는 중첩될 수 있으므로 함수의 지역 스코프도 중첩될 수밖에 없다. 이는 스코프가 함수의 중첩에 의해 계층적 구조를 갖는다는 것을 의미한다. 이렇게 스코프가 계층적으로 연결된 것을 스코프 체인(scope chain)이라 한다. 마치 상속(inheritance)과 유사하다.

 

변수를 참조할 때 자바스크립트 엔진은 스코프 체인을 통해 변수를 참조하는 코드의 스코프에서 시작하여 상위 스코프 방향으로 이동하며 선언된 변수를 검색(identidier resolution)한다. 스코프 체인은 물리적으로 존재한다. 자바스크립트 엔진은 코드를 실행하기에 앞서 렉시컬 환경(Lexical Envirnment)을 실제로 생성한다.

 

렉시컬 환경(Lexical Envirnment)

스코프 체인은 실행 컨텍스트의 렉시컬 환경을 단방향으로 연결(chaining)한 것이다. 전역 렉시컬 환경은 코드가 로드되면 곧바로 생성되고, 함수의 렉시컬 환경은 함수가 호출되면 곧바로 생성된다.

 

4. 함수 레벨 스코프

대부분의 프로그래밍 언어는 모든 코드 블록이 지역 스코프를 만든다. 이러한 특성을 블록 레벨 스코프(block level scope)라 한다. 하지만 var 키워드로 선언된 변수는 오로지 함수의 코드 블록(함수 몸체)만을 지역 스코프로 인정한다. 이러한 특성을 함수 레벨 스코프(function level scope)라 한다.

var x = 1;
if (true) {
  var x = 10;
}

console.log(x); // 10
----------------------------------
var i = 10;
for (let i = 0; i < 5; i++) {
  console.log(i);
}

console.log(i); // 5

 

5. 렉시컬 스코프

자바스크립트는 렉시컬 스코프를 따르므로 함수를 어디서 호출했는지(동적 스코프)가 아니라 함수를 어디서 정의했는지에 따라 상위 스코프를 결정한다. 즉, 함수의 상위 스코프는 언제나 자신이 정의된 스코프다. 이처럼 함수의 상위 스코프는 함수 정의가 실행될 때 정적으로 결정된다.


14장. 전역 변수의 문제점

1. 변수의 생명 주기

변수는 선언에 의해 생성되고 할당을 통해 값을 갖는다. 그리고 언젠가 소멸한다. 즉 생명 주기(life cycle)가 있다. 변수에 생명 주기가 없다면 영원히 메모리 공간을 점유하게 된다.

 

변수는 하나의 값을 저장하기 위해 확보한 메모리 공간 자체 또는 그 메모리 공간을 식별하기 위해 붙인 이름이다. 따라서 변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀(memory pool)에 반환되는 시점까지다. 할당된 메모리 공간은 더 이상 그 누구도 참조하지 않을 때 가비지 콜렉터에 의해 해제되어 가용 메모리 풀에 반환된다.

 

호이스팅은 스코프를 단위로 동작한다. 즉, 변수 선언이 스코프의 선두로 끌어 올려진 것처럼 동작하는데, 전역 변수는 전역 스코프를 갖게 된다. 따라서 코드가 로드되자마자 곧바로 해석되고 실행된다. 명시적 호출 없이 실행되므로 특별한 진입점이 없다.

 

var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다. 이는 전역 변수의 생명 주기가 전역 객체의 생명 주기와 일치한다는 것을 뜻한다. 반면,  지역 변수의 생명 주기는 함수의 생명 주기와 일치한다.

전역 객체

코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 어떤 객체보다도 먼저 생성되는 특수한 객체(window or global)다. 전역 객체는 표준 빌트인 객체, 호스트 객체, var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.

(링크 연결 예정)

 

2. 전역 변수의 문제점

전역 변수를 선언한 의도는 코드 어디서든 참조하고 할당할 수 있는 변수를 사용하겠다는 것이다. 즉, 암묵적 결합(implicit coupling)을 허용하는 것이다. 또한 코드의 가독성은 나빠지고 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.

 

그리고 생명 주기가 긴 만큼 오랜 시간 메모리 리소스를 소비한다. 더욱이 var 키워드는 중복 선언을 허용하므로 변수 이름이 중복될 가능성도 높아진다. 의도치 않은 재할당이 이뤄지는 것이다.

 

전역 변수는 스코프 체인 상에서 종점에 존재한다. 이는 변수를 검색할 때, 전역 변수가 가장 마지막에 검색된다는 것을 말한다.

 

이뿐 아니라 자바스크립트의 가장 큰 문제점 중 하나는 파일이 분리되어 있다 해도 하나의 전역 스코프를 공유하는 데에서 발생한다. 동일한 이름으로 명명된 변수나 함수가 있을 경우 예상치 못한 결과를 야기할 수 있다.

 

3. 전역 변수의 사용을 억제하는 방법

가급적 변수의 스코프 범위를 좁게 만드는 것이 좋다. 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다. 이 방법은 라이브러리 등에 자주 사용된다.

 


15장. let, const 키워드와 블록 레벨 스코프

참고 링크(https://young-taek.tistory.com/188)

  var let const
중복 선언 O X X
재할당 O O X
스코프 함수 레벨  지역 스코프 지역 스코프
선언과 초기화 따로 가능 따로 가능(TDZ 존재) 반드시 동시
호이스팅 O O(블록 안에서) O(블록 안에서)

 

var

ES6를 사용한다면 가급적 var 키워드를 사용하지 않는 것이 좋다. 기존 var를 사용함으로써 발생했던 문제를 해결하기 위해 let과 const가 등장했기 때문이다.

 

무엇보다 함수 레벨 스코프의 특징으로, 함수 외부에서 선언한 변수들은 전역 변수가 된다. 이로 인해, 함수 내부의 블록 레벨 스코프에도 영향을 미칠 수밖에 없다. 즉, 중복 선언의 위험을 야기한다. 문제는 자바스크립트가 중복 선언에 대해 문법 오류로 인식하지 않기에, 예상과 다른 결과를 마주할 수 있다는 점에 있다.

 

또한, var 키워드로 선언한 변수에서는 런타임 이전에 자바스크립트 엔진에 의해 암묵적으로 '선언'과 '초기화'가 한 번에 진행되므로 호이스팅(Hoisting)이 발생한다.

A = 9209;
console.log(A); // 9209
var A;

 

let

let 키워드로 선언한 변수는 '재할당'이 가능하다. 또한, 'var' 키워드와 달리 중복선언할 경우 '문법 에러'가 발생한다. 이때 변수의 스코프는 최대한 좁게 만드는 것이 좋다.

 

1) 변수 선언과 초기화 과정을 따로 작성할 경우, 자비스크립트 엔진은 var 키워드와 달리 변수 선언과 초기화 과정이 따로 작동한다. 그 사이에는 변수를 참조할 수 없는데, 이를 일시적 사각지대(TDZ, Temporal Dead Zone)이라고 한다.

 

2) let 키워드로 선언한 변수도 호이스팅 문제가 발생한다. 하지만 블록 스코프 안에서만 발생한다.

let A = 9209; // 전역 변수
{
  console.log(A); // ReferenceError
  let A = 2; // 지역 변수
}

console.log(A) 가 4를 반환할 거라 예상할 수 있지만, 블록 레벨 안에서는 let A가 호이스팅되기 때문에, 참조 에러(Reference Error)가 발생한다.

 

const

상수를 선언하기 위해 사용하는 키워드이다. 하지만 반드시 상수만을 위해 사용되지는 않는다.

 

1) const 키워드로 선언한 변수는 반드시 선언과 동시에 초기화해야 한다. 그렇지 않으면 문법 에러가 발생한다.

2) 재할당이 금지되어 있다. 재할당을 시도할 경우, 문법 에러가 발생한다.

3) 일반적으로 상수는 대문자로 선언하여 명확히 명시한다. 또한, 스네이크 케이스로 표현한다.

4) const 키워드로 선언한 원시값은 재할당이 불가능하지만, 객체를 할당한 경우에는 값을 변경할 수 있다. 객체는 재할당 없이도 직접 변경 가능하기 때문이다. 즉, const 키워드는 재할당을 금지할 뿐, '불변'을 의미하지는 않는다.

const language = {
  name: "JavaScript"
};

language.name = 'TypeScript';
console.log(language); // { name: "TypeScript" }

참고 자료

모던 자바스크립트 Deep Dive

  • 04장. 변수(p.34-49)
  • 13장. 스코프 (p.189-199)
  • 14장. 전역 변수의 문제점 (p.200-207)
  • 15장. let, const 키워드와 블록 레벨 스코프 (p.208-218)

댓글