HOWTO · JavaScript

JavaScript의 정적 변수

이 자습서에서는 JavaScript의 정적 변수를 소개합니다.

이 페이지의 내용

JavaScript에서는 public static 클래스 필드, 함수 객체의 속성, 또는 클로저가 유지하는 지역 변수로 함수 호출 사이에 값을 보존할 수 있습니다. 이 방식들은 모두 상태를 유지하지만, 상태가 어디에 속하고 누가 접근할 수 있는지는 서로 다릅니다. 클래스 전체가 공유하는 상태에는 클래스 필드를, 특정 함수에 연결된 단순한 public 상태에는 함수 속성을 사용하세요. 바인딩을 직접 읽거나 변경하지 않고 연산만 제공하려면 클로저를 사용할 수 있습니다.

JavaScript에서 정적 변수가 의미하는 것

정적 변수란 특정 함수 호출이 아니라 함수나 클래스에 연결된 상태를 말합니다. JavaScript에는 모든 함수에 적용되는 단일 정적 변수 선언 방식이 없습니다. 대신 여러 패턴을 사용할 수 있습니다. static 키워드는 클래스 자체에 클래스 필드나 메서드를 선언합니다. 함수에 할당한 속성은 해당 함수 객체에 속합니다. 클로저는 바깥 함수가 반환된 뒤에도 안쪽 함수에서 지역 바인딩을 사용할 수 있게 유지합니다.

이 개념들은 서로 연관되어 있지만, 각 방식이 모든 면에서 서로 대체 가능한 것은 아닙니다. 클래스 필드와 함수 속성은 이를 소유한 함수 또는 클래스 객체를 통해 접근할 수 있습니다. 클로저는 바인딩을 비공개로 유지하고 원하는 연산만 노출할 수 있습니다.

클래스 전체의 상태에는 static 클래스 필드 사용하기

public static 클래스 필드는 클래스에 속하므로 모든 인스턴스에서 클래스 이름을 통해 같은 필드를 참조합니다. 이 필드는 각 인스턴스에 복사되지 않습니다. 이 문법을 사용하려면 public static 클래스 필드를 지원하는 런타임이 필요합니다. 런타임이 이를 지원하지 않으면 아래의 함수 속성이나 클로저 패턴을 사용하세요.

다음 예제를 실행해 보세요.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

class VisitCounter {
  static count = 0;

  static recordVisit() {
    return ++VisitCounter.count;
  }
}

console.log(VisitCounter.recordVisit());
console.log(VisitCounter.recordVisit());
console.log(VisitCounter.count);
1
2
2

두 호출은 모두 같은 VisitCounter.count를 갱신합니다. 필드가 public이므로 코드에서 VisitCounter.count로 값을 읽거나 대입할 수도 있습니다. 클래스가 접근을 제어해야 한다면 # 이름을 사용하는 private static 필드를 사용하세요. static 멤버는 new VisitCounter()와 같은 인스턴스가 아니라 클래스에서 접근합니다.

함수에 속성 추가하기

함수도 객체이므로 이름이 있는 함수에 호출 사이에 유지되는 속성을 붙일 수 있습니다. 특정 함수에 연결된 카운터나 캐시에 간결하게 사용할 수 있는 방법입니다. 이 속성은 public입니다. 다른 코드에서 nextId.counter를 확인하거나 변경할 수 있으므로 이 패턴은 비공개 상태를 제공하지 않습니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

function nextId() {
  nextId.counter = (nextId.counter ?? 0) + 1;
  return nextId.counter;
}

console.log(nextId());
console.log(nextId());
console.log(nextId.counter);
1
2
2

같은 방식은 생성자 함수에도 적용됩니다. Widget.count와 같은 속성은 생성자 함수에 속하고, widget.name은 하나의 인스턴스에 속합니다. 각 인스턴스에 Widget.count의 복사본이 생기는 것은 아닙니다. 코드에서는 Widget을 통해 이 공유 속성에 접근합니다. 소유 방식은 클래스 전체가 공유하는 상태와 비슷하지만, JavaScript의 함수 속성은 일부 클래스 시스템이 제공하는 모든 접근 제어나 타입 보장을 제공하지는 않습니다.

비공개 상태에는 클로저 사용하기

클로저를 사용하면 안쪽 함수가 바깥 함수의 실행이 끝난 뒤에도 지역 바인딩을 계속 사용할 수 있습니다. IIFE (Immediately Invoked Function Expression)는 바깥 스코프를 즉시 만들고, 유지되는 값을 갱신할 수 있는 연산을 반환합니다. 바인딩 자체를 public 객체에 두지 않으므로 외부 코드에서 이를 직접 읽거나 대입할 수 없습니다.

이 섹션의 코드와 표시되는 레이블 또는 출력은 영어로 유지됩니다.

const counter = (function () {
  let count = 0;
  return function () {
    return ++count;
  };
})();

console.log(counter());
console.log(counter());
1
2

여기서 counter는 count를 클로저로 유지합니다. counter()를 호출할 때마다 같은 값이 증가하므로 호출 결과는 차례로 1, 2가 됩니다. 값은 비공개이고 counter는 public 인터페이스입니다. 서로 독립된 상태가 필요하면 별도의 팩토리 함수를 호출해 새 클로저를 만들고 반환하세요. 반환된 counter를 다시 호출하는 것만으로는 기존 상태가 증가합니다. 호출자가 카운트를 초기화하거나 확인할 수 있어야 한다면 바인딩 자체를 노출하는 대신 명시적인 메서드를 가진 작은 객체를 반환하세요.

arguments.callee를 피해야 하는 이유

arguments.callee는 arguments 객체를 통해 현재 실행 중인 함수를 가리킵니다. 예전 패턴에서는 이름이 있는 함수에 속성을 붙이는 것처럼 이 함수를 이용해 속성을 추가하기도 했습니다. 하지만 이는 레거시 기법이며 strict mode에서는 금지되어 있습니다. 따라서 ECMAScript 모듈을 포함해 strict mode가 적용되는 코드에서는 실패할 수 있습니다. 이름이 있는 함수를 사용하면 관계가 더 분명해지고, 클로저는 이 제한된 속성에 의존하지 않고 비공개 상태를 제공합니다. 새 코드를 작성할 때는 arguments.callee를 기반으로 하지 말고 위에서 설명한 지원되는 패턴 중 하나를 사용하세요.