JS 실행 컨텍스트와 클로저

wkahd01·2021년 12월 31일
3

JavaScript

목록 보기
20/22
post-thumbnail

실행 컨텍스트

Intro

실행 컨텍스트소스코드를 실행하는 데 필요한 환경을 제공하고 코드의 실행 결과를 실제로 관리하는 영역이다. 단순하게 코드의 실행 환경이라고 생각할 수 있다.

실행 컨텍스트를 이해하면 바인딩, 호이스팅, 클로저, 이밴트 핸들러, 비동기 처리 등 자바스크립트의 핵심 개념들을 쉽게 이해하고 접근할 수 있는만큼 중요한 개념이다.

자바스크립트의 소스코드 처리 과정

자바스크립트는 우리가 짠 코드를 크게 2가지 과정을 거쳐 처리한다.
1. 소스코드 평가
2. 소스코드 실행

의 과정을 거치는데 각 과정을 자세히 살펴보면 다음과 같다.

  1. 소스코드 평가
    1.1. 실행 컨텍스트 생성
    1.2. 선언문 실행
    1.3. 스코프 등록
  1. 소스코드 실행
    2.1. 런타임이 시작되어 소스코드가 순차적으로 실행(선언문 제외)
    2.2. 실행 결과는 다시 실행 컨텍스트가 관리하는 스코프에 등록

이해를 위해 예시를 들어보겠다.

var x = 10;
var y = 20;
console.log(x+y);

1. 소스코드 평가
전역 코드의 변수 선언문과 함수 선언문이 먼저 실행되고, 생성된 변수와 함수가 실행 컨텍스트의 전역 스코프에 등록된다.
즉, x와 y가 undefined의 값을 가진 채, 전역 스코프에 먼저 등록된다.

2. 소스코드 실행
전역 변수에 값이 할당되고 함수가 호출된다.
x에는 10이, y에는 20이라는 값이 할당되고, console.log(x+y);가 순차적으로 실행된다.

실행 컨텍스트 스택

실행 컨텍스트를 해석하면 실행 문맥. 즉, 코드의 실행 환경이다. 따라서, 이를 이용해 전역 환경과 함수 환경을 분리할 수 있다.

앞서 설명했던 소스코드 처리 과정은 중간에 함수가 있으면, 전역이 아닌 함수 내부에서 동작되어야 하기 때문에, 스택을 통해 해당 과정을 처리한다.

이해를 돕기 위해 예시를 들어보겠다.

var x = 10;
var y = 20;
function sum(x,y){
  var z = 30;
  return x+y+z;
}
console.log(sum());

위와 같은 코드에서, z변수는 전역 환경에 저장되면, 함수을 나와서도 해당 변수가 남아있기 때문에, 전역에서 이를 모두 관리하기에는 부적절하다.

따라서 sum 함수와 관련된 실행 컨텍스트를 생성에 이를 관리한다.


실행 컨텍스트 스택은 아래와 같이 동작한다.

  1. 실행 컨텍스트에서 전역 실행 컨텍스트를 생성
    x, y, sum, console가 소스코드 처리 과정을 통해 평가되고 실행됨

  2. 실행 컨텍스트에서 sum 함수 컨텍스트를 생성
    z가 소스코드 처리 과정을 통해 평가되고 실행됨

  3. 실행 컨텍스트에서 sum 함수 컨텍스트를 삭제

  4. 실행 컨텍스트에서 전역 실행 컨텍스트 삭제

렉시컬 환경

앞선 과정에서 소스코드가 어떻게 처리되는지, 실행 컨텍스트 스택이 어떻게 동작하는지 배웠다.

렉시컬 환경은 코드가 어디서 실행되며 주변에 어떤 코드가 있는지를 뜻하며, 자세히는 식별자와 식별자에 바인딩된 값, 상위 스코프에 대한 참초를 기록하는 자료구조이다.

즉, 실행 컨텍스트에 렉시컬 환경이 생성되며, 이를 통해 식별자를 검색하고, 코드 실행 결과를 관리할 수 있다.

아래의 코드로 실행 컨테스트와 렉시컬 환경의 조합을 설명하겠다.

var x = 10;
const y = 20;

function edit(a){
  var x = 1;
  const y = 2;

  return x+y+a;
}
edit(12);


전역 실행 컨텍스트를 생성하고, 전역 렉시컬 환경을 바인딩 시켜준다.
이후, 전역 렉시컬 환경에 속하는 전역 환경 레코드는 객체 환경 레코드와 선언적 환경 레코드로 구성된다.

  • 객체 환경 레코드
    var 키워드로 선언한 전역 변수, 함수 선언문으로 정의된 전역 함수가 들어감

  • 선언적 환경 레코드
    객체 환경 레코드에 들어가지 못했던 선언들이 들어간다.
    let, const가 이에 해당되며, 이외에도 해당 키워드로 선언한 변수에 할당된 함수 표현식 또한 여기에 해당된다.
    let, const 키워드로 선언한 전역 변수는 전역 객체의 프로퍼티가 아닌 개념적인 블록인 선언적 환경 레코드에 존재하게 된다.

1단계: 전역 실행 컨텍스트(소스코드 평가)

  1. 전역 실행 컨텍스트 생성
  2. 전역 렉시컬 환경 생성
    2.1 전역 환경 레코드 생성
      2.1.1. 객체 환경 레코드 생성
      2.1.2. 선언적 환경 레코드 생성
    2.2 this 바인딩(window에 바인딩됨)
    2.3 외부 렉시컬 환경에 대한 참조 결정

  • 객체 환경 레코드
    객체 환경 레코드에는 var 키워드로 선언한 전역 변수인 x와,
    함수 선언문으로 정의된 전역 함수인 edit이 들어간다.
    이 때, var 키워드는 선언과 초기화 단계가 동시에 진행되는 특징으로 인해, undefined를 암묵적으로 바인딩한다.

  • 선언적 환경 레코드
    선언적 환경 레코드에는 const 키워드로 선언한 변수인 y가 들어간다.
    이 때, const, let 키워드는 선언 단계와 초기화 단계가 분리되어 진행되기에, <uninitialized>를 통해 초기화 단계가 진행되지 않아 변수에 접근할 수 없다는 상태를 알려준다.

2단계: 전역 실행 컨텍스트(소스코드 실행)

전역 코드가 순차적으로 실행되기 시작한다. 따라서, x와 y 전역 변수에 값이 순차적으로 할당된다.

3단계: edit 함수 실행 컨텍스트(소스코드 평가)

  1. 함수 실행 컨텍스트 생성
  2. 함수 렉시컬 환경 생성
    2.1 함수 환경 레코드 생성
    2.2 this 바인딩(window에 바인딩됨)
    2.3 외부 렉시컬 환경에 대한 참조 결정


기존 전역 실행 컨텍스트는 전역 환경 레코드가 2가지로 나뉘었다면,
함수 환경 레코드는 한 공간 안에서 처리하게 된다.

따라서 a, argument, x, y, sum 모두가 함수 환경 레코드에 저장되게 된다.

여기서 argument는 한국어로 인자를 뜻하며, 함수 또는 메서드를 호출할 때 함수의 매개변수로 전달하는 값을 말한다. 여기서 argument는 edit(12);에서 12가 된다.

4단계: edit 함수 실행 컨텍스트(소스코드 실행)


런타임이 시작되어 소스코드가 실행되면,
매개변수에 인수가 할당되고 지역 변수 x, y에 값이 할당된다.

이후, 실행 중인 edit 함수 실행 컨텍스트의 렉시컬 환경에서 식별자를 검색한다.
해당 예시에서는 식별자 x, y가 모두 edit 함수 내부에 존재하므로 외부 렉시컬 환경(빨간 선)을 참조할 필요가 없다.

외부 렉시컬 환경 참조

앞서 사용했던 코드에서 edit 함수 내부에 const y = 2;가 없다면 자바스크립트는 어떻게 동작할까?

개발자 도구의 Breakpoints를 통해 어떻게 참조하는지 확인해 보겠다.

기존 코드

var x = 10;
const y = 20;

function edit(a){
  var x = 1;
  const y = 2;

  return x+y+a;
}
edit(12);

기존 코드의 경우, Call Stack에서 edit 함수 실행 컨텍스트가 실행되고, 해당하는 스코프를 보면 Script의 y:20을 참조하지 않았다는 것을 볼 수 있다.

변형한 코드(edit에 const y=2가 없음)

var x = 10;
const y = 20;

function edit(a){
  var x = 1;

  return x+y+a;
}
edit(12);

변형한 코드를 보면 edit 실행 컨텍스트 스코프에서 Script(객체 환경 레코드)의 y를 참조해(외부 렉시컬 환경 참조) x+y+a 연산을 수행하였다.

클로저 (Closure)

클로저는 함수가 선언된 렉시컬 환경이다.
이런 클로저를 활용하면, 아래와 같은 동작이 가능하다.

외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료한 외부 함수의 변수를 참조할 수 있다.


클로저가 어떤 것인지 위의 사진을 통해 다시한번 설명하겠다.

10번 라인에서 print 라는 변수에 main()이 할당되었다. 이 때, main 함수 실행 컨텍스트가 생성된다.

이후, 10번 라인의 실행이 끝나고 11번 라인으로 넘어갔을 때, main 함수 실행 컨텍스트가 제거되고, 더 이상 참조되지 않는 변수들은 가비지 컬렉터에 의해 제거된다.

하지만 11번 라인을 실행한 결과, 1이 나오는데, 이러한 이유는 10번 라인의
const print = main() 코드에서 printValue 변수에 main 함수의 리턴값인 print() 함수가 할당되었기 때문이다.

즉, main의 내부 함수인 print 함수가 printValue 변수에 강제로 묶였고,
10번째 줄이 끝나서 main 함수가 삭제되도, 내부 함수인 print 함수는 삭제되지 않는 기묘한 현상이 발생했다는 뜻이다.

print 함수는 과거에 참조하고 있었던 main 함수의 변수들을 기억(x:1)하게 되는데,
이러한 원리는 main 함수의 실행 컨텍스트가 제거되도, 참조하고 있는 main 함수의 렉시컬 환경은 소멸되지 않기 때문이다.(누군가가 참조중이기 때문에 가비지 컬렉터가 해당 공간을 지울 수 없음)

클로저의 활용

외부 함수의 변수를 참조하는 특징 때문에, 상태를 안전하게 변경하고 유지하고 싶을 때 클로저를 주로 사용한다.

이를 통한 부수 효과로 상태를 은닉시킬 수 있다는 장점이 생긴다.

댓글의 좋아요 버튼을 클릭해 좋아요 횟수를 줄이거나 늘리는 경우, 해당 상태는 안전하게 보관되어야 하기 때문에 클로저를 활용해 은닉할 수 있다.

아래는 increase와 decrease 내부(중첩) 함수를 통해 좋아요 수를 제어하는 코드이다.

const like = (function(){
  let likeCount = 0;
  
  return function(){
    likeCount = change(likeCount);
    return likeCount
  }
}());

함수 내부에서 발생한 일이기 때문에, 외부에서는 직접적으로 likeCount를 변경할 수 없다.

따라서 likeCount를 변경하기 위해선 클로저의 특징을 활용해야 한다.

console.log(like.increase()) // 0 => 1
console.log(like.increase()) // 1 => 2
console.log(liek.decrease()) // 2 => 1
profile
꾸준하게 공부하기

0개의 댓글