이번 글에서는 자바스크립트 관련해서 변수 선언과 관련된 팁들을 글로 적어 공유하고자 한다.
ES6 에서 등장한 const
로 변수를 선언하는 방법은, 기존의 var 키워드와 달리 재할당을 금지한다.
때문에 일반적으로 코드를 작성할 때는 const 를 우선으로 쓰는 것이 좋다.
그 이유는 가장 할 수 있는 것이 적기 때문에, 가장 안전하기 때문이다.
var taxRate = 0.1
var total = 100 + (100 * taxRate)
// 약 100줄의 코드
return `구매 금액은 ${total}입니다.`
위의 코드의 문제점이 무엇일까.
var 로 선언한 변수의 경우 재할당과 중복 선언 모두가 가능하기 때문에, 저기 숨은 100줄의 코드에서 어떤식으로 변경이 되었는지 일일히 확인해봐야 한다는 점이다.
const taxRate = 0.1
const total = 100 + (100 * taxRate)
// 약 100줄의 코드
return `구매 금액은 ${total}입니다.`
반면 이 코드는 저 100줄의 코드를 보지 않아도 taxRate 와 total 이란 변수가 재할당이 안 될 것이기 때문에 값을 정확히 예측할 수 있다.
가급적이면 const 키워드로 변수를 선언하는 이유는, 예측 가능하며 예측 불가능한 변수값 변경으로부터 안전하기 때문이다!
이게 무슨 뜻일까?
말 그대로 const 에 할당한 값이 불변값을 의미하지는 않는다는 뜻이다.
변수를 재할당하는 것은 불가능하지만, 값을 바꾸는 것은 가능하다.
const discountable = []
for (let i = 0; i < cart.length; i++) {
if (cart[i].discountAvailable) {
discountable.push(cart[i])
}
}
위의 코드에서 discountable 변수에 할당된 배열에는 아이템이 추가될 수 있다. 즉 const 로 선언한 변수라도 값이 변할 수는 있다.
하지만 이 경우 const 를 사용하는 의미가 있을까?
때문에 const 로 선언한 변수의 경우는 될 수 있으면 조작(mutation)을 피하는 것이 좋다!
const discountable = cart.filter(item => item.discountAvailable)
discountable 배열의 아이템 자체는 변경될 수 있다.
그러나 코드상에서 더 이상의 조작이 가해지지 않기 때문에 해당 변수의 특성을 유지할 수 있다.
값이 변경되는 경우에는 let
이 아주 좋은 선택지가 될 것이다.
var
는 lexical scope 를 따르지만, let
과 const
는 block scope 를 따르면서 둘 간의 차이가 발생한다.
먼저 코드의 요구사항을 확인해보면,
- 재고가 없으면 0 을 반환한다.
- 어떤 상품이 할인 중이고 재고가 있다면 할인 가격을 반환한다.
- 어떤 상품이 할인 중이 아니거나 할인 중이라도 할인 상품의 재고가 없다면 정상 가격을 반환한다.
function getLowestPrice(item) {
var count = item.inventory
var price = item.price
if (item.salePrice) {
var count = item.saleInventory
if (count > 0) {
price = item.salePrice
}
}
if (count) {
return price
}
return 0
}
위 코드는 헷갈릴 수 있지만 치명적인 문제점을 안고 있다.
var 키워드는 렉시컬 스코프, 즉 함수레벨 스코프를 가진다. 블록레벨 스코프가 아니다.
때문에 하나의 함수 안에서 count 변수가 재할당이 일어나면서 버그를 일으키게 되고, 의도한 동작이 이루어지지 않게 될 수 있다.
때문에 이런 경우를 방지하기 위해서라도 let
키워드로 변할 수 있는 값의 변수를 선언해주는 것이 좋다.
먼저 자바스크립트로 DOM 을 조작하는 경우를 보자
<ul>
<li> 클릭하면 0 </li>
<li> 클릭하면 1 </li>
<li> 클릭하면 2 </li>
</ul>
const items = document.querySelectorAll('li')
for (var i = 0; i < items.length; i++) {
items[i].addEventListener('click', () => alert(i))
}
이 코드는 결과적으로 어떤 li 요소를 클릭해도 3을 출력한다.
밑에 코드를 봐보자
function addClick(items) {
for (var i = 0; i < items.length; i++) {
items[i].onClick = function () { return i }
}
return items
}
const example = [{}, {}, {}]
const clickSet = addClick(example)
clickSet[0].onClick() // 2
clickSet[1].onClick() // 2
clickSet[2].onClick() // 2
DOM 을 직접 조작하는 경우가 아니더라도 위의 코드는 항상 숫자 2를 리턴한다. 왜일까?
이는 바로 유효 범위의 문제다.
var 로 할당한 변수는 함수 유효 범위를 따른다. 즉, 함수 내에서 마지막으로 할당한 값을 참조한다.
함수 밖에서 var 로 변수를 선언했을 때 함수는 항상 가장 마지막에 재할당된 변수를 참조하기 때문에 위의 두가지 경우의 코드 모두 가장 마지막 i 값을 가지는 것이다.
이 방법을 해결하기 위한 전통적인 방법은 클로저, 고차함수, 즉시실행함수를 이용하는 것이었다.
function addClick(items) {
for (var i = 0; i < items.length; i++) {
items[i].onClick = (function (i) {
return function () { return i }
}(i))
}
return items
}
const example = [{}, {}, {}]
const clickSet = addClick(example)
clickSet[0].onClick() // 0
clickSet[1].onClick() // 1
clickSet[2].onClick() // 2
클로저는 외부함수의 내부 값을 기억하고 은닉한다.
고차함수는 즉시 실행 함수로써 그 자리에서 실행되어 클로저를 리턴하고 있다.
클로저는 var 로 선언한 변수 i 가 가지고 있는 값을 기억하고 그것을 그대로 리턴한다.
때문에 의도한대로 동작을 수행할 수 있다.
그러나 문제는 이 방법이 다소 복잡하다는 것이다.
이것을 let 은 쉽게 해결할 수 있다.
function addClick(items) {
for (let i = 0; i < items.length; i++) {
items[i].onClick = function () { return i }
}
return items
}
const example = [{}, {}, {}]
const clickSet = addClick(example)
clickSet[0].onClick() // 0
clickSet[1].onClick() // 1
clickSet[2].onClick() // 2
let 을 통해서 대단한 변경을 하지 않았다. 단지 var 키워드를 let 키워드로 변경한 것이 다지만, 원하는 동작을 수행할 수 있게 됐다.
이게 가능한 이유는, let 이 블록 레벨 스코프를 따르기 때문이다.
블록 내에서 선언한 변수는 해당 블록에서만 유효하다.
때문에 반복되어 값이 변경되어도, 이전에 선언한 함수의 값은 변경되지 않는다.
따라서 var 로 할 수 있는 거의 모든 것이 let 으로 대체가 가능하므로 가급적이면 let 을 사용하는 것이 좋다.