TypeScript 핸드북 2 - 변수 선언

변수 선언

변수 선언

ECMAScript6에 letconst라는 두개의 새로운 타입의 변수 선언자가 추가 되었습니다. 이전에 언급했듯이, let은 어떤면에서는 var와 유사하지만 사용자가 JavaScript로 실행하는 일반적인 “gotchas”를 피할 수 있습니다. const는 변수에 재할당하는 것을 막는다는 점에서 let을 보완한 것입니다.

TypeScript가 JavaScript의 상위 집합체이기 때문에 TypeScript는 당연히 letconst를 지원합니다. 이러한 새로운 선언자들을 조금더 자세히 설명하고 왜 var 선언자보다 바람직한지 자세히 설명 하겠습니다.

만약 여러분이 JavaScript를 offhandedly하게 사용한 적이 있다면, 다음에서 설명할 내용은 여러분이 경험했던 문제들을 해결하는 좋은 방법이 될 수있습니다. 그리고, JavaScript에서 var 선언의 모든 단점을 잘 알고 있다면 여기서 설명하는 내용은 건너 뛰어도 됩니다.

var 선언자

전통적으로 JavaScript에서 변수를 선언하는 방법은 var 선언자를 이용하는 방법입니다.

1
var a = 10;

위 코드는 10의 값을 가진 변수 a를 선언합니다. 아래와 같이 함수 안에서 변수를 선언할 수도 있습니다.

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();
g(); // returns '11'

위의 예제에서 함수 g는 함수 f에서 선언된 변수a를 사용합니다. 함수 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;
}
}
f(); // returns '2'

Scope 규칙

var 키워드를 이용한 변수 선언은 C기반의 다른 언어에 익숙한 사람들에게는 약간 어색할 수 있습니다.
다음 예제를 보겠습니다.

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

몇몇분은 위 예제를 보고 놀랄 수도 있습니다. 위 코드에서 변수 xif 블록 내에서 선언되었지만 그 블록 외부에서 변수에 접근할 수있었습니다. 왜냐하면 var 선언은 포함 함수, 모듈, 네임 스페이스 또는 전역 범위 (모든 요소는 포함된 블록에 관계없이)에서 액세스할 수 있기 때문입니다. 어떤 사람들은 이러한 방식을 var-scoping 또는 Function-scoping라고 표현합니다. 함수의 Parameter도 Function Scope입니다.

이러한 Scope 규칙은 여러가지 실수와 버그를 유발할 수 있습니다. 아래의 코드를 보듯이 그 중 하나는 동일한 변수를 여러 번 선언하는 것이 오류가 아니라는 것입니다.

1
2
3
4
5
6
7
8
9
10
11
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-loop 는 우연히 변수 i 덮어 씁니다. 위 코드에서 보았듯이 경험이 많은 개발자라면 대부분 알고 있겠지만 위와 비슷한 종류의 버그는 코드리뷰에서 많은 논란을 일으킵니다.

Variable capturing quirks

잠깐 시간을내어 다음 스니펫의 결과가 무엇인지 추측 해보십시오.

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

JavaScript의 setTimeout 함수는 특정 밀리초 후에 지정한 함수를 실행합니다.

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

많은 JavaScript 개발자는 코드의 실행 결과가 위와 같다는 것을 알고 있지만, 대부분의 사람들은 출력이 아래와 같을 거라 생각합니다.

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

코드에서 setTimeout에 전달하는 함수 표현식은 실제로 동일 Scope에서 같은 i를 참조합니다. 그게 무슨 뜻인지 잠시 생각해 봅시다. setTimeout은 몇 밀리초 후에 함수를 실행하지만, for loop가 실행을 멈춘 후에 실행됩니다. for loop가 실행을 마치면 i의 값은 10입니다. 따라서 주어진 함수가 호출 될 때마다 10이 출력됩니다!

일반적인 해결 방법은 각 반복마다 i를 캡처하는 IIFE(Immediately Invoked Function Expression : 함수를 바로 호출 하는 표현식)를 사용하는 것입니다.

1
2
3
4
5
6
7
for (var i = 0; i < 10; i++) {
// 'i'의 현재 상태를 캡처
// 현재 값으로 함수를 호출
(function(i) {
setTimeout(function() { console.log(i); }, 100 * i);
})(i);
}

위에 예를 든 이상하게 보이는 코드는 실제로 매우 일반적인 사용 패턴입니다. Parameter list에 있는 i는 실제로 for loop에서 선언된 변수 i의 그림자와 같습니다. 그리고 같은 이름을 사용했기 때문에 loop 본문을 많이 수정하지 않아도 됩니다.

let 선언자

지금까지 var에는 몇 가지 문제가 있다는 것을 설명 했습니다. 이것이 let문이 도입된 이유입니다. 사용된 선언자 키워드는 틀리지만 let 선언자와 var 선언자는 동일한 방식으로 작성됩니다

1
let hello = "Hello!";

중요한 차이점은 구문에있는 것이 아니라 의미에 관한 것입니다. 이제 이 내용을 살펴 보겠습니다.

Block-scoping

변수가 let 선언자 키워드을 사용하여 선언되면, Lexical-scope 또는 Block-scope로 불리는 Scope를 사용합니다. var 키워드로 선언된 변수는 자신을 포함하는 Scope 함수 외부에 노출 되는 것과 달리 Block-Scope 변수는 자신을 포함하는 가장 가까운 블록 외부 또는 for loop 외부에서 엑세스할 수 없습니다.

1
2
3
4
5
6
7
8
9
10
11
12
function f(input: boolean) {
let a = 100;
if (input) {
// 'a'를 참조할 수 있습니다.
let b = a + 1;
return b;
}
// Error: 'b'는 여기에 존재하지 않습니다.
return b;
}

위 예제에는 두 개의 지역 변수 인 ab가 있습니다. a의 Scope은 함수 f의 본문으로 제한되며 b의 Scope은 if 문 블록으로 제한됩니다.

catch 절에서 선언된 변수도 비슷한 범위 지정 규칙을가집니다.

1
2
3
4
5
6
7
8
9
try {
throw "oh no!";
}
catch (e) {
console.log("Oh well.");
}
// Error: 'e'는 여기에 존재하지 않습니다.
console.log(e);

Block-scope 변수의 또 다른 특징은 실제로 선언되기 전에 읽거나 쓸 수 없다는 것입니다. 변수의 선언이 모두 자신의 Temporal dead zone의 일부를 가리킬 때까지 Scope 내에서 “존재” 합니다. 이것은 let 선언문 이전에 변수에 접근할 수 없고 TypeScript는 이러한 내용을 알려 줍니다.

1
2
a++; // 선언전에 'a'를 사용할 수 없습니다.
let a;

주의해야 할 점은 Block-scope의 변수의 선언전에 캡처를 할 수 있다는 것입니다. 그리고 ES2015에서는 이러한 캡처가 함수를 호출하는 시점에 오류를 발생시키지만 지금의 TypeScript는 이러한 방식을 허용하고 있으며 에러를 발생하지 않습니다.(TypeScript에서는 var로 변환하기 때문에)

1
2
3
4
5
6
7
8
9
10
function foo() {
// okay to capture 'a'
return a;
}
// 'a' 선언전에 함수 foo를 호출 하는건 잘못됐습니다.
// runtime시에 error를 발생시킬 것입니다.
foo();
let a;

Temporal dead zone에 대한 자세한 내용은 Mozilla 개발자 네트워크의 관련 내용을 참조하십시오.

재선언 과 Shadow

var 선언자를 이용한 변수선언은 선언 횟수가 중요하지 않습니다.

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

위의 예제에서 x의 모든 선언은 실제로 같은 x를 참조하며 이것은 완벽하게 유효합니다. 이런 방식의 코딩은 종종 버그의 원인이되곤 합니다. 하지만 let 선언자를 이용한 변수 선언은 이러한 방식을 허용하지 않습니다.

1
2
let x = 10;
let x = 20; // error: 같은 Scope 내에서 변수 'x'를 재선언할 수 없습니다.

아래 예제와 같이 Block-scope 변수가 아니어도 변수의 재선언시 Typescript에서 문제가 있음을 알려줍니다.

1
2
3
4
5
6
7
8
function f(x) {
let x = 100; // error: parameter 변수 선언에 간섭하고 있습니다.
}
function g() {
let x = 100;
var x = 100; // error: 변수 'x'를 두개 선언할 수 없습니다.
}

하지만 Block-scope 변수가 Function-scope에 절대로 선언 될 수 없다는 말은 아닙니다. 블록 범위 변수는 뚜렷하게 다른 블록 내에서 선언되어야만 합니다.

1
2
3
4
5
6
7
8
9
10
11
function f(condition, x) {
if (condition) {
let x = 100;
return x;
}
return x;
}
f(false, 0); // returns '0'
f(true, 0); // returns '100'

중첩된 Scope에 기존의 변수 이름을 사용하는 것을 Shadow라고합니다. 하지만 우발적으로 Shadow를 사용할 경우 버그를 유발할 수 있기 때문에 양날의 검일수 있습니다. 아래의 예제는 let 변수를 사용하여 이전에 작성했던 sumMatrix 함수를 다시 작성했습니다.

1
2
3
4
5
6
7
8
9
10
11
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;
}

이 버전의 for loop는 실제로 내부 for loopi가 외부 for loopiShadow 하기 때문에 실제로 합계를 올바르게 수행합니다.

일부 코드에서 Shadow가 필요할 수도 있지만 일반적으로 더 명확한 코드를 작성하기 위해 Shadow는 피해야합니다.

Block-scoped variable capturing

var 선언자로 변수 캡쳐를 했을때 캡처된 변수가 어떻게 작동하는지 간단히 살펴 보았습니다. 조금더 자세히 설명하면, Scope가 실행될 때마다 변수의 “environment”을 생성합니다. Scope 내의 모든 것이 실행을 마친 후에도 “environment”와 캡처된 변수가 존재할 수 있습니다.

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

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

이전의 setTimeout 예제에서는for loop가 반복 될 때마다 변수의 상태를 캡처하기 위해 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 선언자

const 선언은 변수(상수)를 선언하는 또 다른 방법입니다.

1
const numLivesForCat = 9;

const 선언은 let 선언과 같지만, 그 이름이 암시 하듯이 값이 초기화 되면 값을 변경할 수 없습니다. 즉, let 과 동일한 범위 “Block-scope” 규칙을 갖지만 다시 할당할 수는 없습니다. 하지만 const 변수들이 참조하는 값이 불변일꺼라 생각하면 안됩니다.

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

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

let vs. const

비슷한 “Block-scope” 유형을 가진 두 가지 선언이 있다고 가정할 때, letconst중 어떤 것을 사용할지 스스로 선택해야 합니다. 간단히 힌트를 드리자면 다음과 같습니다.

최소 권한의 원칙(Principle of least privilege)을 적용하면 수정하려는 모든 선언은 const를 사용해야 합니다. 변수에 write 할 필요가 없는 경우, 같은 코드를 이용하여 협업하는 사람들이 자동으로 객체에 값을 쓰지 않아야 하는 경우, 변수에 실제로 재할당이 필요하는지 등의 여부를 고려해야합니다. const를 사용하면 데이터 흐름에 대해 추론할 때 코드를 더 예측 가능하게 만듭니다.

많은 사용자들은 간결함을 선호할 것 이기 때문에 var는 더이상 사용하지 않고 let을 사용할 것입니다. 이 핸드북의 대부분은 let 선언문을 사용합니다.

Destructuring

TypeScript에있는 또 다른 ECMAScript2015 기능은 Destructuring입니다. 좀더 자세한 정보는 Mozilla Developer Network의 기사를 참조하십시오. 이 섹션에서는 간단한 개요를 제공합니다.

Array destructuring

가장 간단한 형태의 Destructuring 는 배열의 비구조화 할당입니다.

1
2
3
4
let input = [1, 2];
let [first, second] = input;
console.log(first); // outputs 1
console.log(second); // outputs 2

위 코드는 firstsecond라는 두 개의 새로운 변수를 만듭니다. 이는 인덱스를 이용하는 것과 동일하지만 훨씬 편리합니다.

1
2
first = input[0];
second = input[1];

Destructuring은 이미 선언된 변수와 함께 작동합니다.

1
2
// swap variables
[first, second] = [second, first];

함수에 대한 Parameter에 사용하면 다음과 같습니다.

1
2
3
4
5
function f([first, second]: [number, number]) {
console.log(first);
console.log(second);
}
f([1, 2]);

... 구문을 사용하여 목록의 나머지 항목에 대한 변수를 만들 수 있습니다.

1
2
3
let [first, ...rest] = [1, 2, 3, 4];
console.log(first); // outputs 1
console.log(rest); // outputs [ 2, 3, 4 ]

물론 이것은 JavaScript이므로 관심이 없는 나머지 요소는 무시할 수 있습니다.

1
2
let [first] = [1, 2, 3, 4];
console.log(first); // outputs 1

또는 다른 요소들

1
let [, second, , fourth] = [1, 2, 3, 4];

Object destructuring

객체를 Destructuring 할 수도 있습니다.

1
2
3
4
5
6
let o = {
a: "foo",
b: 12,
c: "bar"
};
let { a, b } = o;

그러면 o.ao.b에서 새로운 변수 ab가 생성됩니다. 필요하지 않다면 c는 건너 뛸 수 있습니다.

배열 Destructuring과 마찬가지로 선언 없이 할당할 수 있습니다.

1
({ a, b } = { a: "baz", b: 101 });

이 문장을 괄호로 묶어야한다는 것을 주목하십시오. JavaScript는 일반적으로{를 블록의 시작으로 구문 분석합니다.

... 구문을 사용하여 객체의 나머지 항목에 대한 변수를 만들 수 있습니다.

1
2
let { a, ...passthrough } = o;
let total = passthrough.b + passthrough.c.length;

Property 재명명(renaming)

Property에 다른 이름을 지정할 수도 있습니다.

1
let { a: newName1, b: newName2 } = o;

구문이 혼란스러워지기 시작합니다. a : newName1을 “a as newName1“로 읽을 수 있습니다. 방향은 왼쪽에서 오른쪽입니다.

1
2
let newName1 = o.a;
let newName2 = o.b;

혼란스럽지만 여기 콜론은 타입을 나타내지 않습니다. 타입을 지정하는 경우 전체 destructuring된 후에 타입을 지정해야 합니다.

1
let { a, b }: { a: string, b: number } = o;

기본값(Default value)

기본값을 사용하면 속성이 정의되지 않은 경우 기본값을 지정할 수 있습니다.

1
2
3
function keepWholeObject(wholeObject: { a: string, b?: number }) {
let { a, b = 1001 } = wholeObject;
}

keepWholeObject 함수는 b가 정의되지 않았더라도 ab 속성이 있는 wholeObject를 가집니다.

Function 선언

Destructuring은 함수 선언에서도 작동합니다. 아래의 간단한 예를 보겠습니다.

1
2
3
4
type C = { a: string, b?: number }
function f({ a, b }: C): void {
// ...
}

그러나 Parameter의 기본값을 지정하는 것이 더 일반적이며, Destructuring시 정확한 기본값을 가져 오는 것은 까다로울 수 있습니다. 우선, 기본값 앞에 타입을 적어야 하는 것을 잊지 말아야 합니다.

1
2
3
4
function f({ a, b } = { a: "", b: 0 }): void {
// ...
}
f(); // ok, default to { a: "", b: 0 }

그런 다음 메인 Initializer가 아닌 Destructured Property의 Optional Property에 대한 기본값을 지정해야 합니다. C에서 b는 Optional로 지정되었다는 것을 기억하세요.

1
2
3
4
5
6
function f({ a, b = 0 } = { a: "" }): void {
// ...
}
f({ a: "yes" }); // ok, default b = 0
f(); // ok, default to { a: "" }, which then defaults b = 0
f({}); // error, 'a' is required if you supply an argument

조심해서 Destructuring을 사용하십시오. 앞의 예제에서 보여 주듯이 단순한 Destructuring 표현을 제외하고는 혼란스러울 수 있습니다. 특히 이름 바꾸기, 기본값, type annotation을 사용하지 않아도 이해하기 힘든 깊이 중첩된 Destructuring에서는 특히 그렇습니다. Destructuring 표현은 단순하면서 최소한 유지하십시오. 언제든지 여러분이 생성한 Desctructuring을 할당해서 사용할 수 있습니다.

Spread

Spread 연산자는 Destructuring의 반대입니다. 배열을 다른 배열로 펼치거나(Spread) 객체를 다른 객체로 퍼뜨릴(Spread) 수 있습니다.

1
2
3
let first = [1, 2];
let second = [3, 4];
let bothPlus = [0, ...first, ...second, 5];

이 코드는 bothPlus에[0, 1, 2, 3, 4, 5] 값을 부여합니다. Spread는 firstsecond의 얕은 복사본을 만듭니다. 그리고 firstsecond는 Spread 의해 값이 변하지 않습니다.

객체를 Spread할 수도 있습니다.

1
2
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { ...defaults, food: "rich" };

이제 search{food : "rich", price : "$$", ambiance : "noisy"} 입니다. 객체 Spread는 배열 Spread보다 복잡합니다. 배열 Spread와 마찬가지로 왼쪽에서 오른쪽으로 진행되지만 결과는 여전히 객체입니다. 즉, 나중에 Spread된 객체의 Property중 이미 이전에 있던 Property와 이름이 같다면 Property의 값을 덮어 씁니다. 그래서 우리가 앞의 예제를 수정하여 끝에 Spread 하면 아래와 같습니다.

1
2
let defaults = { food: "spicy", price: "$$", ambiance: "noisy" };
let search = { food: "rich", ...defaults };

defaultsfood Property는 food :'rich'로 덮어 씁니다.이 경우에는 우리가 원하는 것이 아닙니다.

객체 Spread에는 몇 가지 다른 놀라운 limit가 있습니다.
첫째, 자신의 Enumerable property(열거 가능한 속성)만 포함됩니다. 이는 객체의 인스턴스를 Spread할 때 메서드가 손실된다는 것을 의미합니다.

1
2
3
4
5
6
7
8
9
class C {
p = 12;
m() {
}
}
let c = new C();
let clone = { ...c };
clone.p; // ok
clone.m(); // error!

둘째, Typescript 컴파일러는 generic function의 Parameter 변수의 스프레드를 허용하지 않습니다. 이 기능은 향후 버전에서 지원될 것으로 예상됩니다.


이 내용은 나중에 참고하기 위해 제가 공부하며 정리한 내용입니다.
의역, 오역, 직역이 있을 수 있음을 알려드립니다.
This post is a translation of this original article [https://www.typescriptlang.org/docs/handbook/variable-declarations.html]

참고

공유하기