본문 바로가기
프로그래밍/기타

[JS] var, let, const, 호이스팅과 클로저

by 카프카뮈 2021. 6. 17.

오늘은 처음으로 Javascript 포스트!

예전에 프로젝트를 진행하면서 Vue.JS를 활용한 적이 있었다.

그래서 최근 면접도 보고 하며...관련된 질문도 받고 했었는데,

생각보다 Javascript 기초를 얕게 배웠다는걸 많이 느꼈다.

 

기초부터 다져야 더 복잡한 내용도 잘하리라 믿고,

아는 줄 알았는데 헷갈렸던 내용을 정리해본다.


변수와 상수

자바스크립트에서 사용할 수 있는 데이터 타입(숫자, 텍스트, 날짜 등...)을 다루기 전에, 먼저 변수와 상수가 어떻게 다른지를 정리해보려고 한다. 이들은 자바스크립트가 데이터를 보관하는 메커니즘이다.

자바스크립트에서는 이러한 데이터 보관 메카니즘으로 var, let, const를 제공한다.

  • var: 변수를 저장한다. 값을 여러 번 할당할 수 있으며, 여러 이슈때문에 ES6 이후엔 let으로 대체하길 권한다.
  • let: 변수를 저장한다. 값을 한 번만 선언할 수 있다. (즉, 같은 스코프에 여러 개를 선언할 수 없다)
  • const: 상수를 저장한다. 값을 할당한 뒤 다른 값으로 변경할 수 없다.

코드를 짤 때, 가능하다면 변수 대신 상수를 쓰는 것이 좋다.

데이터의 값이 의도치 않게 바뀌어 생기는 문제가 많기 때문이다.

여러 명이 개발을 하고 해당 코드가 수많은 개발자의 손을 탄다고 생각하면, 그 사이에 누군가의 착각으로 값을 변경하는 것이 불가한 일은 아닐 것이다.


var vs let

변수(Variable)는 간단히 말하면 이름이 붙은 값으로, 변수라는 이름에서 암시되듯 언제든 값이 바뀔 수 있다.

ES6 이전에는 var 키워드만 사용할 수 있었는데, let이라는 키워드가 생기며 변화가 생겼다.

그리고 지금은, var 대신 let을 사용하는 것을 권장하고 있다. 왜 그럴까?

var vs let: 호이스팅

이는 호이스팅이라는 javascript의 메카니즘 때문이다.

x; //undefined
var x = 3;
x; //3

위의 코드를 보면, x를 2번째 줄에 선언해 줬는데 왜 첫번째 줄에 써줘도 사용이 가능한지 의문스럽다.

물론 값이 undefinded로 잡히긴 하지만, 그래도 x에는 접근할 수 있고 이에 대한 에러도 발생하지 않는다는 것이다.

(만약 같은 로직에서 x를 let으로 선언하면, ReferenceError가 발생한다)

 

let으로 변수를 선언하게 되면, 그 변수는 선언하기 전에는 존재하지 않는다. 

그러나 var로 선언한 변수는 현재의 스코프에서는 어디서든 사용할 수 있으며, 심지어 선언 전에도 사용할 수 있다.

Javascript에서는 var로 선언한 변수, 그리고 함수의 선언이 컴파일 단계에서 미리 처리되며, 이것이 물리적인 코드의 상단으로 이동되는 것과 똑같이 동작한다. 하지만 이것이, 변수의 초기화까지 코드 상단에 위치한다는 의미는 아니다.

자바스크립트에서 변수는 선언 단계, 초기화 단계, 할당 단계를 거치게 되는데, 이 중 선언 단계만이 코드 상단으로 이동한다고 이해하면 편하다. 이 탓에 실제 함수나 변수에는 접근할 수 있지만, 변수에 할당한 함수나 변수 자체의 값 대입은 실제 코드 위치에서 일어난다. (별도로 할당하지 않은 함수는 호출할 수 있다)

 

이러한 동작을 호이스팅이라고 한다. 아래의 코드를 보자.

//예시 1: 함수를 선언하기 전에 호출해도 작동
f(); //'f'
function f(){
	console.log('f');
}

//예시 2: var를 선언하기 전에 호출하면 undefined
x; //undefined
var x = 42;
x; //42

//예시 3: let으로 된 변수나 함수를 선언 전에 호출하면 에러
x; //ReferrenceError
f(); //ReferrenceError
let x = 42;
let f = function(){
	console.log('f');
}

이러한 문제가 낳을 수 있는 복잡한 문제 탓에, ES6 이후에는 var 대신 let을 사용하는 것을 권장하고 있다.

레거시 코드의 유지보수 등을 위해 불가피하게 var를 쓸 수도 있지만, var를 let으로 대체할 경우 호이스팅의 문제를 고민할 필요가 없다는 것을 생각하자.

덧. 만약 let으로 변수를 선언하게 되면, 해당 스코프의 시작부터 변수 선언까지는 사각지대(Temporal Dead Zone: TDZ)가 된다. 해당 구간에서 접근을 시도할 경우, ReferrenceError가 발생하게 된다.


var vs let: 블록 스코프의 문제

이외에도, var 대신 let을 써야 할 이유가 한가지 더 있다.

var의 경우, 한번 값을 선언한 뒤 같은 형식자를 또 선언하더라도 선언이 가능하다.

그러나 let의 경우, 한번 값이 선언되면 같은 형식자를 또 선언할 수 없다!

//예시 1: 같은 이름의 var 두번 선언하기
var x = 42;
console.log(x); //42
var x = 21;
console.log(x); //21

//예시 2: 같은 이름의 let 두번 선언하기
let x = 42;
console.log(x); //42
let x = 21; // Error!

이러한 문제 때문에, 코딩 도중 오류가 발생해도 var를 쓰면 인지하기 어렵고, 더 큰 문제를 만들 수도 있다.

독특한 다른 예시도 하나 보자. 만약 어떤 스코프 내에 더 작은 스코프를 만들고 선언하면 어떨까?

//예시 1: 다른 스코프에서 같은 이름의 var 선언하기
var x = 42;
{
	//스코프를 만들고 그 안에 다시 선언해주기
    var x = 21;
}
console.log(x); //21

//예시 2: 다른 스코프에서 같은 이름의 let 선언하기
let x = 42;
{
	//스코프를 만들고 그 안에 다시 선언해주기
    let x = 21;
    console.log(x); //21
}
console.log(x); //42

위처럼 스코프가 중첩되는 경우, let을 사용했다면 외부 스코프의 변수가 가려지고 내부 스코프의 변수를 사용하게 된다. 그러나 var를 사용할 경우, 위의 코드의 의미를 '외부의 x를 내부에서 변경해주는 것'으로 인식하여 값을 재할당하게 된다. 이 경우 스코프가 엉켰을 때 큰 문제를 만들 수 있고, 클로저 등의 유용한 패턴을 사용하면서 문제를 만들 수 있다.


스트릭트 모드

ES5 문법에서는 암시적 전역 변수라는 문제가 발생할 수 있었다고 한다.

var로 변수를 선언하는 것을 잊고 어떤 값을 사용했을 때, 자바스크립트는 이것이 전역 변수라고 짐작하고, 해당 전역 변수의 선언을 코드에 추가했다. 이 경우, 수많은 문제가 발생할 것이 뻔한 일이다.

이러한 문제 때문에, 자바스크립트에서는 스트릭트 모드(strict mode)를 도입했다.

코드의 맨 앞에 use strict 라는 구절을 추가하면, 이러한 암시적 전역 변수를 허용하지 않는다.


클로저

앞서 스코프라는 말을 잠시 사용했다.

스코프는 프로그램의 현재 실행 중인 부분, 즉 실행 컨텍스트에서 현재 보이고 접근할 수 있는 식별자의 유효범위를 의미한다. 이에 대해 이해해보자면, 전역 변수와 지역 변수의 예시를 들면 적절하겠다.

let x = 42;
function f(){
    let y = 21;
    console.log(x); //42
    console.log(y); //21
}
console.log(x); //42
console.log(y); //Error!

전역 스코프에 선언된 x의 경우, 함수 f와 그 바깥에서 모두 참조할 수 있다.

그러나 함수의 정적 스코프 내에 선언된 y의 경우, 함수 바깥에서 참조하려고 하면 에러가 발생한다.

이러한 정적 스코프는 전역 범위, 함수 범위, 블록 범위( {}로 범위를 잡는 경우 )에 적용된다.


그런데, 최신 자바스크립트에서는 함수가 필요한 곳에서 즉석으로 정의되는 경우가 생긴다.

함수를 변수나 객체 프로퍼티에 할당하고, 배열에 추가하며, 다른 함수에 전달하고, 함수가 함수를 반환하며, 이름도 없는 경우가 많다고 하는 것이다.

이때 내가 사용하고자 하는 함수가 특정한 스코프에 접근할 수 있도록 의도록으로 함수를 해당 스코프 내에 정의하는 경우가 있다. 이를 클로저(Closure)라고 한다.

function init() {
    var name = "kafka"; //init라는 함수의 스코프 내 지역 변수
    function displayName() {
    	console.log(name);
    }
    displayName();
}
init(); //kafka!

위 코드를 보자. 만약 우리가 init를 호출하면, 우리는 전역 스코프에서도 init의 정적 스코프 내에 있는 변수 name에 접근할 수 있다! 일반적으로는 스코프에서 빠져나가면 해당 스코프에서 선언한 변수를 메모리에서 제거해도 안전하겠지만, 위의 경우에서는 해당 함수를 스코프 바깥에서 접근할 수 있으므로 스코프를 유지해 가능한 일이다.

즉, init()의 실행이 끝난 뒤에도 name 변수에 접근할 수 있다는 것이다.

 

좀 더 복잡한 예시를 보자. 이 코드는 MDN에서 참조한 코드이다.

function makeAdder(x) {
  var y = 1;
  return function(z) {
    y = 100;
    return x + y + z;
  };
}

var add5 = makeAdder(5);
var add10 = makeAdder(10);
//클로저에 x와 y의 환경이 저장됨

console.log(add5(2));  // 107 (x:5 + y:100 + z:2)
console.log(add10(2)); // 112 (x:10 + y:100 + z:2)
//함수 실행 시 클로저에 저장된 x, y값에 접근하여 값을 계산

위의 코드에서, add5와 add10은 모두 클로저이다. 

이들은 같은 함수의 본문 정의를 공유하지만, 서로 다른 맥락 환경을 저장한다.

그렇기에 각각의 함수를 호출할 때 인자값은 스코프에 남아 저장된다.

또한 y 값 역시, 100으로 변경된 것을 보면, 클로저가 리턴된 뒤에도 남아 외부함수에서 접근 가능함을 볼 수 있다.

 

마지막으로 클로저의 실용적인 활용 예시! 이 역시 MDN을 출처로 하는 코드이다.

function makeSizer(size) {
  return function() {
    document.body.style.fontSize = size + 'px';
  };
}

var size12 = makeSizer(12);
var size14 = makeSizer(14);
var size16 = makeSizer(16);
//---
document.getElementById('size-12').onclick = size12;
document.getElementById('size-14').onclick = size14;
document.getElementById('size-16').onclick = size16;

위 코드는 웹페이지에서 사용자의 입력을 받아, 글자 크기를 조절하는 몇 개의 버튼을 만드는 예시이다.

각각의 사이즈 버튼에는 onclick 함수가 연될되는데, 이 함수는 클로저로 저장된다.

이때 makeSizer를 통해 반환받은 함수는 인자로 받은 size의 정보를 해당 클로저 내에 저장하게 되며, 

이를 활용해 makeSizer라는 팩토리를 바탕으로 여러 옵션의 함수를 만들 수 있게 된다.


아래는 글을 쓰며 참고한 출처이다.

Learning JavaScript : 예전에 자바스크립트 공부를 위해 산 책인데, 이번에 제대로 읽어봤다. 내용이 매우 충실하다.

MDN의 Hoisting 문서 : 호이스팅의 로직을 정확히 설명하고 좋은 예제가 많은 포스팅이다. 추천.

MDN의 Closure 문서 : 클로저에 대해 책을 읽고도 이해하기 어려웠는데, 자세한 설명 뿐 아니라 좋은 활용예와 에러 사례까지 담아줬다. 관련해서 더 공부하고 싶다면 꼭 읽어보시길 권한다.

 

프론트엔드 학습경력이 길지 않아 잘못된 내용이 있을 수 있습니다.

잘못된 내용이 있다면 날카로운 지적 부탁드립니다!

반응형

댓글