회사에서 프론트엔드 프로젝트에 투입되어 자바스크립트를 배우고 있습니다. 그러던 중, 괴상한 코드를 하나 마주하게 됩니다.
const component = () => {
//..
const functionA = (value) => {
//함수 로직
}
//..
functionA(whoAreYou); //whoAreYou는 component 내부 어디에도 선언되어 있지 않았음
//..
}
위의 코드를 작업하며 들었던 위화감, 무엇인지 보이시나요? functionA에 파라미터로 넘어간 whoAreYou라는 변수는 component의 스코프 어디에도 선언되어 있지 않은 녀석입니다. 변수로 선언이 되어 있지 않은 녀석을 사용하는데 빌드 에러부터 런타임 에러까지 없이 정상적으로 동작하고 있는 괴이한 현상을 마주했습니다.
도대체, 저 whoAreYou는 어디서 온 녀석일까요? 이 녀석이 어디서 왔는지 알기 위해선 우리는 자바스크립트의 변수 선언 방법에 대해 진지하게 살펴볼 필요가 있습니다.
이 글은 [10분 테코톡] 태태, 샐리의 var let const 비교를 보고 해당 영상의 내용을 정리, 내용을 추가한 글입니다. 이 영상을 한 번 보시는 것, 꼭 추천드립니다!
본격적으로 자바의 변수 선언 방식을 살펴보기에 앞서 우리는 많은 사람들이 말하는 ES6라는 것을 간단하게 알아보고 넘어가야겠습니다.
ES 는 ECMAScript의 약자로 ECMA-262 기술 규격에 정의된 표준화된 스크립트 프로그래밍 언어, 보다 쉽게 말하자면 자바스크립트의 표준 문법을 말합니다. (물론 꼭 자바스크립트가 아니더라도 액션 스크립트, J스크립트 등 다양한 스크립트 구현체들에 해당합니다.)
1990년대 자바스크립트의 등장 이후 MS가 J스크립트를 개발하면서 비슷한 형태의 여러 스크립트 언어가 등장하고 사용되기 시작합니다. 이에 따라 여러 스크립트 언어들에 대한 표준의 필요성이 대두되었고 그렇게 등장한 것이 바로 ES입니다.
자바스크립트는 2015년까지 ES5의 문법을 따랐습니다. ES5의 문법을 따르는 동안에는 var를 중심으로 변수 선언이 이뤄졌는데 이후 ES6가 도입되면서 let과 const가 새롭게 등장합니다. var, let, const 3개 모두 자바스크립트의 변수를 선언하지만, var가 내포하고 있는 불안한 문제를 해결하기 위해 let과 const가 변수 선언의 중심에 서게 됩니다.
var는 중복 선언이 가능합니다. 이게 무슨 말인지 아래의 코드를 한 번 같이 봅시다.
var x = 1;
var y = 1;
var x = 100; //여기서 x를 한 번 더 선언할 수 있음 -> 딱 봐도 무슨 문제를 일으킬지 보이죠?
var y; //값이 할당된 y를 다시 선언할 수 있음
console.log('x : ', x) // 100
console.log('y : ', y) // 1
이미 var x = 1
이 선언되어 있는 상태에서 var x = 100
을 또 선언할 수 있다는 것입니다. 개발하는 과정에 대입해서 생각해보면 개발자 A가 먼저 x와 y를 선언하고 값을 할당해서 작업을 했습니다. 이 후, 시간이 흘러 다른 개발자 B가 x와 y가 선언되어 있는지 모르고 x와 y를 다시 선언해 사용할 수 있다는 것입니다. var 자체는 중복 선언이 가능하기 때문에 런타임 시에 오류가 발생하지 않지만, 개발자 B는 의도하지 않는 값을 받아보게 되는 것입니다.
이 주제를 다루기에 앞서 간단한 코드를 하나 살펴보고 가겠습니다.
var x = 1;
var condition = true;
if(condition) {
var x = 100;
}
console.log('x : ', x) // 100
일반적인 언어, 쉽게 생각하면 자바에서 선언한 변수의 생명 주기는 선언된 스코프를 따라갑니다. 예를 들어 if문 안에 선언된 x라는 변수는 if문이 끝나고 나면 더 이상 유효하지 않은 변수일텐데, var로 선언된 이 녀석은 끝까지 살아남아 100이라는 값을 넘겨 주고 갑니다. 이게 도대체 무슨 일일까요?
많은 사람들이 놓치면서도 중요하게 작용하는 var의 특징 중 하나가 바로 var는 함수 레벨의 스코프를 가진다는 것입니다.
var는 함수 블럭(if, for …)이 아닌 다른 블록에서 선언되었다면, 해당 변수는 전역 변수로 선언되게 됩니다. 즉, 이 특징을 이해하지 않고 var를 선언해서 사용했을 경우 의도치 않게 전역 변수를 중복해서 선언할 가능성이 높아지게 됩니다.
변수 호이스팅이란 변수 선언문이 코드의 선두로 끌어 올려진 것처럼 동작하는 자바스크립트의 고유의 특징입니다. 자바스크립트 엔진은 코드를 실행하기 전에 한 줄씩 소스 평가를 진행한 후 코드를 실행하게 됩니다.
console.log(x);
var x;
x = 100;
console.log(x);
위의 코드를 보고 실행 순서가 어떻게 되는지 예상을 해봅시다. 먼저 console.log(x);
에 작성되어 있는 변수 x를 먼저 찾게 됩니다. 이로 인해 실제 실행할 때 var x;
코드를 가장 먼저 실행한 것처럼 동작하게 됩니다. (자바라는 정적 언어를 중심으로 개발하던 저에게 아직 선언되지 않은 변수가 코드 최상단으로 끌어 올려져서 선언된 것 처럼 사용되는 모습이 마냥 좋아보이지는 않습니다만, 이러한 유연성이 자바스크립트가 가지고 있는 장점 중 하나라고 생각합니다.)
(이미지 출처 : 나)
그렇다면, 이러한 변수 호이스팅으로 인해 어떤 문제가 발생할지 살펴봅시다.
console.log(x);
x = 100;
console.log(x);
var x;
(이미지 출처 : 나)
위의 코드를 호이스팅을 통해 실행한 순서는 위의 사진과 같습니다. 변수 x가 선언된 것으로 인식하고 첫번째 console.log(x)
에는 undifined가 출력될 것입니다. 그 다음 변수에 값이 할당되고 마지막 console.log(x)
에는 100이 출력될 것입니다.
즉, 우리가 임의로 코드를 작성해도 컴파일 레벨에서 오류를 잡아주는 것이 아니라 코드를 재해석해 실행됩니다. var가 가지고 있는 특징 중 유연한 변수 호이스팅은 코드의 실행에 대한 직관성 및 가독성을 저해하고 에러가 어디서 발생할지 예측할 수 없기 때문에 에러의 발생 가능성을 높이고 원인 분석을 어렵게 만듭니다.
var는 변수 호이스팅에서도 살펴 보았지만 굉장히 유연하게 선언하고 처리할 수 있는 녀석입니다. 그리고 var의 특징 중 지금 다룰 특징은 이 글을 작성하게 된 진짜 이유이기도 합니다.
자바스크립트는 var, let, const 없이 그냥 변수만 선언이 가능하고 이렇게 선언한 변수에 값을 할당 하는 것이 가능합니다. 문제는 이렇게 선언한 변수는 var로 선언된 것으로 간주해 처리됩니다. 위에서 살펴본 함수 레벨 스코프의 특징이 문제가 되는 부분은 여기입니다. 함수 레벨의 스코프 외에서 선언된 var 변수는 전역 변수로 인식이 됩니다.
이 글 처음 보았던 문제가 됬던 코드를 다시 한 번 봅시다.
const component = () => {
//..
const functionA = (value) => {
//함수 로직
}
//..
functionA(whoAreYou); //whoAreYou는 component 내부 어디에도 선언되어 있지 않았음
//..
}
whoAreYou라는 변수는 component 어디에도 선언되어 있지 않습니다. 하지만 변수 호이스팅에 의해 이미 선언된 변수처럼 동작하고 있거나, component 외부 다른 스코프에 선언되어 이미 전역 변수가 되어 있을 가능성이 있는 것입니다. 이렇게 되면 whoAreYou라는 변수는 무엇을 가리키는지, 어떠한 값을 가지고 있는지 전혀 알 방법이 없게 됩니다.
한창 코드를 뒤져가며 찾아가던 중 드디어 whoAreYou를 찾았습니다.
const otherComponent = () => {
//..
whoAreYou = 10;
//..
}
그런데, var의 특징을 종합적으로 고려하면 이 녀석은 otherComponent에서 맨 처음 선언한 녀석일까요? 아니면, 이미 다른 곳에서 선언한 전역 변수를 가져와서 쓰고 있는 녀석일까요? whoAreYou를 찾아가는 과정에서 이런 유연함에 화가 많이 났습니다. 해당 스코프에서 선언된 녀석인지 다른 곳에서 선언해 값을 할당해주고 있는 코드인지 알 방법은 없었습니다. bad practice를 몸으로 경험한 셈이 되었습니다.
whoAreYou는 var 없이 선언되었지만 var로 선언된 것으로 동작하면서 전역 변수로 선언됩니다. 그리고 변수 호이스팅이 이 문제를 극대화 시킵니다.
변수 호이스팅을 할 때 해당 변수가 쓰인 곳에 변수의 선언이 없다면 더 큰 스코프로 넘어가 선언이 되었는지 찾게 됩니다. 거기에도 없다면 더 큰 스코프로 넘어가서 변수를 찾는데 이 과정을 반복해 변수를 찾게 되면 해당 변수는 전역 변수로 올려 놓고 사용하게 됩니다.
아까 살펴보았던 변수 호이스팅의 문제가 여실히 드러나는 예시로, component에서 선언되지 않은 변수가 쓰이니 가독성과 코드의 직관성이 떨어지고, 해당 변수가 선언된 곳을 찾을 수 없게 되는 문제가 발생합니다. 저는 운이 좋게도 해당 변수가 오류를 뿜어내는 녀석이 아니었기 때문에(혹은 오류를 뿜어내는 녀석인지도 몰랐을 수 있습니다…) 넘어 갔습니다만, 결코 좋은 코드는 아니란 것을 배웠습니다.
결론은 var를 쓰지 말고, 전역 변수를 쓰지않은 것이 좋겠습니다. 프로그램이 고장나도 어디서 문제를 일으키는지 찾을 수 없을 뿐더러 이걸 사용하게 되면 어제의 나는 어디가 문제인지 알겠지만, 오늘의 나는 어디가 문제인지 모르게 되는 일이 발생할 것입니다.
let과 var의 차이 중 하나는 바로 변수를 중복해서 선언할 수 없다는 것입니다. 아래와 같이 동일한 변수명을 선언해 사용하게 된다면 문법 에러가 발생하게 됩니다.
let x = 100;
let x = 200; //syntax error -> 즉 이렇게 사용은 불가
let은 var와는 다르게 블록 레벨의 스코프를 가집니다.
자바스크립트의 스코프는 ES5까지는 전역(global)과 함수 스코프만을 사용했는데, 이는 var가 함수 레벨의 스코프를 가질 수 밖에 없었던 이유입니다.
반면, let과 const가 추가된 ES6부터 block, local 스코프 사용이 가능해지는데, let은 코드 블럭을 local 스코프로 보는 블록 레벨 스코프를 따릅니다. 일반적인 언어에서 다루고 있는 스코프처럼 선언한 스코프가 다르면 다른 변수로 인식이 되는 것입니다.
let x = 100;
const printX = () {
let x = 200;
console.log(x);
}
console.log(x); //100
printX() //200
var로 선언된 변수의 호이스팅은 아래의 코드처럼 선언되지 않은 변수가 사용될 경우, 마치 선언한 것처럼 호이스팅이 일어납니다.
console.log(x); //undefined
var x;
그러나 아래의 코드를 보시면 let은 referenceError가 발생하면서 코드가 동작하지 않게 됩니다.
console.log(x); //referenceError 발생 -> 일시적 사각지대
let x;
console.log(x); //undefined
x = 100;
console.log(x); //100
호이스팅을 하기 전에 코드 평가가 진행되는데 이 때 선언 단계와 초기화 단계 2가지 단계로 나눠집니다.
선언 단계는 자바스크립트 엔진에 변수의 존재를 알리고, 초기화 단계는 변수를 undefined로 초기화를 진행합니다. var는 선언 단계와 초기화 단계의 구분 없이 코드 평가 단계에서 선언과 초기화를 같이 진행하기 때문에 undefined로 초기화 되면서 동작 시 에러를 일으키지 않습니다.
반면 let은 호이스팅이 동작하지 않은 것 처럼 보이는데 이는 선언 단계와 초기화 단계가 분리되어서 실행되기 때문입니다. 위의 코드를 보면 자바스크립트 엔진은 x의 값을 초기화 단계로 가기 전까지는 알 수 없습니다. 그렇기 때문에 let x;
에 앞서 console.log(x);
에 접근을 하면 값을 알 수 없기 때문에 referenceError(참조 에러)가 발생하게 됩니다.
이처럼 스코프 시작 지점부터 초기화 시작 지점까지 변수를 참조할 수 없는 구간을 일시적 사각지대라고 부릅니다. let에서 변수 초기화 단계는 변수가 선언되는 지점에서 진행이 되는데 위의 코드로 보자면 let x;
에서 초기화 단계가 시작되는 것입니다.
(이미지 출처 : 나)
const는 let과 동일한 특징을 가지고 있지만, 선언과 초기화 및 재할당 금지, 상수 선언의 특징을 더 가지고 있습니다.
const는 선언과 동시에 초기값을 가지고 있어야 하기 때문에 const x;
처럼 사용할 수 없습니다.
const x; //const는 선언과 동시에 초기값을 가지고 있어야함 -> 이렇게 쓰면 에러!
const x = 100; //요렇게 선언과 동시에 초기값을 넣어주어야 함 -> 상수를 선언하기 위해 사용
또한 상수 선언의 특징을 가지고 있기 때문에 이미 초기화된 값에 다른 값을 재할당 할 수 없습니다.
const x = 100;
x = 200; //이렇게 쓰면 TypeError가 발생
const가 가지고 있는 상수의 특징은 아래와 같습니다.
const로 변수를 선언하면 상수의 특징을 가지게 되어 중간에 값이 변할 수 없기 때문에 유지 보수를 하기 용이해집니다.
단, 원시 값은 내부적으로는 어떠한 변경으로 불가능하지만 객체로 선언하게 되면 내부 값은 언제든 변경할 수 있습니다.
const objectX = {
x: 100;
}
objectX.x = 200;
console.log(objectX.x); //200
객체로 선언 시 내부의 값을 바꿀 수 있지만, const로 선언된 objectX
에 새로운 객체를 할당하는 것은 불가능합니다.
결론은 글 중간에도 보셨듯이 딱 한 줄입니다.
var 쓰지말고 let, const 씁시다
let과 const를 사용하게 되면 변수의 이름이 중복되는 것을 방지할 수 있고 변수의 값이 할당 되기 전에 사용 할 때 예기치 못한 오동작을 방지합니다. 이는 일시적 사각지대(TDZ)가 생기는 것에 대한 이점입니다. 또한 블록 레벨의 스코프를 통해 변수 간의 차이를 명확하게 알 수 있습니다.
var 사용으로 인해 겪은 문제로 var 웬만하면 let과 const로 꼭 바꿔서 씁시다! 정말 어디서 프로젝트가 터지는지 정말 찾기 힘듭니다ㅠㅠ
https://medium.com/@wainy254/변수-선언-c696c3b9e787
https://velog.io/@bathingape/JavaScript-var-let-const-차이점
https://gmlwjd9405.github.io/2019/04/22/javascript-hoisting.html
https://www.youtube.com/watch?v=ZU4MXkwDb9g&list=PLgXGHBqgT2TvpJ_p9L_yZKPifgdBOzdVH&index=24&t=1s
https://ko.javascript.info/function-basics