Typescript 변수 선언

letconst는 자바스크립트에서 비교적 새로운 변수 선언 방법 입니다. letvar는 비슷하지만 자바스크립트에서 흔한 “gotchas”https://en.wikipedia.org/wiki/Gotcha_(programming) 를 피할 수 있게 해 줍니다. constlet에 변수에 재할당을 막도록 확장된 것입니다.

TypeScript는 자바스크립트의 superset으로 letconst를 지원합니다. 이 두가지 새로운 선언에 대해서 자세히 알아 보고 왜 var보다 더 바람직한지 설명할 것입니다.

var 선언

자바스크립트에서 변수를 선언은 var 키워드로 해왔습니다.

1
var a = 10;

함수 안에서도 변수를 선언할 수 있습니다.

1
2
3
4
function f() {
var message = "Hello, world!";
return message;
}

그리고 다른 함수에서 이 변수들에 접근할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
function f() {
var a = 10;
return function g() {
var b = a + 1;
return b;
}
}
var g = f();
console.log(g()); // 11을 출력

위 예에서 gf에서 선언된 변수 a를 캡처(capture)합니다. g가 호출될때 a의 값은 f안의 a의 값에 묶입니다. f가 실행을 완료했어도 g가 호출되면 a에 접근하고 수정할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function f() {
var a = 1;
a = 2;
var b = g();
a = 3;
return b;
function g() {
return a;
}
}
console.log(f()); // 2를 출력

Scoping rules

var 선언은 이상한 scoping rules를 가집니다. 다음 예제를 보면:

1
2
3
4
5
6
7
8
function f(shouldInitialize: boolean) {
if (shouldInitialize) {
var x = 10;
}
return x;
}
console.log(f(true)); // 10
console.log(f(false)); // undefined

변수 xif 블럭(block)안에서 선언되었지만 블럭 밖에서도 접근할 수 있습니다. var 선언을 포함하는 블럭과는 관계 없이 그것을 감싸고 있는 함수, 모듈(module), 네임스페이스(namesapce) 또는 global scope의 어디에서나 접근이 가능합니다. 어떤 사람들은 이것을 var-scoping 또는 function-scoping라고 부릅니다. 파라미터(parameter)도 또한 function-scoping을 따릅니다.

이러한 scoping rule은 몇몇 형태의 실수를 유발할 수 있습니다. 그중 하나는 변수를 여러번 선언해도 에러가 아니라는 사실이다.

1
2
3
4
5
6
7
8
9
10
function sumMatrix(matrix: number[][]) {
var sum = 0;
for (var i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (var i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

어쩌면 몇몇은 찾아내기 쉬울 수도 있습니다. i는 동일한 function-scope 변수이기 때문에 안쪽의 for-루프는 뜻하지 않게 변수 i를 덮어 씁니다.
이런 버그는 코드리뷰에서도 잡기 어렵습니다.

변수 캡처의 이상한 점(variable capturing quirks)

1
2
3
for (var i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}

setTimeout은 일정 시간 후에 함수를 실행합니다.

실행 결과는 이렇습니다.

1
2
3
4
5
6
7
8
9
10
10
10
10
10
10
10
10
10
10
10

많은 자바스크립트 개발자는 이 동작을 잘 알고 있지만, 대부분의 사람들은 출력이 다음과 같을 거라고 기대합니다.

1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

setTimeout에 전달하는 모든 함수 표현식은 실제로 동일한 범위의 동일한 i를 참조합니다.

setTimeout은 일정 시간 후에 함수를 실행하지만 for 루프가 실행을 중지 한 후에만 실행됩니다. for 루프가 실행을 멈출 때 i의 값은 10입니다. 따라서 주어진 함수가 호출될 때마다 10을 출력합니다.

일반적인 해결 방법은 즉시 호출 함수 표현인 IIFE(Immediately Invoked Function Expression)을 사용해서 각 반복에서 i를 캡처하는 것입니다.

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
// capture the current state of 'i'
// by invoking a function with its current value
(function(i) {
setTimeout(function() { console.log(i); }, 100 * 1);
})(i);
}

이 특이한 패턴은 실제로 매우 일반적으로 사용됩니다.

let 선언

지금까지 var 키워드는 몇가지 문제가 있다는 것을 알게 되었고 그것이 let가 도입된 이유입니다.

1
let hello = "Hello!";

Block-scoping

let을 사용해서 변수를 선언하면 lexical-scoping 또는 block-scoping을 사용합니다. 감싸고 있는 함수 전체로 scope를 넓히는 var로 선언된 변수와를 다르게 block-scope 변수는 가장 가까운 포함 블럭 또는 for루프 외부에서는 볼 수 없습니다.

1
2
3
4
5
6
7
8
9
function f(input: boolean) {
let a = 100;
if (input) {
let b = a + 1;
return b;
}
return b;
}

ab는 로컬 변수입니다. a의 scope는 f의 바디로 제한되는데 비해 b의 scope는 if 문의 블럭으로 제한됩니다.

catch절 안에서 선언된 변수도 비슷한 scoping 룰이 적용됩니다.

1
2
3
4
5
6
7
8
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
console.log(e);

블럭 범위 변수의 또 다른 특성은 실제로 선언되기 전에 읽거나 쓸 수 없다는 것입니다.

1
2
a++;
let a;

주의해야 할 점은 블럭 범위가 선언되기 전에 변수를 캡처할 수 있다는 것입니다. 단, 선언되기 전에 함수를 호출하는 것은 안됩니다. TypeScript에서는 에러가 발생하지는 않지만 ES2015에서는 실행할때 ReferenceError 예외가 발생합니다.

1
2
3
4
5
function foo() {
return a;
}
foo();
let a;

Re-declarations and Shadowing

var 선언은 동일한 이름으로 여러번 선언해도 에러가 발생하지 않습니다.

1
2
3
4
5
6
7
function f(x) {
var x;
var x;
if (true) {
var x;
}
}

위 예에서 모든 x 선언은 실제로 동일한 x를 참조합니다. 이것은 종종 버그의 원인이 됩니다.
let은 다릅니다.

1
2
let x = 10;
let x = 20; // 에러: 동일한 scope에서는 다시 선언할 수 없습니다.
1
2
3
4
5
6
7
8
function f(x) {
let x = 100; // 에러: 파라미터 선언과 간섭을 일으킵니다.
}
function g() {
let x = 100;
var x = 100; // 에러
}

블럭을 지정해 주면 동일한 이름으로 변수를 선언할 수 있습니다.

1
2
3
4
5
6
7
8
9
function f(condition:boolean, x:number) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // reutrn 0
f(true, 0); // reutrn 100

안쪽 scope에 새로 선언하는 것을 shadowing라고 합니다. 어떤 버그는 막을 수 있지만 또 다른 버그를 만들어 낼 수 있습니다.
예를 들어 sumMatrix 함수를 let으로 다시 작성했다고 생각해 봅시다.

1
2
3
4
5
6
7
8
9
10
function sumMatrix(matrix: number[][]) {
let sum = 0;
for (let i = 0; i < matrix.length; i++) {
var currentRow = matrix[i];
for (let i = 0; i < currentRow.length; i++) {
sum += currentRow[i];
}
}
return sum;
}

이 버전은 안쪽 i가 바깥 루프의 i를 가려서 정확하게 합산을 수행할 수 있습니다.

더 명확한 코드를 작성하기 위해 shadowing은 일반적으로 피해야 합니다.

Block-scoped variable capturing

var 선언으로 변수 캡처의 개념을 처음 접했을 때 일단 캡처 된 변수가 어떻게 작동하는지 간략하게 살펴 보았습니다. 이것에 대한 더 나은 직감을주기 위해 스코프가 실행될 때마다 변수의 “환경”을 생성합니다. 해당 환경 및 캡처 된 변수는 범위 내의 모든 항목이 완료된 후에도 존재할 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
function theCityThatAlwaysSleeps() {
let getCity;
if (true) {
let city = "Seattle";
getCity = function() {
return city;
}
}
return getCity();
}

환경안에서 city를 캡처했으므로 if블럭이 실행을 완료했음에도 불구하고 여전히 액세스 할 수 있습니다.

이전의 setTimeout 예제에서는 for 루프를 반복 할 때마다 변수의 상태를 캡처하기 위해 IIFE를 사용해야한다는 결론을 얻었습니다. 사실 우리가 수행 한 작업은 캡처 된 변수에 대한 새로운 변수 환경을 만드는 것이 었습니다. 그것은 약간의 고통 이었지만, 다행스럽게도 TypeScript에서는 다시 할 필요가 없습니다.

루프의 일부로 선언될 때 let 선언은 철저하게 다른게 동작합니다.
루프 자체에 새로운 환경을 도입하기보다는 반복마다 새로운 scope를 만듭니다.
지금까지는 IIFE로 해왔으나 setTimeout 예제를 let 선언을 사용해서 바꿀 수 있습니다.

1
2
3
for (let i = 0; i < 10; i++) {
setTimeout(function() { console.log(i); }, 100 * i);
}
1
2
3
4
5
6
7
8
9
10
0
1
2
3
4
5
6
7
8
9

const 선언

1
const numLivesForCat = 9;

let과 비슷하나 값을 바꿀 수 없습니다. 다시 말해서 동일한 scope 내에서 다시 할당 할 수 없습니다.

이것은 참조하는 값이 불변이라는 생각과 혼동되어서는 안됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const numLivesForCat = 9;
const kitty = {
name: "Aurora",
numLives: numLivesForCat
}
// 에러
kitty = {
name: "Danielle",
numLives: numLivesForCat
}
kitty.name = "Rory";
kitty.name = "Kitty";
kitty.name = "Cat";
kitty.numLives--;

이를 피하기 위해 특별한 조치를 취하지 않으면 const 변수의 내부 상태가 여전히 수정 가능합니다. 다행히도 TypeScript를 사용하면 객체의 멤버를 읽기 전용으로 지정할 수 있습니다. 인터페이스 장에서 자세히 설명합니다.

Share