본문 바로가기

스코프와 클로저

본 내용은 카일 심슨의 YOU DON'T KNOW JS 타입과 문법, 스코프와 클로저(2017)를 읽고 서술적인 내용을 Q&A 형식으로 정리한 글입니다.

 

들어가기 전에

개인적으로 이 장은 코어 자바스크립트에서 학습했던 실행 컨텍스트클로저를 참고하는 편이 훨씬 이해가 더 잘될거 같았다.

따라서 복습할 겸해서 정리하기로 하였다.

 

1. 스코프

1.1 자바스크릡트의 컴파일레이션 과정을 설명하시오.

아래는 예시 코드

var a = 2

토크나이징(렉싱) 〰️파싱 〰️코드 생성으로 진행된다.

 

토크나이징(렉싱) Tokenizing(Lexing)

문자열을 공백 기준으로 나누어 의미 있는 조각(토큰)을 생성한다. 상태 유지 파싱 규칙을 적용하여 어떤 문자열이 별개의 토큰인지, 다른 토큰의 일부인지를 판단하는 것을 렉싱이라고 한다.

var, a, =, 2

 

파싱 Parsing

토큰들을 일정한 문법 규칙을 적용하여 tree를 생성한다.

파싱 트리 예시

코드 생성 Code Generation

var a = 2라는 대입문을 처리하기 위해 컴파일러는 var aa = 2로 나누어 작업한다.

 

1. var a

a라는 변수가 스코프 컬렉션에 선언되어 있는지 검색하여 선언되어 있지 않다면 스코프 컬렉션에게 a에 대한 선언을 요청한다.

 

2. a = 2

a = 2를 처리하기 위해 실행 코드를 만든다. 이 실행코드로 엔진은 현재 스코프부터 시작하여 현재 스코프가 중첩된 상위 스코프를 거슬러 올라가며 a를 검색하고, a를 찾으면 2를 대입한다.

[참고] 함수 선언문 또한 컴파일러가 코드 생성과정에서 미리 선언한다. 따라서 함수 선언문이 호출되면 엔진은 스코프에게 해당 함수가 선언되었는지만 검색한다.

1.2 스코프를 한 줄로 설명하시오.

확인자 이름으로 변수를 찾기 위한 규칙의 집합

 

1.3 LHS, RHS 참조가 각각 언제 수행되는지 설명하시오.

LHS 참조는 변수에 값을 대입할 때 수행한다. 스코프 컬렉션에서 해당 변수가 선언되어 있는지 검색하여 대입 연산을 수행한다.

RHS 참조는 변수의 값을 가져올 때 수행한다.

 

1.4 렉시컬 스코프에 대해 설명하시오.

위의 컴파일레이션 과정 중 렉싱 타임 때 정의되는 스코프이다.

즉, 개발자가 변수와 스코프 블록을 어디서 작성하는가에 기초하여 lexer가 코드를 처리할 때 확정된다.

 

(우리가 흔히 아는 것처럼) 중첩된 스코프 블록에서 확인자를 검색할 때 자신의 스코프를 시작으로 가까운 스코프 블록부터 가장 최상위의 스코프 블록 순으로 검색한다. 확인자를 찾으면 즉시 검색을 중단한다.

 

1.5 함수 스코프의 동작 방식에 대해 호이스팅과 연관되게 설명하시오.

코어자바스크립트 02. 실행컨텍스트를 참고하자. 직접 정리한 글은 여기로 🎶

함수가 호출되면 콜 스택에 실행 컨텍스트가 생성되는데, 실행 컨텍스트의 내부 구성 요소 중 LexicalEnvironment의 environmentRecord 에 해당 함수의 지역변수, 함수선언, 매개변수의 환경정보를 함수 실행 전 미리 저장해둔다. 이것을 호이스팅이라고 한다.

[참고] 호이스팅이 되는 원리를 1.1 컴파일레이션 과정 중 코드 생성 과정을 기반으로 설명할 수도 있다.
var a와 같은 선언문은 스코프 컬렉션에서 미리 선언되지만 a = 2와 같은 대입문은 원래 위치에서 행해지기 때문이다.

 

호이스팅의 규칙은 다음과 같다.

  • 변수의 선언부만 호이스팅되고, 할당은 원래 위치에서 실행된다.
  • 함수 선언문은 선언문 전체가 호이스팅되지만 함수 표현식은 변수처럼 동작한다.

이처럼 함수 내부에 선언된 변수, 함수는 함수 스코프 내에서 호이스팅되어 함수 전체(중첩된 함수 스코프 포함)에 걸쳐 재사용될 수 있고 함수가 필요로 하는 정보를 외부로부터 숨길 수 있다.

 

1.6 전역 변수를 최소화하는 방법에 대해 스코프 역할을 하는 함수와 블록으로 나누어 나열하시오.

스코프 역할을 하는 함수

- 즉시실행함수 (IIFE)

스코프 역할을 하는 블록

  • try/catch의 catch문
try {
  // ...
} catch(err) {
  cosole.log(err) // err 변수는 이 블록에서만 유효함
}

블록 스코프를 지원하지 않는 ES6 이전 환경에서는 catch 문이 블록 스코프를 형성할 수 있다는 것을 활용하여 다음과 같이 블록 스코프를 생성하기도 한다.

// ES6 환경
{
  let a = 27
  console.log(a) // 27
}

console.log(a) // ReferenceError

// polyfilling block scope
try {
  throw 27
} catch(a) {
  console.log(a) // 27
}

console.log(a) // ReferenceError
  • (ES6) let, const

 

1.7 블록 스코프가 유용한 이유는?

메모리 회수와 관련하여 가비지 콜렉션이 동작하게끔 처리할 수 있다.

예를 들어 다음과 같은 로직이 있다.

var bigData = {} // 매우 큰 용량의 데이터
function process(data) {}

process(bigData)

var btn = document.getElementById('btn')

btn.addEventListener('click', function clickEvt(e) {
  // ... 
}, false)

bigData는 process 이후에 사용하지 않으니 메모리에서 해제하고 싶어도 clickEvt가 해당 스코프 전체의 클로저를 가지고 있어 가비지 컬렉팅되지 않는다.

 

이 문제를 해결하는데에 블록 스코프가 유용하다.

function process() {}

{
  let bigData = {}
  process(bigData)
}

var btn = document.getElementById('btn')

btn.addEventListener('click', function clickEvt(e) {
  // ... 
}, false)

 

2. 클로저

2.1 클로저를 한 문장으로 표현하시오.

어떤 외부함수가 가진 내부함수가 외부함수의 지역변수를 참조하고 있고 외부함수는 이 내부함수를 외부로 전달할 때 내부함수가 참조하고 있는 지역변수가 GC되지 않는 현상 (코어 자바스크립트 참조하기)

 

책: 함수가 속한 렉시컬 스코프를 기억하여 함수가 렉시컬 스코프 밖에서 실행될 때에도 이 스코프에 접근할 수 있게 하는 기능

 

2.2 비동기 작업에서의 클로저를 설명하시오.

첫번째 예시

function setupButton(name) {
  const btn = document.createElement('button')
  btn.innerText = name
  btn.addEventListener('click', function() {
    console.log(name)
  })
  
  document.body.appendChild(btn)
}

setupButton('btn#1')
setupButton('btn#2')

setupButton 함수의 name 변수를 이벤트리스너의 콜백 함수가 참조하고 있다.

 

두번째 예시

for(var i=0;i<5;i++) {
  setTimeout(function() {
    console.log(i)
  }, i * 1000)
}

다음의 결과는 원하는대로 1초 단위로 0,1,2,3,4가 콘솔에 찍힐까? 그렇지 않다.

 

setTImeout의 콜백함수는 i라는 변수를 참조하는데 var i에서 i가 호이스팅되기 때문에 setTimeout의 i는 항상 가장 마지막값인 5이다. 반복문이 끝났을 때의 i의 값은 4가 아닌 5이기 때문이다.

 

따라서 위의 경우에는 클로저를 한단계 더 생성한다.

function timer(val) {
  return function () {
    console.log(val)
  }
}

for(var i=0;i<5;i++) {
  setTimeout(timer(i), i*1000)
}

또다른 방법으로는 IIFE로 빈 스코프를 하나 더 생성한다.

for(var i=0;i<5;i++) {
  (function(j) {
    setTimeout(function () {
      console.log(j)
    }, j*1000)
  })(i)
}

ES6에서는 반복별로 블록 스코프를 생성하는 let, const를 활용하는 것이다.

for(let i=0;i<5;i++) {
  setTimeout(() => console.log(i), i*1000)
}

다음 MDN을 참고하여 클로저를 학습하는 것을 추천한다.

 

2.2 다음 IIFE는 클로저인가?

첫번째 예시

const quarter = (() => {
    const period = {
        1: {
            start: '01',
            end: '03'
        },
        2: {
            start: '04',
            end: '06'
        },
        3: {
            start: '07',
            end: '09'
        },
        4: {
            start: '10',
            end: '12'
        }
    }

    return {
        get: (num = 1) => period[num],
        startMonth: (num = 1) => period[num].start,
        endMonth: (num = 1) => period[num].end
    }
})()

클로저가 맞다. period는 자바로 따지면 private변수이고 quarter가 반환하는 객체의 각 메서드들이 quarter의 내부 변수인 period를 참조하고 있다.

[참고] 첫번째 예시는 특히 모듈이라고 하는 자바스크립트 패턴이다. 그 중에서도 오직 하나의 인스턴스만을 생성하는 싱글톤 모듈이다.

 

두번째 예시

var a = 2

(() => {
  console.log(a)
})()

클로저가 아니다. 즉시실행함수가 a와 같은 스코프에서 실행되었기 때문이다. 따라서 그저 일반적인 렉시컬 스코프에서 가져왔을 뿐이다.

 

 

'Books > YOU DON'T KNOW JS' 카테고리의 다른 글

문법  (0) 2020.09.08
강제변환  (0) 2020.08.23
타입, 값 그리고 네이티브  (0) 2020.08.04