변수, 스코프 그리고 호이스팅 in Javascript

Javascript

직전 3개의 포스트는 사실 Javascript의 스코프와 호이스팅을 더 잘 이해하기 위한 Javascipt 엔진의 함수와 변수를 처리하는 실행 컨텍스트에 대해 공부해봤습니다. 사실 이전 (블로그를 리뉴얼하기 전에도) 다룬적 있지만 사실 그땐 음.. 이해했다기 보단 그저 암기를 했다고 생각했습니다. 그렇다면… 이번에는 잘 이해할 수 있을지 궁금합니다.

이번 주제는 공부하면서 참고했던 Tania RasciaUnderstanding Variables, Scope, and Hoisting in JavaScript 를 번역하고 정리해보는 것으로 진행해보겠습니다.

2018년 2월에 게재되었다고 하지만 구독 횟수?가 무려 143.4k 이다…

image-20201019191043253


변수, 스코프, 호이스팅 이해하기

소개

변수 (Variables) 는 많은 프로그래밍 언어에서 기초적인 부분이며 초보 코더(coder)들이 배워야할 첫번째이자 가장 중요한 개념이다.

Javascript에는 변수의 이름을 지을때 따라야하는 몇가지 규칙 뿐만 아니라 많은 다양한 속성들이 있다. Javascript에서 변수를 선언하기 위해 사용되는 키워드로는 var, let 그리고 const 가 있으며 각 키워드들은 코드가 변수를 다르게 해석하도록 영향을 끼친다.

이 튜토리얼은 변수는 무엇이며, 선언과 변수명 정하는 법 그리고 var, let 그리고 const 의 차이점을 면밀히 살펴볼 것입니다. 또한, 호이스팅의 효과와 변수의 동작에 대한 전역 그리고 지역 스코프에 대한 중요성도 살펴볼 것입니다.

변수 이해하기

변수(variable) 란 값을 저장하기 위해 사용되는 이름을 가진 컨테이너이며, 여러번 참조할지 모르는 일부 정보를 나중에 사용하거나 수정하기 위해 변수 안에 저장할 수 있습니다. Javascript에서 변수에 포함된 값은 숫자, 문자열 또는 객체를 포함하는 특정 JavaScript 데아이터 타입 이 될 수 있습니다.

오늘날 Javascript의 바탕이 되는 ECMAScript 2015 (ES6) 언어 스펙 이전에는 오직 var 키워드를 사용하는 것이 변수를 선언하는 유일한 방법이었습니다. 결과적으로 대부분의 낡은 코드와 학습 리소스들은 오직 var 만을 사용할 것입니다. 우리는 var, let, and const의 차이점” 섹션 에서 var, let 그리고 const 키워드의 차이점을 살펴보겠습니다.

변수 자체의 개념을 설명하기 위해서 우리는 var 을 사용할 수 있습니다. 아래의 예제어서 우리는 변수를 선언할 것 이고, 값을 할당할 것 입니다.

1
2
// 문자열 값 Sammy를 username 식별자에 할당합니다.
var username = "sammy_shark";

위 문장은 몇 가지 부분으로 구성되어 있습니다:

  • var 키워드를 사용한 변수 선언
  • username 변수 이름 (또는 식별자(identifier))
  • = 문법으로 표현되는 할당 식별자
  • 할당된 값, sammy_shark

현재 코드상에서 우리는 username 을 사용할 수 있습니다. Javascript는 문자열 값 sammy_shark 을 나타내는 ``username` 을 기억할 것입니다.

1
2
3
4
// 변수가 값과 동일한지 확인해봅시다
if (username === "sammy_shark") {
console.log(true);
}
1
2
// Output
true

이전에 언급했던 것처럼, 변수는 Javascript 데이터 타입으로 나타낼 수 있습니다. 예와 같이 우리는 문자열(String), 숫자(Number), 객체(Object), 논리값(Boolean) 그리고 null 값으로 변수를 선언할 수 있습니다.

1
2
3
4
5
6
7
// 다양한 변수 할당
var name = "Sammy";
var spartans = 300;
var kingdoms = [ "mammals", "birds", "fish" ];
var poem = { roses: "red", violets: "blue" };
var success = true;
var nothing = null;

우리는 console.log 을 사용해 특정 변수에 담긴 값을 볼 수 있습니다.

1
2
// spartans 변수를 console에 전송
console.log(spartans);
1
2
// Output
300

변수는 추후에 접근하거나 수정할 수 있는 메모리에 데이터를 저장하고, 또한 새로운 값으로 재할당하거 지정할 수도 있습니다. 아래의 간단한 예제는 비밀번호를 변수에 저장하고 업데이트 하는 방법을 설명합니다.

1
2
3
4
5
6
7
// password 변수에 값을 할당
var password = "hunter2";

// 새로운 값을 변수 값에 재할당
password = "hunter3";

console.log(password);
1
2
// Output
'hunter3'

변수 이름짓기

변수 이름은 JavaScript에서 식별자 (identifier) 로 알려져있습니다. Understanding Syntax and Code Structure in JavaScript 에서 식별자 이름 짓기에 대한 몇 가지 규칙에 대해 논했으며, 아래와 같이 요약할 수 있습니다:

  • 변수 이름은 오직 문자(a-z), 숫자(0-9), 달러 기호($) 그리고 언더스코어(_)
  • 변수 이름은 어떠한 빈칸(tab 또는 space)도 포함할 수 없습니다.
  • 숫자로 변수 이름을 시작할 수 없습니다.
  • 변수 이름으로 사용할 수 없는 몇 가지 예약어 가 있습니다.
  • 변수 이름은 대소문자를 구분합니다.

또한, JavaScript는 var 또는 let 으로 선언된 함수 또는 변수의 이름에 카멜 케이스(camel case, 때때론 camelCase)를 사용하는 규칙이 있습니다. 이것은 첫번째 글자는 소문자이고 그 이후의 글자는 공백없이 모든 후속 단어의 첫글자를 대문자로 쓰는 관행입니다. 상수(constant)가 아닌 대부분의 변수는 몇 가지 예외와 함께 이 규칙을 따라야합니다. const 키워드로 선언된 상수(constant)인 변수의 이름은 전형적으로 모두 대문자로 작성합니다.

배워야 할 규칙들이 많아보이겠지만, 이와 같은 관습들은 유효하고 규칙에 맞는 변수 이름을 작성하기 위한 제 2의 본성이 매우 빠르게 될 것입니다.

var, let, and const 차이점

JavaScript는 변수를 선언하기 위해 언어의 복잡성을 더하는 세개의 다른 키워드가 있습니다. 3개의 키워드 차이점은 스코프(scope), 호이스팅(hoisting) 그리고 재할당(reassignment)에 바탕을 두고 있습니다.

Keyword Scope Hoisting Can Be Reassigned Can Be Redeclared
var Function scope Yes Yes Yes
let Block scope No Yes No
const Block scope No No No

여러분은 자신의 프로그램에 세 가지중 어떤 것을 사용해야만 하는지 궁금해할지도 모릅니다. 흔히, 받아들여지는 관행은 가능하 한 const 를, 반복문과 재할당의 경우에는 let 를 사용하도록 하는 것입니다. 일반적으로, var 은 작동하고 있는 레거시 코드 외부에서 피할 수 있습니다.

Variable Scope (변수 스코프)

JavaScript에서 Scope 는 JavaScript에게 변수 접근성을 결정하는 코드의 현재 컨텍스트를 참조합니다. 스코프에는 두가지 타입으로, 지역(local)전역(global) 이 있습니다.

  • Global variables : 블록 밖에 선언된 변수
  • Local variables : 블록 안에 선언된 변수

아래 예제처럼, 우리는 전역 변수를 생성할 수 있다.

1
2
// 전역변수 초기화
var creature = "wolf";

우리는 변수가 재할당 가능하다고 배웠습니다. 지역 변수를 사용함으로써 우리는 원래 값을 변경하거나 재할당 없이 외부 스코프에 있는 변수와 같은 이름으로 새로운 변수를 생성할 수 있습니다.

아래 예제에서 우리는 전역 species 변수를 생성했고, 함수 안에 같은 이름으로 지역 변수가 또 있습니다. 이 코드를 콘솔에 전송함으로써 우리는 변수의 값이 스코프에 따라 어떻게 다르며, 원래 값은 변하지 않는다는 것을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
// Initialize a global variable
var species = "human";

function transform() {
// Initialize a local, function-scoped variable
var species = "werewolf";
console.log(species);
}

// Log the global and local variable
console.log(species);
transform();
console.log(species);
1
2
3
4
// Output
human
werewolf
human

예제에서, 지역 변수는 함수 스코프(function-scoped) 이다. var 키워드로 선언된 변수들은 구분된 스코프를 가짐으로써 변수들이 함수를 인지한다는 의미에서 항상 함수 스코프이다. 따라서, 이러한 지역적 스코프 변수는 전역 스코프에서 접근할 수 없다.

하지만 새로운 키워드인 letconst블록 스코프(block-scoped) 인데, 함수 블록, if 구문 그리고 forwhile 반복문을 포함한 특정 종류의 블록으로 생성된 새로운 지역 스코프를 의미한다.

함수 스코프(function-scoped)와 블로 스코프(block-scoped) 내 변수의 차이를 형상화하기 위해서 let 을 사용해 if 블록 안에 새로운 변수를 할당할 것 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
var fullMoon = true;

// Initialize a global variable
let species = "human";

if (fullMoon) {
// Initialize a block-scoped variable
let species = "werewolf";
console.log(`It is a full moon. Lupin is currently a ${species}.`);
}

console.log(`It is not a full moon. Lupin is currently a ${species}.`);
1
2
3
// Output
It is a full moon. Lupin is currently a werewolf.
It is not a full moon. Lupin is currently a human.

위 예에서 보듯이, species 변수는 전역적으로 하나의 값(human)만, 지역적으로 werewolf 라는 다른 값을 가집니다. 만약 우리가 var 을 사용했다면, 다른 결과를 만들 것입니다.

1
2
3
4
5
6
7
8
9
10
// Use var to initialize a variable
var species = "human";

if (fullMoon) {
// Attempt to create a new variable in a block
var species = "werewolf";
console.log(`It is a full moon. Lupin is currently a ${species}.`);
}

console.log(`It is not a full moon. Lupin is currently a ${species}.`);
1
2
3
// Output
It is a full moon. Lupin is currently a werewolf.
It is not a full moon. Lupin is currently a werewolf.

결과적으로, 전역 변수와 블록 스코프 변수 둘다 werewolf 라는 같은 값으로 끝났습니다. 이것은 var 로 새로운 지역 변수를 생성하는 것 대신에, 같은 스코프안에서 같은 변수에 재할당했기 때문입니다. varif 조건문을 다른 부분으로 즉, 새로운 스코프로 인식하지 않습니다. 의도치 않게 변수 값을 덮어쓰는 것과 같은 코드를 덜 생산하기 위해, 일반적으로 블록 스코프로 변수를 선언하기를 추천합니다.

Hoisting (호이스팅)

지금까지 대부분의 예제에서 우리는 변수를 선언하기 위해 var 을 사용했었고, 값으로 초기화 했습니다. 선언과 초기화 이후에 우리는 값을 접근하거나 재할당할 수 있습니다.

만약 우리가 변수가 선언되고 초기화되기 전에 사용하려고 시도한다면, 이것은 undefined 를 반환할 것입니다.

1
2
3
4
5
// 변수가 선언되기 전에 사용 시도
console.log(x);

// 변수 할당
var x = 100;
1
2
// Output
undefined

그러나, 만약 우리가 var 키워드를 생략했다면, 우리는 더이상 변수를 선언할 수 없으며 오직 초기화만 할 수 있을 것입니다. 이것은 ReferenceError 를 반환할 것이고 스크립트의 실행을 중단합니다.

1
2
3
4
5
// Attempt to use a variable before declaring it
console.log(x);

// Variable assignment without var
x = 100;
1
2
// Output
ReferenceError: x is not defined

이런 이유는 호이스팅(hoisting) 으로, 변수와 함수 선언을 해당 스코프의 가장 위로 옮기는 Javascript의 동작입니다. 초기화가 아닌 실제 선언만 끌어올려지기(hoist) 때문에 첫 번째 예제의 값은 undefined 를 반환합니다.

이 개념을 더 확실하게 설명하기 위해, Javascript가 실제로 어떻게 해석하는지 아래의 코드에 작성해보았습니다.

1
2
3
4
5
6
7
8
// 우리가 작성한 코드
console.log(x);
var x = 100;

// JavaScript가 해석한 방법
var x;
console.log(x);
x = 100;

JavaScript는 스크립트를 실행하기 전에 변수로써 x 를 메모리에 저장한다. 정의되기 전에 여전히 x 를 호출하기 때문에 결과는 100 아닌 undefined 이다. 하지만, ReferenceError 가 발생하지 않고 스크립트가 중단된다. 비록 var 키워드가 실제로 var 의 위치를 변경하지는 않았지만, hoisting의 작동 장식에 대한 유용한 코드입니다. 그러나 이 코드를 작성한 프로그래머가 x의 결과값이 undefined 일때, true 일 것이라고 예상하기 때문에, 이러한 동작은 이슈를 유발할 수 있습니다.

또한 다음 예제에서 우리는 호이스팅이 예상치 못한 결과를 어떻게 유발할 수 있는지 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
// 전역 스코프에 변수 x 초기화
var x = 100;

function hoist() {
// 코드 결과에 아무런 영향을 주지 않아야하는 조건문
if (false) {
var x = 200;
}
console.log(x);
}

hoist();
1
2
// Output
undefined

이 예제에서, 우리는 전역적으로 x100 이 되도록 선언했습니다. if 선언문에 따라 x200 으로 변할수도 있었지만 그 조건문이 false 이기 때문에 x 의 값에 영향을 주지 못했습니다. 대신에 xhoist() 함수의 가장 꼭대기로 끌어올려지고, 값은 undefined 가 됐습니다.

이러한 예상치 못한 작동의 유형은 잠재적으로 프로그램에 버그들을 유발할 수 있습니다. letconst는 블록 스코프(block-scoped)이기 때문에, 아래와 같이 이러한 문법에서는 끌어올려지지 않을 것입니다.

1
2
3
4
5
6
7
8
9
10
11
12
// Initialize x in the global scope
let x = true;

function hoist() {
// Initialize x in the function scope
if (3 === 4) {
let x = false;
}
console.log(x);
}

hoist();
1
2
// Output
true

var 로 가능한 변수 중복 선언은 letconst 에서는 에러를 던질 것입니다.

1
2
3
4
5
// var 으로 선언된 변수 덮어쓰기 시도
var x = 1;
var x = 2;

console.log(x);
1
2
// Output
2
1
2
3
4
5
// let 으로 선언된 변수 덮어쓰기 시도
let y = 1;
let y = 2;

console.log(y);
1
2
// Output
Uncaught SyntaxError: Identifier 'y' has already been declared

요약하자면, var 로 도입된 변수는 메모리에 변수 선언을 저장하는 Javascript 메커니즘인 호이스팅에 의해 영향을 잠재적으로 받게 됩니다. 이러한 메커니즘은 정의되지 않은 변수가 생길 수 있습니다. letconst 의 도입은 변수를 선언하기 전 사용하려고 하거나 한번 이상 선언하려고 할때 에러를 던짐으로써 이러한 이슈를 해결해줍니다.

Constants (상수)

많은 프로그래밍 언어는 값을 수정하거나 변경할 수 없는 상수 (constant) 라는 기능이 있습니다. Javascript에서 const 식별자는 상수(Constants) 이후에 따라서 만들어졌으며, const 에 할당된 값은 재할당할 수 없습니다.

모든 const 식별자 대문자로 작성하는 것이 일반적인 규범이며 다른 변수 값과 쉽게 구별할 수 있도록 표시합니다.

아래의 예제에서, 우리는 const 키워드로 상수로써 SPECIES 변수를 초기화했고, 변수를 재할당하려고 할때 결과가 에러일 것입니다.

1
2
3
4
5
6
7
// const에 값 할당
const SPECIES = "human";

// 값 재할당 시도
SPECIES = "werewolf";

console.log(SPECIES);
1
2
// Output
Uncaught TypeError: Assignment to constant variable.

const 값은 재할당할 수 없기 때문에, 선언과 초기화가 동시에 될 필요가 있거나 에러를 던질 것입니다.

1
2
3
4
// const 초기화 없이 선언
const TODO;

console.log(TODO);
1
2
// Output
Uncaught SyntaxError: Missing initializer in const declaration

프로그래밍에서 변경할 수 있는 변수는 mutable (변경 가능한 또는 변하기 쉬운) 인 반면에, 변경할 수 없는 값들은 immutable(불변의) 로 알려져 있습니다. 비록 const 값은 재할당할 수 없지만, const 로 선언된 객체의 속성을 수정하는 것은 변경 가능(mutable)하다.

1
2
3
4
5
6
7
8
9
10
// CAR 객체에 두개의 속성 생성
const CAR = {
color: "blue",
price: 15000
}

// CAR 속성 수정
CAR.price = 20000;

console.log(CAR);
1
2
// Output
{ color: 'blue', price: 20000 }

상수(Constants)는 프로젝트에서 미래의 여러분 자신과 다른 개발자들에게 변수가 재할당될 수 없는 의도를 명확히 하는데 유용합니다. 만약 미래에 변수가 수정될지도 모른다고 예상한다면, 여러분은 대신에 let 을 사용해 변수를 선언하기를 원할 것입니다.

Conclusion

튜토리얼에서, 우리는 변수는 무엇이며 변수명 짓기의 규칙과 변수값을 할당하는 법에 대해 살펴봤으며 또한, 스코프(scope) 와 호이스팅(hoisting), var 키워드의 몇가지 한계 뿐만 아니라 letconstvar 키워드에 의한 이슈들을 해결하는 방법에 대해서도 학습해봤습니다.


번역을 마치며

이상 Javascript의 변수와 지역/전역 스코프에 따른 변수 처리 그리고 변수를 선언하기 위한 var, let 그리고 const 키워드의 차이와 각 선언 키워드의 스코프에 대해서 배워봤습니다.

다음으로는 ko.javascript.info의 closure 파트의 연습문제를 풀어보며 Javascript의 lexical Environment, 스코프, 호이스팅 그리고 var, let, const 에 대해 복습해보도록 하겠습니다.

본 포스트는 Tania RasciaUnderstanding Variables, Scope, and Hoisting in JavaScript 를 번역했음을 알려드립니다.