변수의 생명 주기
변수는 선언에 의해 생성되고, 할당을 통해 값을 갖는다. 그리고 언젠가 소멸한다. 즉, 변수에는 생명 주기(life cycle)가 존재한다. 만약 변수의 생명 주기가 없다면 한번 선언된 변수는 프로그램이 종료되지 않는 한 영원히 메모리 공간을 점유하게 된다.
변수는 자신이 선언된 위치에서 생성되고 소멸한다. 전역 변수의 생명 주기는 애플리케이션의 생명 주기와 같다. 하지만 함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고 함수가 종료하면 소멸한다.
지역 변수의 생명 주기
변수 선언은 선언문이 어디에 있든 상관없이 런타임 이전인 소스코드의 평가 과정에서 자바스크립트 엔진에 의해 가장 먼저 실행된다. 하지만 이는 전역 변수에 한정된 것이다.
함수 내부에서 선언한 지역 변수는 함수가 호출된 직후에 자바스크립트 엔진에 의해 함수 몸체 내부의 변수 선언문이 가장 먼저 실행되어 변수가 선언되고 undefined로 초기화된다. 그 이후에 변수 할당문이 실행되어 값이 할당되는 것이다.
즉, 함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고, 함수가 종료되면 소멸한다. 지역 변수의 생명 주기는 함수의 생명 주기와 대부분 일치한다.
변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점까지다. 함수 내부에서 선언된 지역 변수는 함수가 생성한 스코프(변수에 접근할 수 있는 범위)에 등록된다. 이 스코프는 렉시컬 환경이라는 물리적 실체를 갖는다. 따라서 변수는 자신이 등록된 스코프가 소멸(메모리 해제)될 때까지 유효하다.
할당된 메모리 공간은 누구도 참조하지 않을 때 가비지 컬렉터에 의해 해제되어 가용 메모리 풀에 반환된다. 만약 누군가가 스코프를 참조하고 있으면 스코프는 소멸하지 않고 생존하게 되며, 스코프 내의 변수도 마찬가지로 소멸되지 않는다.
호이스팅은 스코프 단위로 동작한다. 전역 변수의 호이스팅은 전역 변수의 선언이 전역 스코프의 선두로 끌어 올려진 것처럼 동작하며, 지역 변수의 호이스팅은 지역 변수의 선언이 지역 스코프의 선두로 끌어 올려진 것처럼 동작한다. 호이스팅은 변수 선언이 스코프의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트의 고유한 특징이다.
전역 변수의 생명 주기
함수와 달리 전역 코드는 명시적인 호출 없이 실행된다. 함수 호출과 같은 특별한 진입점 없이 코드가 로드되자마자 곧바로 해석되고 실행되는 것이다. 따라서 전역 코드에서는 반환문을 사용할 수 없으며, 마지막 문이 실행되어 더 이상 실행할 문이 없을 때 종료된다.
var 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 된다. 이는 var 키워드로 선언한 전역 변수의 생명 주기가 전역 객체의 생명 주기와 일치함을 의미한다.
전역 객체는 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 가장 먼저 생성되는 특수한 객체를 말한다. 클라이언트 사이드 환경(브라우저)에서는 window, 서버 사이드 환경(Node.js)에서는 global 객체를 의미하며, 이는 ES11에서 globalThis로 통일되었다.
전역 객체는 표준 빌트인 객체(Object, String, Number 등)와 환경에 따른 호스트 객체(브라우저의 Web API, Node.js의 호스트 API), 그리고 var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.
브라우저 환경에서는 전역 객체가 window이므로 var 키워드로 선언한 전역 변수는 전역 객체 window의 프로퍼티다. 전역 객체 window는 웹페이지를 닫을 때까지 유효하므로, var 키워드로 선언한 전역 변수 역시 웹페이지를 닫을 때까지 유효하다. 즉, var 키워드로 선언한 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다.
요약
함수 내부에서 선언된 지역 변수는 함수가 호출되면 생성되고 함수가 종료하면 소멸한다.
변수 선언의 실행 시점과 변수 호이스팅
- 변수 선언은 런타임 이전에 자바스크립트 엔진에 의해 가장 먼저 실행된다. 하지만 이는 전역 변수에 한정된 설명이다.
- 함수 내부에서 선언한 변수는 함수가 호출된 직후, 함수 몸체의 코드가 한 줄씩 순차적으로 실행되기 이전에 자바스크립트 엔진에 의해 먼저 실행된다.
호이스팅은 스코프 단위로 동작
- 전역 변수의 호이스팅: 전역 변수의 선언이 전역 스코프의 선두로 끌어 올려진 것처럼 동작한다.
- 지역 변수의 호이스팅: 지역 변수의 선언이 지역 스코프의 선두로 끌어 올려진 것처럼 동작한다.
지역 변수의 생명 주기
- 함수 호출 시 함수 내부의 변수 선언문이 가장 먼저 실행되어 변수가 선언되고 undefined로 초기화된다.
- 이후 함수 몸체의 코드가 순차적으로 실행되면서 변수에 값이 할당된다.
- 함수가 종료되면 변수도 소멸하여 생명 주기가 종료된다.
일반적으로 지역 변수의 생명 주기는 함수의 생명 주기와 일치한다. 하지만 클로저와 같은 경우 지역 변수가 함수보다 오래 생존할 수 있다.
변수의 생명 주기는 메모리 공간이 확보된 시점부터 메모리 공간이 해제되어 가용 메모리 풀에 반환되는 시점까지다. 할당된 메모리 공간을 더 이상 누구도 참조하지 않을 때 가비지 콜렉터에 의해 해제된다.
전역 코드는 함수와 달리 명시적인 호출 없이 실행된다. 코드가 로드되자마자 곧바로 해석되고 실행된다. 전역 코드에는 반환문을 사용할 수 없으므로 마지막 문이 실행되어 더 이상 실행할 문이 없을 때 종료한다.
var 키워드로 선언한 전역 변수의 특징
- 전역 객체의 프로퍼티가 된다.
- 전역 변수의 생명 주기는 전역 객체의 생명 주기와 일치한다.
전역 객체
- 코드가 실행되기 이전 단계에 자바스크립트 엔진에 의해 가장 먼저 생성되는 특수한 객체이다.
- 브라우저 환경에서는 window, Node.js 환경에서는 global 객체를 의미한다.
- ES11에서 globalThis로 통일되었다.
- 표준 빌트인 객체, 환경에 따른 호스트 객체, var 키워드로 선언한 전역 변수와 전역 함수를 프로퍼티로 갖는다.
브라우저 환경에서 var 키워드로 선언한 전역 변수는 웹페이지를 닫을 때까지 유효하며, 전역 객체인 window의 프로퍼티로 존재한다.
전역 변수의 문제점
전역 변수를 선언한 의도는 전역, 즉 코드 어디서든 참조하고 할당할 수 있는 변수를 사용하겠다는 것이다. 하지만 전역 변수의 무분별한 사용은 많은 문제를 야기한다. 전역 변수의 문제점을 정리하면 다음과 같다.
1. 암묵적 결합
모든 코드가 전역 변수를 참조하고 변경할 수 있는 암묵적 결합(implicit coupling)을 허용하는 것과 같다. 변수의 유효 범위가 크면 클수록 코드의 가독성은 나빠지고 의도치 않게 상태가 변경될 수 있는 위험성도 높아진다.
2. 긴 생명 주기
전역 변수는 생명 주기가 길다. 따라서 메모리 리소스도 오랜 기간 소비한다. 또한 전역 변수의 상태를 변경할 수 있는 시간도 길고 기회도 많다. 지역 변수의 경우 생명 주기가 짧기 때문에 메모리 리소스를 짧은 기간만 소비하며, 전역 변수에 비해 상태 변경에 의한 오류가 발생할 확률이 적다.
var x = 1;
// ...
// 변수의 중복 선언. 기존 변수에 값을 재할당한다.
var x = 100;
console.log(x); // 100
더욱이 var 키워드로 선언한 전역 변수는 중복 선언이 허용되므로 생명 주기가 긴 전역 변수는 변수 이름이 중복될 가능성이 있고, 이로 인해 의도치 않은 재할당이 이루어질 수 있다.
3. 스코프 체인 상에서 종점에 존재
전역 변수는 스코프 체인 상에서 가장 마지막에 위치한다. 이는 변수를 검색할 때 전역 변수가 가장 마지막에 검색된다는 것을 의미한다. 즉, 전역 변수의 검색 속도가 가장 느리다. 검색 속도의 차이가 매우 크지는 않지만 분명한 차이는 존재한다.
4. 네임스페이스 오염
자바스크립트의 가장 큰 문제점 중 하나는 파일이 분리되어 있어도 하나의 전역 스코프를 공유한다는 것이다. 따라서 다른 파일 내에서 동일한 이름으로 명명된 전역 변수나 전역 함수가 같은 스코프 내에 존재할 경우 예상치 못한 결과를 가져올 수 있다.
이러한 전역 변수의 문제점들은 코드의 복잡성을 가중시키고 의도치 않은 상태 변경을 유발하여 애플리케이션의 안정성을 해치는 요인이 된다. 따라서 전역 변수의 사용을 최소한으로 줄이고, 지역 변수를 사용하는 것이 바람직하다. 모듈 패턴이나 ES6 모듈을 사용하면 전역 변수를 사용할 필요 없이 네임스페이스를 분리하여 변수나 함수를 모듈 내부로 캡슐화할 수 있어, 전역 변수로 인한 문제를 예방할 수 있다.
전역 변수의 사용을 억제하는 방법
전역 변수의 무분별한 사용은 많은 문제를 야기하므로 전역 변수의 사용을 최대한 억제해야 한다. 전역 변수를 반드시 사용해야 할 이유를 찾지 못한다면 지역 변수를 사용해야 한다. 변수의 스코프는 좁을수록 좋다. 다음은 전역 변수의 사용을 억제할 수 있는 방법들이다.
즉시 실행 함수
전역 변수 사용을 억제하기 위해 즉시 실행 함수(IIFE, Immediately Invoked Function Expression)를 사용할 수 있다. 즉시 실행 함수는 정의되자마자 즉시 호출되는 함수를 말한다. 모든 코드를 즉시 실행 함수로 감싸면 모든 변수는 즉시 실행 함수의 지역 변수가 된다.
(function () {
var foo = 10; // 즉시 실행 함수의 지역 변수
// ...
}());
console.log(foo); // ReferenceError: foo is not defined
즉시 실행 함수를 사용하면 전역 변수를 생성하지 않으므로 라이브러리 등에 자주 사용된다. 또한 즉시 실행 함수는 밖에서 접근할 수 없는 자체적인 스코프를 가지므로 private한 변수를 만들 수 있다.
네임 스페이스 객체
네임스페이스 객체를 사용하는 것은 전역에 네임스페이스 역할을 담당할 객체를 생성하고 전역 변수처럼 사용하고 싶은 변수를 프로퍼티로 추가하는 방법이다.
var MYAPP = {}; // 전역 네임스페이스 객체
MYAPP.name = 'Lee';
console.log(MYAPP.name); // Lee
네임스페이스 객체에 또 다른 네임스페이스 객체를 프로퍼티로 추가해서 네임스페이스를 계층적으로 구성할 수도 있다.
var MYAPP = {}; // 전역 네임스페이스 객체
MYAPP.person = {
name: 'Lee',
address: 'Seoul'
};
console.log(MYAPP.person.name); // Lee
네임스페이스 객체를 사용하면 식별자 충돌을 막을 수는 있지만, 네임스페이스 객체 자체가 전역 변수에 할당되므로 그다지 유용한 방법은 아니다.
모듈 패턴
모듈 패턴은 클래스를 모방해서 관련이 있는 변수와 함수를 모아 즉시 실행 함수로 감싸 하나의 모듈을 만드는 것이다. 모듈 패턴은 자바스크립트의 강력한 기능인 클로저(함수가 자신이 선언되었을 때의 환경인 스코프를 기억하는 현상)를 기반으로 동작한다.
모듈 패턴을 사용하면 전역 변수의 억제는 물론 캡슐화까지 구현할 수 있다. 여기서 캡슐화란 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하는데, 이를 정보 은닉(information hiding)이라 한다.
var Counter = (function () {
// private 변수
var num = 0;
// 외부로 공개할 데이터나 메서드를 프로퍼티로 추가한 객체를 반환한다.
return {
increase() {
return ++num;
},
decrease() {
return --num;
}
};
}());
// private 변수는 외부로 노출되지 않는다.
console.log(Counter.num); // undefined
console.log(Counter.increase()); // 1
console.log(Counter.increase()); // 2
console.log(Counter.decrease()); // 1
console.log(Counter.decrease()); // 0
위 예제의 모듈 패턴에서 즉시 실행 함수가 반환하는 객체 리터럴에는 외부에 공개하고자 하는 데이터나 메서드를 추가한다. 이때 반환되는 객체 리터럴의 프로퍼티는 외부로 공개되는 public 멤버가 되며, 그 외의 변수나 함수는 private 멤버가 된다.
자바스크립트는 public, private, protected 등의 접근 제한자를 제공하지 않기 때문에, 모듈 패턴은 이러한 접근 제한을 모방하여 구현한 것이라 할 수 있다.
스코프와 클로저
스코프는 변수에 접근할 수 있는 범위를 말한다. 자바스크립트에는 전역 스코프와 지역 스코프(함수 스코프, 블록 스코프)가 있으며, 스코프는 중첩될 수 있다. 내부 스코프에서는 외부 스코프의 변수에 접근할 수 있지만 그 반대는 불가능하다.
클로저는 함수와 그 함수가 선언되었을 때의 환경(스코프)의 조합이다. 좀 더 쉽게 말하면, 클로저는 함수가 속해 있는 스코프를 기억하고 있는 함수라고 할 수 있다.
일반적인 예시로 드는 클로저의 기본 개념은 다음과 같다.
- 내가 방 안에 있다고 상상해보자. 이 방이 하나의 함수라고 생각하면 된다.
- 방 안에는 여러 물건들(변수들)이 있다.
- 그리고 보통 내가 방을 나가면 그 방 안의 물건들을 더 이상 사용할 수 없게 된다.
- 방에 어쨌거나 있어야 물건을 쓸 수 있으니 말이다.
- 하지만 클로저는 내가 방을 나갈 때 그 방의 물건 중 일부를 가지고 나올 수 있게 해주는 주머니(혹은 손) 같은 것이다.
함수는 보통 선언된 스코프 내에서만 접근할 수 있는데, 클로저를 사용하면 함수 외부에서도 함수 내부의 변수에 접근할 수 있게 된다.
간단한 코드 예시를 보자.
function outerFunction(x) {
var y = 10;
function innerFunction() {
console.log(x + y);
}
return innerFunction;
}
var closure = outerFunction(5);
closure(); // 15
위 코드에서 outerFunction은 innerFunction을 반환한다. 이때 innerFunction은 outerFunction의 스코프에 접근할 수 있다.
outerFunction(5)를 호출하면 x에 5가 할당되고, innerFunction이 반환되어 closure 변수에 저장된다.
closure()를 호출하면 innerFunction이 실행되는데, 이때 innerFunction은 선언되었던 outerFunction의 스코프에 여전히 접근할 수 있다. 그래서 x와 y에 접근하여 그 합을 출력할 수 있는 것이다.
이렇게 클로저를 사용하면 함수 외부에서도 함수 내부의 변수에 접근할 수 있다. 이를 활용하면 전역 변수의 사용을 최소화하면서도 데이터를 공유할 수 있을 것이다.
모듈 패턴에서는 클로저를 사용하여 private 변수를 구현한다. 즉시 실행 함수로 모듈을 정의하고, 클로저를 통해 외부에 공개할 부분만 반환하는 것이다. 이렇게 하면 모듈 내부의 세부 사항은 숨기고, 외부에는 필요한 부분만 노출할 수 있을 것이다.
이렇게 클로저는 클로저는 스코프와 밀접한 관련이 있으며, 함수 내부의 변수를 외부에서도 접근할 수 있게 하여 변수의 생명 주기를 연장시킬 수 있다. 물론 함수형 프로그래밍과 모듈화 등에도 유용하게 사용된다.
ES6 모듈
ES6에서는 클라이언트 사이드 자바스크립트에서도 동작하는 모듈 기능을 추가했다. ES6 모듈을 사용하면 전역 변수를 사용할 수 없다. ES6 모듈은 파일 자체의 독자적인 모듈 스코프를 제공하므로 모듈 내에서 var 키워드로 선언한 변수는 더이상 전역 변수가 아니며 window 객체의 프로퍼티도 아니다.
ES6 모듈은 script 태그에 type="module" 어트리뷰트를 추가하면 로드된 자바스크립트 파일을 모듈로서 동작하게 한다.
<script type="module" src="lib.mjs"></script>
<script type="module" src="app.mjs"></script>
단, 아직은 브라우저가 지원하는 ES6 모듈 기능보다는 Webpack 등의 모듈 번들러를 사용하는 것이 일반적이다. ES6 모듈의 파일 확장자는 mjs를 권장한다.
예상 문제
전역 변수의 문제점에 대해 설명해주세요.
- 전역 변수에는 몇 가지 주요 문제점이 있습니다.
- 첫째, 암묵적 결합을 허용합니다. 즉, 모든 코드가 전역 변수를 참조하고 변경할 수 있어 코드의 가독성과 유지보수성이 떨어집니다.
- 둘째, 생명 주기가 깁니다. 이는 메모리 리소스를 오래 소비하고, 변수의 상태가 변경될 기회가 많아집니다.
- 셋째, 스코프 체인의 종점에 위치해 검색 속도가 가장 느립니다.
- 마지막으로, 네임스페이스 오염을 일으킵니다. 서로 다른 파일에서 동일한 이름의 전역 변수를 사용할 경우 충돌이 발생할 수 있습니다.
변수의 생명 주기에 대해 설명해주세요.
- 변수의 생명 주기는 변수가 메모리에 할당되는 시점부터 메모리에서 해제되는 시점까지를 말합니다.
- 전역 변수의 경우 프로그램이 시작될 때 생성되어 프로그램이 종료될 때까지 유지됩니다.
- 반면 지역 변수는 함수가 호출될 때 생성되고 함수가 종료될 때 소멸됩니다.
- 단, 클로저와 같은 특별한 경우에는 지역 변수가 함수 종료 후에도 계속 존재할 수 있습니다. 변수의 생명 주기를 이해하는 것은 메모리 관리와 스코프 관리에 중요합니다.
호이스팅에 대해 설명해주세요.
- 호이스팅은 변수나 함수 선언문이 스코프의 최상단으로 끌어올려진 것처럼 동작하는 자바스크립트의 특성입니다.
- 쉽게 설명하자면, 변수나 함수가 실제 코드보다 먼저 선언된 것처럼 동작하는 것입니다.
- 이는 코드의 실행 전에 자바스크립트 엔진이 먼저 전체 코드를 스캔하여 변수와 함수 선언을 메모리에 저장하기 때문입니다.
- var로 선언한 변수는 선언과 동시에 undefined로 초기화되지만, let과 const로 선언한 변수는 초기화되지 않은 상태로 호이스팅됩니다.
전역 변수의 사용을 억제하는 방법에는 어떤 것들이 있나요?
- 전역 변수 사용을 억제하는 방법에는 여러 가지가 있습니다.
- 첫째, 즉시 실행 함수를 사용할 수 있습니다. 이는 함수를 선언하고 바로 실행하여 지역 스코프를 만듭니다.
- 둘째, 네임스페이스 객체를 사용할 수 있습니다. 이는 전역에 하나의 객체를 만들고 그 안에 필요한 변수들을 속성으로 추가하는 방식입니다.
- 셋째, 모듈 패턴을 사용할 수 있습니다. 이는 즉시 실행 함수와 클로저를 조합하여 private한 변수와 메소드를 구현하는 방식입니다. 마지막으로, ES6의 모듈 시스템을 사용할 수 있습니다. 이는 파일 단위의 모듈 스코프를 제공하여 전역 변수 사용을 방지합니다.
전역 변수와 지역 변수의 차이점은 무엇인가요?
- 전역 변수와 지역 변수의 가장 큰 차이점은 유효 범위입니다. 전역 변수는 코드 어디에서든 접근할 수 있는 변수이고, 지역 변수는 자신이 선언된 함수나 블록 내에서만 접근할 수 있는 변수입니다.
- 또한 생명 주기도 다릅니다. 전역 변수는 애플리케이션이 시작할 때 생성되어 애플리케이션이 종료될 때까지 유지되는 반면, 지역 변수는 함수가 호출될 때 생성되고 함수가 종료될 때 소멸합니다. 전역 변수는 불가피한 경우가 아니라면 사용을 자제해야 합니다.
- 전역 변수는 코드 어디서든 접근할 수 있고 변경할 수 있기 때문에, 코드의 복잡성을 증가시키고 의도치 않은 결과를 초래할 수 있기 때문입니다.
클로저란 무엇인가요?
- 클로저는 함수와 그 함수가 선언되었을 시점의 렉시컬 환경(스코프)의 조합입니다.
- 클로저를 사용하면 함수 내부의 변수를 함수 외부에서도 접근할 수 있게 됩니다. 예를 들어, 외부 함수 내에 내부 함수를 정의하고, 외부 함수가 내부 함수를 반환하면 클로저가 형성됩니다.
- 이때 내부 함수는 외부 함수의 변수에 접근할 수 있고, 외부 함수가 종료되어도 내부 함수가 외부 함수의 변수를 기억하고 있기 때문에 그 변수에 계속 접근할 수 있습니다.
- 클로저는 private 변수를 만들거나, 함수형 프로그래밍에서 많이 사용됩니다. 하지만 클로저를 잘못 사용하면 메모리 누수가 발생할 수 있으므로 주의해야 합니다.
모듈 패턴이란 무엇이고, 어떤 장점이 있나요?
모듈 패턴은 관련된 변수와 함수를 하나의 모듈로 묶어 캡슐화를 제공하는 디자인 패턴입니다. 모듈 패턴은 즉시 실행 함수를 사용하여 private 변수와 함수를 만들고, 외부에 공개할 public 변수와 함수를 반환합니다.
모듈 패턴의 장점은 다음과 같습니다.
- 전역 변수의 사용을 최소화할 수 있습니다.
- private 변수와 함수를 만들 수 있어 정보 은닉이 가능합니다.
- 코드의 구조를 개선하고 유지보수성을 높일 수 있습니다.
- 의존성을 줄이고 코드의 재사용성을 높일 수 있습니다.
'🧱 프론트엔드 주제 > JavaScript' 카테고리의 다른 글
[모던 자바스크립트 Deep Dive] 17장 - 생성자 함수에 의한 객체 생성 (0) | 2024.07.15 |
---|---|
[모던 자바스크립트 Deep Dive] 15장 let, const 키워드와 블록 레벨 스코프 (0) | 2024.07.11 |
[모던 자바스크립트 Deep Dive] 13장 스코프 (0) | 2024.07.07 |
[모던 자바스크립트 Deep Dive] 12장 함수 (1) | 2024.06.30 |
[모던 자바스크립트 Deep Dive] 11장 원시 값과 객체의 비교 (0) | 2024.06.27 |