
제로베이스 자바스크립트 기초개념 심화학습 부분 정리
축약된 부분이 존재할 수 있습니다.
우리가 화살표 함수에서 this키워드를 사용할 때 렉시컬 스코프에서 정의가 된다고 배웠다.
그래서 이번시간에 렉시컬 스코프에 대해서 좀 더 자세히 알아볼 것이다.
렉시컬 스코프(Lexical Scope)
const a = {
fnA() {
console.log("fnA", this);
const b = {
fnB() {
console.log("fnB", this);
const c = {
fnC() {
console.log("fnC", this);
console.log("a", a);
console.log("b", b);
console.log("c", c);
console.log("x", x);
},
};
return c;
},
};
return b;
},
fnX() {
console.log("fnX", this);
const x = {
fnY() {
console.log("fnY", this);
console.log("a", a);
console.log("b", b);
console.log("x", x);
},
};
},
};
a.fnA().fnB().fnC();
a.fnY();
이 코드를 보면 렉시컬 스코프에 대해서 이해할 수 있다.
fnC를 기준으로 설명을 해주게 된다면, fnC는 fnB에 속하고 fnB는 fnA에 속하기에 fnC입장에서 보면 fnA와 fnB의 범위가 유효한 범위이다. 하지만 fnC입장에서 fnX의 포함된 부분이 아니기에 fnC에서 fnX에 접근을 할 수 없다.
이렇게 fnC의 상위의 범위를 렉시컬 스코프(정적 스코프)라고 부른다. 왜 정적 스코프라고도 부르냐면 fnC를 만드는 단계에서 정적으로 유효 범위가 정해지기 때문이다.
물론 위 코드는 일반 함수이기에 호출된 위치에서 this키워드가 정의되지만 화살표함수라면 가장 먼저 만난 일반 함수의 this를 사용하게 될 것이다.
클로저(Closure)는 함수가 선언될 때의 렉시컬 스코프를 기억하고 있다가, 함수가 호출될 때 그 스코프에 접근할 수 있는 개념(특성)을 말합니다.
let count1 = 0;
function c1() {
return (count1 += 1);
}
console.log(c1());
console.log(c1());
console.log(c1());
let count2 = 77;
function c2() {
return (count2 += 1);
}
console.log(c2());
console.log(c2());
console.log(c2());
////////////////////////////////////////////////
function createCount(count) {
return function () {
return (count += 1);
};
}
const c3 = createCount(0);
console.log(c3());
console.log(c3());
console.log(c3());
const c4 = createCount(77);
console.log(c4());
console.log(c4());
console.log(c4());
구분선 부분의 위쪽을 보게 되면 변수와 함수를 같이 만들어야지 함수를 사용해서 변수의 값을 늘릴 수 있다.
이 부분을 축소하기 위해서 우리는 클로져라는 개념을 사용할 수 있다.
구분선 아래 부분을 보면 c3이 함수를 반환받는데 c3라는 함수는 count라는 변수가 정의되어 있지 않다. 하지만 함수에서 반환을 할 때 count라는 함수를 사용하고 있기 때문에, c3함수가 호출이 될 때, 그 함수가 만들어질 때의 렉시컬 스코프를 가지고 있어서 count라는 변수에 접근할 수 있는 개념(특징)이다.
클로저의 사용예시
const h1El = document.querySelector("h1");
let h1IsRed = false;
h1El.addEventListener("click", () => {
h1IsRed = !h1IsRed;
h1El.style.color = h1IsRed ? "red" : "black";
});
이런식으로 변수와 함수를 분리해서 사용하는 경우가 있는데 이러한 상황이 계속된다면 변수를 계속 만들어야 하는 불편함이 있을 것이다.
이것을 클로저라는 특성을 활용하여 현재 코드를 더 효율적으로 만들어 관리할 수 있다.
const h1El = document.querySelector("h1");
const createToggleHandler = () => {
let isRed = false;
return (event) => {
isRed = !isRed;
event.target.style.color = isRed ? "red" : "black";
};
};
h1El.addEventListener("click", createToggleHandler());
이런식으로 코드를 작성하게 되면, 재사용도 가능하고 효율성도 좋아지기에 클로저를 사용할 수 있는 상황에서 사용하면 좋다.
가비지 컬렉션
가비지 컬렉션을 효율적으로 동작하기 위한 주의해야 할 점을 알아볼 것이다.
불필요한 데이터 참조를 피하세요!
// 불필요한 데이터 참조를 피하세요!
const user = {
name: "Neo",
age: 85,
emails: ["abc@gmail.com", "xyz@naver.com"],
};
const removedEmail = user.emails.splice(1, 1);
console.log(removedEmail);
console.log(user.emails);
이런식으로 splice로 잘라서 확인하기 위해 변수 담아둔다면 가비지 컬렉션이 메모리를 순회하면서 저 부분을 찾아도 removedEmail이 참조를 하고 있기에 지울 수가 없어진다.
그렇기에 확인을 하고나면 변수를 지워줘야 한다.
불필요한 전역 변수 사용을 피하세요!
// 불필요한 전역 변수 사용을 피하세요!
window.hello = "Hello world!";
window.thw = { name: "200won", age: 85 };
우리가 어디에서나 접근할 수 있는 객체를 전역 객체라고 부른다.
window도 전역 객체이다. 이렇게 전역 객체에서 어떤 속성에 데이터를 할당하게 되면 우리가 직접 제거를 하지 않는 이상 데이터를 제거하는 상황을 만들기가 어렵다. 그렇기에 전체영역에서 사용할 수 있는 변수에게 데이터를 만드는 행위를 주의해야 한다.
제거된 요소가 참조되지 않도록 주의하세요!
const h1El = document.querySelector("h1");
window.addEventListener("click", () => {
console.log(h1El);
h1El.remove();
});
이렇게 되면 우리는 querySelector를 활용해서 h1El에 할당한 것이기에 화면상에서는 제거 되었지만, 저 변수가 사라지지 않는다면 메모리상에 계속 존재해 사용할 수 있게 된다.
window.addEventListener("click", () => {
const h1El = document.querySelector("h1");
if (h1El) {
console.log(h1El);
h1El.remove();
}
});
이런식으로 코드를 작성하게 되면, 완전히 가비지 컬렉션을 사용해서 제거할 수 있다.
불필요한 타이머를 해제하세요!
// 불필요한 타이머를 해제하세요!
let a = 0;
setInterval(() => {
a += 1;
}, 100);
setTimeout(() => {
console.log(a); // 10
}, 1000);
이 부분의 코드는 메모리가 계속 낭비되고 있는 중이다. 왜냐하면 setInterval을 사용하면 setTimeout으로 값을 확인하고 나서도 계속 값이 늘어나기 때문이다.
그러기에 타이머를 해제하는 코드가 필요해진다.
// 불필요한 타이머를 해제하세요!
let a = 0;
const intervalId = setInterval(() => {
a += 1;
}, 100);
setTimeout(() => {
console.log(a); // 10
clearInterval(intervalId);
}, 1000);
이런식으로 clearInterval이라는 함수를 사용해서 interval을 멈춰줄 수 있다.
불필요한 클로저를 제거하세요!
// 불필요한 클로저를 제거하세요!
const getFn = (x) => {
return (name) => {
x += 1;
console.log(x);
return `Hello ${name}~`;
};
};
const fn = getFn(0);
console.log(fn("Neo"));
console.log(fn("Lewis"));
fn("Evan");
fn("Amy");
getFn을 호출하여 fn이 함수를 리턴받게 되었는데 그 함수에서 x가 사용되기에 렉시컬 스코프에서 x를 가져와서 사용하게 된다. 하지만 x는 출력하는 행위 외에는 아무 행동도 하고 있다. 이렇게 클로저가 발생되어 의미없는 변수를 참조해서 메모리가 사용되는 현상을 피해야한다.
지금까지 우리가 본 예시들 전부 불필요한 것들을 사용하지 않게 만드는 것이였다.
그래서 우리가 코드를 작성할 떄는 꼭 필요한 내용만 넣어줘야 한다. 개발이 끝나게 되면 console.log같은 부분은 다 지워줘서 메모리를 불필요하게 차지하는 데이터를 사용하지 않게 만들 수 있다.
자바스크립트는 저수준의 오래 걸릴 수 있는 일(Timer, Network 등)은 Web API에게 위임하고, 고수준의 작업은 자바스크립트 엔진(싱글 스레드)에서 처리하는 방식으로 빠른 속도와 확장성을 유지합니다.
setTimeout(() => {
console.log(1);
}, 0);
window.addEventListener("load", () => {
console.log(2);
});
fetch("/").then(() => console.log(3));
for (let i = 0; i < 1000; i++) {
console.log(4);
}
그래서 이런 코드를 실행하였을 때, 작성한 순서대로 실행되는 것이 아니라 다른게 실행 될 수도 있는 현상이 발생한다.
그럼 이제 콜 스택과 이벤트 루프에 대해서 알아보기 전에 두 가지 용어만 정리하고 가보자
FIFO(FIrst In First Out)

자바스크립트 엔진입장에서는 Heap이라는 영역으로 메모리가 관리가 되고, Call Stack 즉 호출된 함수들의 내역이 쌓이는 곳이다.
이 부분의 JS Runtime이 자바스크립트 엔진이 동작하는 부분이다.
그리고 자바스크립트는 저수준의 오래걸리는 일들은 엔진에서 하지않고 브라우저에서 동작한다고 하였는데

바로 그것이 Web API라는 것이다. 이렇게 오래걸릴 수 있는 일들이 끝나게 되면 콜 스택으로 가는 것이 아니라. Queue라는 영역으로 가서 하나씩 쌓이게 된다.

Queue영역에서는 Call Stack의 함수가 전부 호출되서 비워지면 Event Loop라는 것을 통해서 Queue에 있는 순서대로 Call Stack으로 올라가 처리가 될 수 있다.
function a() {
console.log("A");
function b() {
setTimeout(() => {
console.log("B1");
console.log("B2");
}, 0);
}
b();
}
function c() {
console.log("C");
}
function first() {
a();
c();
}
function second() {
c();
}
first();
second();
코드와 위 사진을 보며 어떤식으로 동작되는지 생각해보자!