[ JS ] - 심화학습 2

200원짜리개발자·2024년 6월 20일
0

FrontEnd

목록 보기
29/29
post-thumbnail

제로베이스 자바스크립트 기초개념 심화학습 부분 정리
축약된 부분이 존재할 수 있습니다.

렉시컬 스코프

우리가 화살표 함수에서 this키워드를 사용할 때 렉시컬 스코프에서 정의가 된다고 배웠다.

그래서 이번시간에 렉시컬 스코프에 대해서 좀 더 자세히 알아볼 것이다.

렉시컬 스코프(Lexical Scope)

  • 정적 스코프(Static 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)

  • 선입선출, 먼저 들어온 데이터가 먼저 나감
    LIFO(Last 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();

코드와 위 사진을 보며 어떤식으로 동작되는지 생각해보자!

profile
고3, 프론트엔드

0개의 댓글