ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 스코프에 대해서
    개발 2020. 3. 31. 15:48

    스코프란 ( Scope, 유효범위 )

    특정 장소에 변수를 저장하고 나중에 그 변수를 찾는 데는 잘 정의된 규칙이 필요한데 바로 이런 규칙을 '스코프' 라고 합니다.


    ★ 스코프의 구문

    스코프는 두가지의 종류가 있습니다. 전역스코프(Global scope)지역스코프(Local scope)로 나뉩니다.

    전역스코프(Global scope) 코드 어디에서든지 참조할 수 있습니다.
    지역스코프(Local scope) 함수 코드 블록이 만든 스코프로 함수 자신과 하위 함수에서만 참조할 수 있다.


    스코프를 이해하기에 앞서 자바스크립트의 컴파일레이션에 대해서 간략하게 설명해보려고 합니다.

    컴파일레이션이란?
    - 컴파일러 언어의 처리 과정에서는 프로그램을 이루는 소스 코드가 실행되기 바로 직전에 보통 3단계를 거치는데, 이를 컴파일레이션이라고 합니다. 자바스크립트의 컴파일레이션의 3단계는 다음과 같이 이루어져 있습니다.

    1. 토크나이징 / 렉싱 코드를 의미있는 조각(토큰)으로 만듭니다. 예) var a = 5; -> var, a, =, 5, ;
    2. 파싱 토큰 배열을 프로그램의 문법 구조를 반영하여 중첩 원소를 갖는 트리 형태로 바꾸는 과정입니다. 파싱의 결과로 만들어진 트리를 AST(추상 구문 트리)라 부릅니다.
    3. 코드 생성 파싱한 결과(AST)를 컴퓨터에서 실행 코드(기계어)로 바꾸는 과정입니다.

    ★ 엔진, 컴파일러, 스코프 이해하기

    • 엔진 : 컴파일레이션의 시작부터 끝까지의 모든 과정과 자바스크립트 프로그램 실행을 책임집니다.
    • 컴파일러 : 파싱과 코드 생성의 모든 잡일을 도맡아 합니다. (엔진의 친구)
    • 스코프 : 선언된 모든 변수 검색목록을 작성하고 유지합니다. (엔진의 또 다른 친구)


    앞서 설명한 이 세가지의 처리과정에서 var a = 5;를 어떻게 처리하는지 설명해보자면 var a = 5; 를 보면 하나의 구문으로 보입니다. 그러나 엔진은 그렇게 보지 않습니다. 엔진은 두 개의 서로 다른 구문으로 봅니다. 하나는 컴파일러가 컴파일레이션 과정에서 처리할 구문이고, 다른 하나는 실행 과정에서 엔진이 처리할 구문입니다.

    먼저, 이 프로그램에서 컴파일러가 할 첫 번째 일은 렉싱을 통해 구문을 토큰으로 쪼개는 것입니다. 그 후 토큰을 파싱해 트리구조(AST)로 만듭니다. 이 과정을 좀 더 정리하여 순서대로 나열해보자면 다음과 같습니다.

    1. 컴파일러가 var a를 만나면 스코프 안에 변수 a가 있는지 체크합니다.
    2. 변수 a가 있다면 컴파일러는 선언을 무시하고, 없다면 컴파일러는 새로운 변수 a를 스코프 내에 선언하라고 요청합니다.
    3. 그 후 컴파일러는 a = 2 대입문을 처리하기 위해 엔진이 실행할 수 있는 코드(기계어)를 생성합니다.
    4. 엔진이 실행하는 코드는 먼저 스코프에게 a라는 변수가 현재 스코프 내에서 접근할 수 있는지 체크합니다.
    5. 가능하다면 엔진은 변수 a를 사용하고, 아니라면 상위 스코프로 올라가 접근할 수 있는지를 체크합니다.
    6. 글로벌 스코프에서 까지 찾지 못한다면 엔진은 에러를 발생시킵니다.
    요약 : 두 가지 동작을 취하여 변수 대입문을 처리한다. 첫째, 컴파일러가 변수를 선언한다(현재 스코프에 미리 변수가 선언되지 않은 경우). 둘째, 엔진이 스코프에서 변수를 찾고 변수가 있다면 값을 대입한다.


    그렇다면 다음과 같은 코드가 주어졌을 때 엔진과 스코프가 어떻게 처리를 하는지 알아보겠습니다.

    function foo(a){ console.log(a); // 2 } foo(2);
    1. 엔진: 스코프에게 foo에 대한 참조가 있는지 확인합니다.
    2. 스코프: 컴파일러가 좀 전에 선언한 foo함수를 찾아서 엔진에게 넘겨 줍니다.
    3. 엔진: 스코프가 넘겨준 foo함수를 실행하고 매개변수로 받아온 a의 참조가 있는지 다시한번 스코프에게 확인합니다.
    4. 스코프: 컴파일러가 foo의 매개변수로 a를 선언한걸 확인한 후(foo를 실행하며 인자 값으로 넘겨준 2), 엔진에게 a도 넘겨줍니다.
    5. 엔진: 스코프가 넘겨준 a에 2를 대입합니다. 대입한 후에 console에 대한 검색을 스코프에게 물어봅니다.
    6. 스코프: console은 내장되어 있는 log()함수를 넘겨줍니다.
    7. 엔진: console.log()의 인자에 넣은 a 값을 엔진은 좀 전에 받아서 가지고 있지만 확실히 해두기 위해 다시한번 스코프에게 a의 참조를 확인합니다.
    8. 스코프: 다시한번 확인한 a를 엔진에게 넘겨줍니다.
    9. 엔진: 스코프로부터 받아온 a에 값인 2를 넣어 log()에 넘겨줍니다.

    ★ 중첩 스코프

    스코프는 하나의 블록이나 함수는 다른 블록이나 함수 안에 중첩될 수 있으므로 스코프도 다른 스코프 안에 중첩될 수 있습니다. 따라서 찾고자 하는 변수를 현재 스코프에서 발견하지 못하면 엔진은 다음 바깥의 상위 스코프로 넘어가는 식으로 변수를 찾고 상위 스코프에서도 찾지 못한다면 글로벌 스코프라 부르는 가장 상위 스코프에 도달할 때까지 계속합니다.
    간단하게 요약하자면 다음과 같습니다.

    • 엔진은 현재 스코프에서 변수를 찾기 시작하고, 찾지 못하면 한 단계씩 상위 스코프로 올라갑니다.
    • 최상위 글로벌 스코프에 도달하면 변수를 찾았든, 못찾았든 검색을 멈춥니다. (찾지 못할 경우 에러 발생)

    다음 예시는 foo라는 함수안에서 글로벌 스코프에 있는 변수를 참조하여 중첩스코프를 나타내는 예시입니다.

    var b = 5; function foo(a){ console.log(a + b); } foo(10); // 15

    foo함수 입장에서는 변수 b에 대한 참조를 처리할 수없고, 현재 foo함수를 포함하고 있는 글로벌 스코프에서 처리합니다.


    ★ 렉시컬 스코프

    렉싱타임

    렉시컬 스코프를 알아보기 전에 컴파일러는 스크립트 코드를 실행하는 첫 단계 때 토크나이징/렉싱이라는 작업을 한다고 위에서 언급한적이 있습니다. 토큰 처리과정에서 토큰을 분석하여 생성된 토큰에 의미를 부여하는 것을 렉싱 이라고하고, 이 과정을 렉싱타임이라고합니다.
    렉시컬 스코프는 프로그래머가 코드를 짤 때 변수와 스코프 블록을 어디서 작성하는가에 따라서 렉싱타임에 토큰이 분석되며 스코프가 결정되는 것이고, 이 렉싱타임에 정의되는것이 바로 렉시컬 스코프 입니다.


    동적 스코프 vs 렉시컬 스코프

    함수의 상위 스코프를 결정하는데에는 두 가지 방법이 있습니다. 첫 번째 방법은 동적 스코프로 함수가 어디서 호출했는지에 따라 상위 스코프를 결정하고, 두 번째 방법은 렉시컬 스코프로 함수가 어디서 선언되었는지에 따라 상위 스코프를 결정합니다. 다음의 예제 코드를 통해 렉시컬 스코프를 좀 더 정확하게 알아보겠습니다.
    다음 예시에서 outer()의 실행결과로 동적 스코프라면 10이고, 렉시컬 스코프라면 1이 나올 것 입니다.

    var a = 1; function outer() { var a = 10; inner(); } function inner() { console.log(a); } outer(); // 1 inner(); // 1


    실행해보면 렉시컬 스코프라는 것을 확인할 수 있습니다. inner함수를 호출한 곳은 outer함수 내부였지만, inner함수가 선언된 곳은 전역이기 때문에 a는 전역에 선언된 1의 값을 가지게 됩니다.
    그렇다면 렉시컬 스코프를 따른다는 것을 알게 되었으니 아래의 예제에서 outer 실행 결과를 예상해볼 수 있습니다.

    var a = 1; function outer(){ var a = 10; function inner(){ console.log(a); } inner(); } outer(); // 10


    outer()의 실행 결과는 10입니다. outer 함수 내부에서 inner 함수를 호출했으므로 이 inner함수는 outer 내부의 지역스코프를 가지게 됩니다. 따라서, inner함수에는 a가 없기 때문에 상위 스코프인 outer함수에서 a를 찾습니다.
    즉, inner함수가 선언되는 시점에서 스코프를 가지고 outer와의 중첩 스코프가 되어 inner함수의 상위 스코프는 outer가 되고 outer 내부에 있는 변수 a에 접근하여 변수를 찾았기 때문에 전역 스코프인 var a = 1; 까지 접근하지 않고 검색을 멈춘 것 입니다.


    ★ 블록스코프

    블록 스코프는 변수를 함수 밖에서 선언하면 함수 스코프 변수처럼 전역 접근할 수 있습니다. 하지만 블록 안에서 선언하면 자신을 정의한 블록과 하위 블록에서만 접근이 가능합니다. 블록스코프는 중괄호{}로 경계를 구분합니다.
    이 블록 스코프는 ES6에서 새로운 키워드 let과 const가 채택되면서 그저 중괄호{}를 문법에 맞게 추가만 해도 let과 const를 통해 선언된 변수를 묶을 수 있는 명시적인 블록을 만들 수 있습니다.

    let a = "global"; function foo(){ let b = "block"; console.log(a); // "global" console.log(b); // "block" } foo(); console.log(a); // "global" console.log(b); // Uncaught ReferenceError: b is not defined

    이 처럼 글로벌스코프에서 foo안의 let 키워드로 선언한 변수 b는 블록 스코프이기 때문에 조회할 수 없습니다.

    '개발' 카테고리의 다른 글

    prototype과 prototype Chain  (0) 2020.04.14
    생성자와 new  (0) 2020.04.12
    객체지향 프로그래밍  (0) 2020.04.12
    재귀함수  (0) 2020.04.12
    클로저에 대해서  (0) 2020.04.01
Designed by Tistory.