⚠️ 해당 게시글은 모던애자일 팀원이 준비한 테크톡 내용입니다.

출처 : 모던애자일 3기 이한결

https://velog.io/@leephoter

 

leephoter (leephoter) - velog

앱 vs 웹 테크톡 재희님 (2021.12.16) 2021년 12월 16일 · 0개의 댓글

velog.io

Scope & Closure

Scope

유효범위

프로그래밍언어에서 Scope 는 어느 범위까지 참조할 지를 뜻한다.

scope 가 없다면 식별자 이름은 프로그램 전체에서 하나밖에 사용할 수 없다. (같은 식별자는 서로 충돌을 일으킨다)

Scope 는 함수를 호출할 때가 아닌 선언할 때 생기는 구조이다.

따라서 모든 변수는 scope 를 갖는다.

Global scope

전역 스코프 👉🏻 스크립트 전체를 참조, 어디에서든 참조 가능

Global variable

전역 변수 👉🏻 전역에서 선언된 변수, 어느 영역에서든 참조 가능

Local scope

지역 스코프 👉🏻 정의된 함수 내에서만 참조, 함수 밖의 영역에서는 참조 불가

Local variable

지역 변수 👉🏻 지역(함수) 내에서 선언된 변수, 그 지역과 그 지역 내의 지역에서만 참조 가능

JavaScript 의 scope 특징

대부분의 C-family language 는 block-level scope (블록 레벨 스코프) 를 따른다. block-level scope 란 코드 블록 ( { ... } ) 내에서 유효한 (접근 가능한) scope 를 의미한다.

하지만 JS는 function-level scope (함수 레벨 스코프) 를 따른다. function-level scope 란 함수 코드 블록 내에서 선언된 변수는 함수 코드 블록 내에서만 유효하고 함수 외부에서는 유효하지 않다.

이를 해결하기 위해 ECMAScript 6 에서 let const keyword 를 도입했다.

  • var 키워드로 선언된 변수는 함수 레벨 스코프를 따른다
  • let const 키워드로 선언된 변수는 블록 레벨 스코프를 따른다JS는 타 언어와 달리 특별한 Entry point (시작점) 이 없기 때문에 전역에 변수 or 함수를 선언하기 쉽다.JS 는 다른 C-family language 와 달리 특별한 Entry point 가 없으니 코드가 나타나는 즉시 해석되고 실행된다. 따라서 전역 변수를 많이 생성하게 되는 문제를 일으킨다.
  • 전역변수의 사용은 의도치 않은 재할당에 의한 상태 변화로 코드를 예측하기 어렵게 만들기 때문에 지양한다.
  • C언어의 경우 main 함수가 시작점이 되기 때문에 대부분의 코드는 main 함수 내에 포함된다. C언어의 경우 전역 변수를 선언 하기 위해서는 의도적으로 main 함수 밖에 변수를 선언해야 한다.
  • 전역에서 변수를 선언하면 어디서든 이 변수를 참조할 수 있다. var 키워드로 선언한 전역 변수는 Global Object (전역 객체) window 의 프로퍼티이다.

Block-level scope

블록 레벨 스코프 👉🏻 모든 코드 블록 (함수, if 문, for 문, while 문, try/catch 문 등의 모든 { ... })

{

}

if (1) {

}

while (1) {

}

// etc ...

function-level scope

함수 레벨 스코프 👉🏻 함수 내에서 선언된 변수는 함수 내에서만 유효하며 함수 외부에서는 참조할 수 없다. 즉, 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수이다.

함수 코드 블럭 내에서 선언된 변수는 함수 코드 블럭 내에서만 유효하고 함수 외부에서는 유효하지 않다는 것이다. (이는 let or const 선언문을 이용하여 block-level scope를 사용할 수 있다)

JS는 function-level scope 를 따르기 때문에 함수 내부에서 선언한 번호 는 함수 외부에서 참조할 수 없다.

Non block-level scope (비 블록 레벨 스코프)

function-level scope 를 제외한 block-level scope 내에서 var 키워드로 선언한 변수는 전역 변수가 된다. (JS 는 function-level scope 를 따르기 때문)

  • let const 로 해결Screen Shot 2022-02-23 at 10.24.42 PM.png

Lexical scope (Static scope)

JS 는 함수가 선언된 시점에서의 유효범위를 갖는다.

위의 실행 결과는 함수 함수2 의 상위 scope 가무엇인지에 따라 결정된다. 두가지 패턴을 예측할 수 있는데 첫번째는 함수를 어디서 호출했는지, 두번째는 함수를 어디서 선언했는지에 따라 상위 scope 가 결정된다.

 

먼저 함수를 호출한 위치에 따라 상위 scope 를 결정한다면 함수2 의 상위 scope 는 함수1 과 전역이고, 선언한 위치에 따라 결정한다면 함수2 의 상위 scope 는 전역이 될텐데...

 

일단 프로그래밍 언어는 이 두가지 방식 중 하나의 방식으로 함수의 상위 scope 를 결정한다. 첫 번째 방식을 Dynamic scope (동적 스코프) 라 하고, 두 번째 방식을 Lexical scope (정적 스코프, Static scope) 라고 한다. JavaScript 를 포함한 대부분의 프로그래밍 언어는 Lexical scope 를 따른다.

 

Lexical scope “함수를 어디서 호출하는지”가 아닌, “함수를 어디에 선언했는지” 에 따라 결정된다. JavaScript 는 Lexical scope 를 따르기 때문에 함수를 선언한 시점에 상위 scope 가 결정되는데, 함수를 어디에서 호출했는지는 scope 결정에 아무 의미를 부여하지 않는다. 위 예제의 함수 함수2 는 전역에서 선언되었기 때문에 전역변수 num (1) 이 두번 출력된다.

 

아래의 예시로 Lexical scope 를 따르는 원리를 쉽게 알 수 있다.

함수2 는 단순히 숫자 라는 변수를 출력하는 기능만 갖는다. 만약 Dynamic scope 를 따른다면 오른쪽 예시와 같이 변수 숫자2 값을 갖는다. 하지만 JS는 Lexical scope 를 따르기 때문에 왼쪽 예시 코드의 10 ~ 12 번 줄 코드에서 함수2 가 선언되는 당시에 숫자 는 지역변수가 아닌 전역변수 숫자 이기 때문에 실행 시 함수2 에서 숫자 의 값은 1 이다.

  • Dynamic scope 를 따르는 언어
    • LaTeX
    • 텍스트 위주의 문서를 작성하는 TeX이었으나, 여러 가지 기능들이 덧대어져 LaTeX으로 진화했으며, 결국에는 출판물을 작성할 수 있는 강력한 도구로 탄생
    • Lisp
    • 인공지능 언어, 해커 etc ...
  • ⭐️ 최소한의 전역변수 사용하기 ⭐️
  • 전역 변수 사용을 최소화하는 방법 중 하나는 전역 객체 하나를 만들어 사용하는 것이다.
  • ⭐️ implied global ⭐️명시적으로 변수 앞에 var 를 붙여주지 않으면 암묵적 전역변수가 된다.숫자1 은 선언문 (var, let, const) 을 통해 선언된 변수가 아니기 때문에 전역객체 (global or window) 에 property 로 추가되긴 하지만 Hoisting 이 발생하지 않는다.
  • 숫자2 는 선언문으로 선언되었기 때문에 Hoisting 이 발생하여 변수에 값을 할당하기 전에 참조하면 쓰레기값을 반환한다.
  • 숫자1 은 선언문으로 선언되지 않았기 때문에 Hoisting 이 발생하지 않아 쓰레기값도 채워지지 않은 상태다. 따라서 첫번째 출력에서 값을 참조할 수 없다.
  • 반면 숫자2숫자3 은 선언문을 통해 변수로 선언되었기 때문에 Hoisting 이 발생하고 전역객체에 property 로 추가된다.
  • Screen Shot 2022-02-23 at 10.41.34 PM.png
  • 암묵적 전역

Scope Chain

스코프 체인 : Identifier (식별자) 를 찾는 과정

(Prototype chain : 어떤 Object 의 Property 를 찾는 과정)

새롭게 정의된 scope 는 상위의 scope 에 접근할 수 있다.

scope chain 은 scope 의 가장 내부에서 scope chain 을 따라 바깥쪽으로 참조하게 된다.

전역에서 선언된 숫자0 , 함수1 에서 선언된 숫자1함수2 내부에서 참조할 수 있는 이유는 Scope Chain 때문이다.

프로그램이 실행 될 때 Execution Context (실행 컨텍스트) 가 생성되고 이 때 (Execution Context, Lexical Environment) 하위 Execution Context 의 outer Lexical Environment 가 상위의 Environment Record 에 연결되어 상위의 식별자를 참조할 수 있게 된다.

함수2 내부에서 숫자0 을 찾는 과정 : 함수2 내부 → 함수1 내부 → 전역 (참조)

함수2 내부에서 숫자1 을 찾는 과정 : 함수2 내부 → 함수1 내부 (참조)

함수2 내부에서 숫자2 을 찾는 과정 : 함수2 내부 (참조)

Closure

클로저

closure 는 함수가 선언된 환경의 Lexical scope 를 기억하여 함수가 scope 밖에서 실행될 때에도 이 scope에 접근할 수 있게 한다.

내부함수는 외부함수의 지역변수에 접근 할 수 있는데 외부함수의 실행이 끝나서 외부함수가 소멸된 이후에도 내부함수가 외부함수의 변수에 접근 할 수 있다.

모든 변수들은 더 이상 참조되지 않았을 때 GC (garbage collection) 에 수집되어 삭제된다. 참조가 되고 있으면 수집 대상에서 제외된다.

내부함수에서 변수를 참조하고 있기 때문에 (closure) GC의 수집 대상에서 제외가 되어 외부 함수가 종료되더라도 접근이 가능하다.

왼쪽 예시 코드는 함수 증가함수 가 호출될 때 지역변수 숫자 가 메모리구조 Stack 에 할당되고 함수 증가함수 가 종료 되면 지역변수 숫자 는 GC에 의해 수집되어 삭제된다. 따라서 증가함수 가 호출될 때마다 숫자 는 0으로 초기화 된다.

오른쪽 예시 코드는 증가함수 내부의 함수에서 증가함수 의 지역변수 숫자 를 참조하고 있기 때문에 외부함수가 종료되더라도 GC의 수집 대상에서 제외되어 접근이 가능하다. 내부함수는 자신이 생성됐을 때의 Lexical environment 를 기억하는 closure 이다.

변수의 값은 누군가에 의해 변경될 수 있기 때문에 오류 발생의 원인이 될 수 있다. Immutability (불변성) 을 지향하는 함수형 프로그래밍에서 오류를 피하고 안정성을 높이기 위해 closure 는 적극 활용해야 한다.

Closure 의 장점 : 정보 은닉

외부 함수의 데이터를 사용하기 때문에 전역에서 해당 데이터에 쉽게 접근할 수 없다. 따라서 안정성이 높다.

  • 네이버 블러그 공유하기
  • 네이버 밴드에 공유하기
  • 페이스북 공유하기
  • 카카오스토리 공유하기