let

let

let 선언은 재할당 가능한 블록 스코프(block-scoped) 지역 변수를 선언하며, 선택적으로 각각을 값으로 초기화합니다. Baseline Widely available — 2016년 9월부터 여러 브라우저에서 지원되는 잘 정착된 기능입니다.

출처: let

본문

let 선언은 재할당 가능한 블록 스코프 지역 변수를 선언하고, 선택적으로 각각을 값으로 초기화합니다.

시도해 보기 (Try it)

let x = 1;

if (x === 1) {
  let x = 2;

  console.log(x);
  // Expected output: 2
}

console.log(x);
// Expected output: 1

구문 (Syntax)

let name1;
let name1 = value1;
let name1 = value1, name2 = value2;
let name1, name2 = value2;
let name1 = value1, name2, /* …, */ nameN = valueN;

매개변수 (Parameters)

  • nameN: 선언할 변수의 이름입니다. 각각은 유효한 JavaScript 식별자이거나 구조 분해 바인딩 패턴이어야 합니다.
  • valueN: 변수의 초기 값입니다. 유효한 표현식이면 무엇이든 될 수 있습니다. 기본값은 undefined입니다.

설명 (Description)

let으로 선언된 변수의 스코프는 let 선언을 가장 가깝게 포함하는 다음 중괄호로 감싸진 구문 중 하나입니다:

  • 블록 문
  • switch
  • try...catch
  • let이 문의 헤더에 있을 때 for 문 중 하나의 본문
  • 함수 본문
  • 정적 초기화 블록

또는 위의 어느 것도 해당하지 않으면:

  • 모듈 모드로 실행되는 코드인 경우 현재 모듈
  • 스크립트 모드로 실행되는 코드인 경우 전역 스코프

var와 비교해 let 선언은 다음 차이점이 있습니다:

  • let 선언은 함수뿐만 아니라 블록으로도 스코프됩니다.
  • let 선언은 선언 위치에 도달한 후에만 접근할 수 있습니다(시간적 사각지대 참고). 이런 이유로 let 선언은 흔히 호이스팅되지 않는 것으로 간주됩니다.
  • let 선언은 스크립트의 최상위 수준에서 선언될 때 globalThis에 속성을 만들지 않습니다.
  • let 선언은 같은 스코프의 다른 어떤 선언으로도 재선언될 수 없습니다.
  • let은 문(statement)이 아니라 선언(declaration)을 시작합니다. 즉, 단독 let 선언을 블록의 본문으로 사용할 수 없습니다(변수에 접근할 방법이 없으므로 말이 됩니다). if (true) let a = 1; // SyntaxError: Lexical declaration cannot appear in a single-statement context

참고: let은 비엄격 모드에서 varfunction으로 선언할 때 식별자 이름으로 허용되지만, 예기치 않은 구문 모호성을 방지하기 위해 let을 식별자 이름으로 사용하는 것은 피해야 합니다.

많은 스타일 가이드(MDN의 것 포함)는 변수가 스코프 안에서 재할당되지 않을 때마다 let보다 const를 사용할 것을 권장합니다. 이렇게 하면 변수의 타입(또는 원시 값의 경우 값)이 절대 바뀌지 않는다는 의도가 명확해집니다. 다른 사람들은 변경된(mutated) 비원시 값에 대해 let을 선호할 수도 있습니다.

let 키워드 뒤의 목록은 바인딩 목록이라고 불리며 쉼표로 구분됩니다. 여기서 쉼표는 쉼표 연산자가 아니고 =는 할당 연산자가 아닙니다. 목록의 뒷부분 변수의 이니셜라이저는 목록의 앞부분 변수를 참조할 수 있습니다.

시간적 사각지대 (Temporal dead zone, TDZ)

let, const, class로 선언된 변수는 블록 시작부터 코드 실행이 변수가 선언되고 초기화되는 위치에 도달할 때까지 "시간적 사각지대(TDZ)"에 있다고 합니다.

TDZ 안에 있는 동안 변수는 값으로 초기화되지 않았으며, 접근을 시도하면 ReferenceError가 발생합니다. 변수는 코드 실행이 선언된 위치에 도달할 때 값으로 초기화됩니다. 변수 선언으로 초기 값이 지정되지 않았다면 값 undefined로 초기화됩니다.

이는 선언되기 전에 접근하면 undefined 값을 반환하는 var 변수와 다릅니다. 아래 코드는 letvar가 선언 위치보다 앞의 코드에서 접근될 때 다른 결과를 보여줍니다.

{
  // TDZ starts at beginning of scope
  console.log(bar); // "undefined"
  console.log(foo); // ReferenceError: Cannot access 'foo' before initialization
  var bar = 1;
  let foo = 2; // End of TDZ (for foo)
}

"시간적(temporal)"이라는 용어는 이 영역이 코드가 작성된 순서(위치)가 아니라 실행 순서(시간)에 의존하기 때문에 사용됩니다. 예를 들어 아래 코드는 동작하는데, let 변수를 사용하는 함수가 변수 선언보다 앞에 나오더라도 그 함수가 TDZ 밖에서 호출되기 때문입니다.

{
  // TDZ starts at beginning of scope
  const func = () => console.log(letVar); // OK

  // Within the TDZ letVar access throws `ReferenceError`

  let letVar = 3; // End of TDZ (for letVar)
  func(); // Called outside TDZ!
}

TDZ에 있는 변수에 typeof 연산자를 사용하면 ReferenceError가 발생합니다:

{
  typeof i; // ReferenceError: Cannot access 'i' before initialization
  let i = 10;
}

이는 선언되지 않은 변수와 undefined 값을 가진 변수에 typeof를 사용하는 것과 다릅니다:

console.log(typeof undeclaredVariable); // "undefined"

참고: letconst 선언은 현재 스크립트가 처리될 때만 처리됩니다. 하나의 HTML 안에서 스크립트 모드로 실행되는 두 개의 <script> 요소가 있다면, 첫 번째 스크립트는 두 번째 스크립트에 선언된 최상위 let이나 const 변수에 대한 TDZ 제한을 받지 않습니다. 다만 첫 번째 스크립트에서 let이나 const 변수를 선언했다면, 두 번째 스크립트에서 다시 선언하면 재선언 오류가 발생합니다.

재선언 (Redeclarations)

let 선언은 let, const, class, function, var, import 선언을 포함한 다른 어떤 선언과도 같은 스코프에 있을 수 없습니다.

{
  let foo;
  let foo; // SyntaxError: Identifier 'foo' has already been declared
}

함수 본문 안의 let 선언은 매개변수와 같은 이름을 가질 수 없습니다. catch 블록 안의 let 선언은 catch로 바인딩된 식별자와 같은 이름을 가질 수 없습니다.

function foo(a) {
  let a = 1; // SyntaxError: Identifier 'a' has already been declared
}
try {
} catch (e) {
  let e; // SyntaxError: Identifier 'e' has already been declared
}

Firefox 웹 콘솔(Tools > Web Developer > Web Console) 같은 REPL에서 실험할 때, 두 개의 별도 입력에서 같은 이름의 let 선언을 두 번 실행하면 같은 재선언 오류가 발생할 수 있습니다. 이 이슈에 대한 추가 논의는 Firefox bug 1580891을 참고하세요. Chrome 콘솔은 서로 다른 REPL 입력 사이에서 let 재선언을 허용합니다.

switch 문은 블록이 하나뿐이므로 오류가 발생할 수 있습니다.

let x = 1;

switch (x) {
  case 0:
    let foo;
    break;
  case 1:
    let foo; // SyntaxError: Identifier 'foo' has already been declared
    break;
}

오류를 피하려면 각 case를 새 블록 문으로 감싸세요.

let x = 1;

switch (x) {
  case 0: {
    let foo;
    break;
  }
  case 1: {
    let foo;
    break;
  }
}

예제 (Examples)

스코프 규칙 (Scoping rules)

let으로 선언된 변수는 선언된 블록과 그 안에 포함된 모든 하위 블록에 스코프를 가집니다. 이런 점에서 letvar와 매우 비슷하게 동작합니다. 주요 차이점은 var 변수의 스코프는 전체 둘러싸는 함수라는 것입니다:

function varTest() {
  var x = 1;
  {
    var x = 2; // same variable!
    console.log(x); // 2
  }
  console.log(x); // 2
}

function letTest() {
  let x = 1;
  {
    let x = 2; // different variable
    console.log(x); // 2
  }
  console.log(x); // 1
}

프로그램과 함수의 최상위 수준에서 letvar와 달리 전역 객체에 속성을 만들지 않습니다. 예를 들어:

var x = "global";
let y = "global";
console.log(this.x); // "global"
console.log(this.y); // undefined

어휘 스코핑과 결합된 TDZ

다음 코드는 표시된 줄에서 ReferenceError를 발생시킵니다:

function test() {
  var foo = 33;
  if (foo) {
    let foo = foo + 55; // ReferenceError
  }
}
test();

if 블록은 외부 var foo가 값을 가지므로 평가됩니다. 그러나 어휘 스코핑(lexical scoping) 때문에 이 값은 블록 안에서 사용할 수 없습니다: if 블록 안의 식별자 foolet foo입니다. 표현식 foo + 55let foo의 초기화가 완료되지 않았으므로 — 여전히 시간적 사각지대에 있으므로 — ReferenceError를 발생시킵니다.

이 현상은 다음과 같은 상황에서 혼란스러울 수 있습니다. let n of n.a의 명령은 이미 for...of 루프 블록의 스코프 안에 있습니다. 따라서 식별자 n.a는 명령 자체의 첫 부분(let n)에 있는 n 객체의 속성 a로 해석됩니다. 이는 선언 문에 아직 도달해 종료되지 않았으므로 여전히 시간적 사각지대에 있습니다.

function go(n) {
  // n here is defined!
  console.log(n); // { a: [1, 2, 3] }

  for (let n of n.a) {
    //          ^ ReferenceError
    console.log(n);
  }
}

go({ a: [1, 2, 3] });

다른 상황들 (Other situations)

블록 안에서 사용될 때 let은 변수의 스코프를 그 블록으로 제한합니다. var는 선언된 함수 안으로 스코프된다는 차이에 유의하세요.

var a = 1;
var b = 2;

{
  var a = 11; // the scope is global
  let b = 22; // the scope is inside the block

  console.log(a); // 11
  console.log(b); // 22
}

console.log(a); // 11
console.log(b); // 2

하지만 아래의 varlet 선언의 조합은 var가 블록으로 스코프되지 않아 둘이 같은 스코프에 있게 되므로 SyntaxError입니다. 이는 변수의 암시적 재선언을 초래합니다.

let x = 1;

{
  var x = 2; // SyntaxError for re-declaration
}

구조 분해를 사용한 선언

=의 왼쪽도 바인딩 패턴일 수 있습니다. 이를 통해 한 번에 여러 변수를 만들 수 있습니다.

const result = /(a+)(b+)(c+)/.exec("aaabcc");
let [, a, b, c] = result;
console.log(a, b, c); // "aaa" "b" "cc"

자세한 내용은 구조 분해(Destructuring) 문서를 참고하세요.

명세 (Specifications)

let 문은 ECMAScript 언어 명세에 정의되어 있습니다.

브라우저 호환성 (Browser compatibility)

이 기능은 대부분의 현대 브라우저에서 널리 지원됩니다(Baseline Widely available). 자세한 호환성 표는 MDN의 브라우저 호환성 섹션을 참고하세요.

더 알아보기