Study JavaScript 0614 - 변수 var, 전역 객체

변승훈·2022년 6월 14일
0

Study JavaScript

목록 보기
29/43

1. 변수 var

변수 var는 let과 유사하다. 둘이 바꿔써도 큰 문제 없이 동작한다.
하지만 var는 초기 javscript 구현 방식 때문에 let과 const로 선언한 변수와는 다른 방식으로 동작한다.

오래된 스크립트에서 var를 만난다면 let으로 바꿔줘야 할 테고, 바꿔줄 경우 var에 대해 제대로 알아두어야 예상치 못한 에러를 피할 수 있다!

1-1. 'var'는 블록 스코프가 없다

var로 선언한 변수의 스코프(범위)는 함수 스코프이거나 전역 스코프이다. 블록 기준으로 스코프가 생기지 않기 때문에 블록 밖에서 접근이 가능하다.

if (true) {
  var test = true; // 'let' 대신 'var'를 사용
}

console.log(test); // true(if 문이 끝났어도 변수에 여전히 접근할 수 있음)

var는 코드 블록을 무시하기 때문에 test는 전역 변수가 된다. 이렇게 되면 전역 스코프에서 이 변수에 접근이 가능하다.

만약 위의 예시에서 var대신 let을 사용하면 어떻게 될까?
잘 알다시피 if문 안에서만 접근이 될 것이다.

if (true) {
  let test = true; // 'let'으로 변수를 선언
}

console.log(test); // Error: test is not defined

이는 반복문에서도 유사한 일이 일어난다.

for (var i = 0; i < 10; i++) {
  // ...
}

console.log(i); // 10, 반복문이 종료되었지만 'i'는 전역 변수이므로 여전히 접근 가능하다

이제 함수안에 코드 블록이 있으면 var는 함수 레벨 변수가 되는 것을 확인해보자.

function sayHi() {
  if (true) {
    var phrase = "Hello";
  }

  console.log(phrase); // 제대로 출력된다.
}

sayHi();
console.log(phrase); // Error: phrase is not defined

위에서 살펴본 바와 같이 var는 오래전의 javascript에서는 블록 수준 렉시컬 환경이 만들어지지 않았기 때문에 발생하는 현상이다.

1-2. var의 재선언

var는 let과 다르게 재선언이 가능하다. 예시를 보자

// let으로 선언
let user;
let user;	// SyntaxError: 'user' has already been declared

// var로 선언
var user = "Hun";
var user = "Seung";

console.log(user);	// Seung

1-3. 호이스팅

var 선언은 함수가 시작될 때 처리된다. 전역에서 선언한 변수라면 스크립트가 시작될 때 처리가 된다.

함수 본문 내에서 var로 선언한 변수는 선언 위치와 상관 없이 함수 본문이 시작되는 지점에서 정의가 된다. 단, 변수가 중첩 함수 내에서 정의되지 않아야 이 규칙이 적용된다.)

따라서 아래의 세 가지 예시 모두 동일하게 적용이 된다.

function sayHi() {
  phrase = "Hello";

  console.log(phrase);

  var phrase;
}
sayHi();
function sayHi() {
  var phrase;
  
  phrase = "Hello";

  console.log(phrase);

}
sayHi();
function sayHi() {
  phrase = "Hello"
  
    if (false) {
    var phrase;
  }

  console.log(phrase);
}
sayHi();

'호이스팅'은 이렇게 변수가 끌어 올려지는 현상이다. var로 선언한 모든 변수는 함수의 최상위 요소로 끌어 올려지기 때문이다.

마지막 예제에서 if 블록 안의 코드는 실행되지 않지만 이는 호이스팅에 영향을 주지 않는다.
if 내부의 var는 함수의 시작 부분에서 처리가 되므로 이미 phrase변수에 정의가 된 상태인 것이다.

선언은 호이스팅 되지만 할당은 호이스팅 되지 않는다.

예시를 통해 살펴보자.

function sayHi() {
  console.log(phrase);

  var phrase = "Hello";
}

sayHi();

var phrase = "Hello"에서는 두 가지 일이 일어난다.
1. 변수 선언
2. 변수에 값을 할당.

변수 선언은 함수 실행이 시작될 때 처리되지만(호이스팅) 할당은 호이스팅 되지 않기 때문에 할당 관련 코드에서 처리된다. 따라서 위 예제는 아래 코드처럼 동작하게 된다.

function sayHi() {
  var phrase; // 선언은 함수 시작 시 처리

  console.log(phrase); // undefined

  phrase = "Hello"; // 할당은 실행 흐름이 해당 코드에 도달했을 때 처리
}

sayHi();

이처럼 모든 var 선언은 함수 시작 시 처리되기 때문에 var로 선언한 변수는 어디서든 참조할 수 있다. 하지만 변수에 무언가를 할당하기 전 까지는 undefined이다.

1-4. 즉시 실행 함수 표현식

과거엔 var만 사용할 수 있었지만 var의 스코프는 블록 레벨 수준이 아니였다. 개발자들은 var도 블록 레벨 스코프를 가질 수 있게 여러가지 방안을 고려하게 되는데, 이때 만들어진 것이 '즉시 실행 함수 표현식(IIFE, immediately-invoked function expressions)'이다.

즉시 실행 함수 표현식을 요즘에는 자주 쓰지 않지만 오래된 스크립트에서 만날 수 있기 때문에 즉시 실행 함수 표현식이 무엇인지 알아 둘 필요가 있다.

(function() {

  let message = "Hello";

  console.log(message); // Hello

})();

함수 표현식이 만들어지고 바로 호출되면서, 해당 함수가 바로 실행되었다.

즉시 실행 함수를 만들 땐, 함수 표현식을 괄호로 둘러쌓아 (function {…})와 같은 형태로 만든다. 이렇게 괄호로 둘러싸지 않으면 에러가 발생한다. javascript는 'function’이라는 키워드를 만나면 함수 선언문이 시작될 것이라 예상하는데, 함수 선언문으로 함수를 만들 땐 반드시 함수의 이름이 있어야 한다.

따라서 아래와 예시를 실행하면 에러가 발생한다.

// 함수를 선언과 동시에 실행하려고 함
function() { // <-- Error: Function statements require a function name

  let message = "Hello";

  console.log(message); // Hello

}();

"그럼 이름을 넣으면 되는 거 아닌가?"라고 생각해 이름을 넣어도 에러가 발생한다. javascript는 함수 선언문으로 정의한 함수를 정의와 동시에 바로 호출하는 것을 허용하지 않기 때문이다.

// 맨 아래의 괄호 때문에 문법 에러가 발생한다
function go() {

}(); // <-- 함수 선언문은 선언 즉시 호출할 수 없다

함수를 괄호로 감싸면 javascript가 함수를 함수 선언문이 아닌 표현식으로 인식하도록 속일 수 있다. 함수 표현식은 이름이 없어도 괜찮고, 즉시 호출도 가능하다.

괄호를 사용하는 방법 말고도, javascript가 함수 표현식이라고 인식하게 해주는 다른 방법들이 있다.

// IIFE를 만드는 방법

(function() {
  console.log("함수를 괄호로 둘러싸기");
})();

(function() {
  console.log("전체를 괄호로 둘러싸기");
}());

!function() {
  console.log("표현식 앞에 비트 NOT 연산자 붙이기");
}();

+function() {
  console.log("표현식 앞에 단항 덧셈 연산자 붙이기");
}();

위와 같은 방법을 사용하면 함수 표현식처럼 인식되어 바로 실행이 가능하지만 모던 javascript에서는 이렇게 코드를 작성할 필요가 없다.

2. 전역 객체

전역 객체를 사용하면 어디서나 사용 가능한 변수나 함수를 만들 수 있다. 전역 객체는 언어 자체나 호스트 환경에 기본 내장되어 있는 경우가 많다.

브라우저 환경에선 전역 객체를 window, Node.js 환경에선 global라고 부르는데, 각 호스트 환경마다 부르는 이름은 다르다.

전역 객체의 이름을 globalThis로 표준화하자는 내용이 최근에 javascript 명세에 추가되었기 때문에 모든 호스트 환경이 이를 따라야 한다. Chromium 기반이 아닌 몇몇 브라우저는 아직 globalThis를 지원하진 않지만, 이에 대한 폴리필(polyfill)을 쉽게 만들 수 있다.

전역 객체의 모든 프로퍼티는 아래와 같이 직접 접근할 수 있다.
참고로 window가 되지 않는다 하면 globalThis로 대체하여 사용해보자!

console.log("Hello");
// 위와 동일하게 동작
window.console.log("Hello");

브라우저에서 let이나 const가 아닌 var로 선언한 전역 함수나 전역 변수는 전역 객체의 프로퍼티가 된다.

var gVar = 5;

console.log(window.gVar); // 5 (var로 선언한 변수는 전역 객체 window의 프로퍼티가 된다)

하위 호환성 때문에 이런 방식으로 전역 객체를 사용해도 동작은 하지만, 모듈을 사용하는 모던 javascript는 이런 방식을 지원하지 않으므로 사용하지 말자.

var 대신 let을 사용하면 위 예시와는 달리 전역 객체를 통해 변수에 접근할 수 없다.

let gLet = 5;

console.log(window.gLet); // undefined (let으로 선언한 변수는 전역 객체의 프로퍼티가 되지 않는다)
// console.log(globalThis.gLet);

중요한 변수라서 모든 곳에서 사용할 수 있게 하려면, 아래와 같이 전역 객체에 직접 프로퍼티를 추가해 주자.

// 모든 스크립트에서 현재 사용자(current user)에 접근할 수 있게 이를 전역 객체에 추가함
window.currentUser = {
  name: "John"
};

// 아래와 같은 방법으로 모든 스크립트에서 currentUser에 접근할 수 있음
console.log(currentUser.name);  // John

// 지역 변수 'currentUser'가 있다면
// 지역 변수와 충돌 없이 전역 객체 window에서 이를 명시적으로 가져올 수 있음
console.log(window.currentUser.name); // John

전역 변수는 되도록 사용하지 않는 것이 좋다.
함수를 만들 땐 외부 변수나 전역 변수를 사용하는 것보다 ‘인풋’ 변수를 받고 이를 이용해 '아웃풋’을 만들어내게 해야 테스트도 쉽고, 에러도 덜 만들어낸다.

폴리필 사용하기

전역 객체를 이용해 현재 사용중인 브라우저가 최신 javascript 기능을 지원하는지 여부를 확인할 수 있다.

내장 객체 Promise를 지원하는지 여부를 아래와 같이 테스트할 수 있다. 구식 브라우저는 Promise 객체를 지원하지 않기 때문에 alert 창이 뜰 것이다.

if (!window.Promise) {
  console.log("구식 브라우저를 사용 중이시군요!");
}

명세에는 있는 기능이지만 해당 기능을 지원하지 않는 오래된 브라우저를 사용하고 있다면 직접 함수를 만들어 전역 객체에 추가하는 방식으로 "폴리필"을 만들 수 있다.

if (!window.Promise) {
  window.Promise = ... // 모던 자바스크립트에서 지원하는 기능을 직접 구현함
}
profile
잘 할 수 있는 개발자가 되기 위하여

0개의 댓글