ABOUT ME

-

My Email
  • kdmin0211@gmail.com
  • Today
    -
    Yesterday
    -
    Total
    -

    • [Javascript] Scope
      Javascript 2021. 9. 14. 16:11

      해당 글은 YOU DON'T KNOW JS(타입) 책을 읽으며 정리한 Post입니다.

      실제 책의 내용을 간략화 한 Post이고 실제로 책을 읽으면 많은 예제와 자세한 내용이 서술되어 있습니다:D

       

      스코프(Scope)란?

      개발자가 작성한 코드에 대해서 엔진이 식별자(변수)를 어디서 어떻게(스코프, 또는 중첩 스코프) 찾는가에 대한 '규칙의 집합'입니다.

       

      RHS, LHS 참조 검색

      스코프를 통해 식별자를 찾는 이유는 변수의 값을 대입(LHS 참조 검색) 혹은 변수의 값을 얻기 위해서입니다(RHS 참조 검색).

      RHS, LHS 참조 검색은 현재 스코프에서 시작하여 식별자를 찾지 못한 경우 상위 스코프로 넘어가며 검색합니다. 상위 스코프로 넘어가는 검색의 반복은 식별자를 찾지 않으면 글로벌 스코프까지 도달해야 작업을 종료합니다. 이때 RHS 참조 검색은 식별자가 없는 경우 ReferenceError 가 발생하고 LHS 참조 검색은 글로벌 스코프에 새로 해당 식별자를 생성합니다.

       

      렉시컬 스코프(Lexical Scope)

      앞서 정의한 스코프의 동작 방식은 크게 '렉시컬 스코프', '동적 스코프'로 나뉩니다. 몇몇 언어를 제외한 일반적인 언어는 '렉시컬 스코프' 방식을 사용하고 있습니다. 일반적인 언어의 컴파일러는 첫 과정으로 토크나이징/렉싱을 통해 토큰들을 만들어 의미를 부여합니다. 렉시컬 스코프는 해당 렉싱 타임에서 정의되는 스코프입니다. 즉, 개발자가 코드를 작성하며 함수가 선언된 위치에 따라 따라 렉서가 확정 짓는 스코프입니다.

       

      - 검색

      검색의 실행 시점에서 가장 안쪽 스코프에서 시작하여 상위 스코프로 올라가며 대상을 찾으면 멈추고, 없으면 계속 올라갑니다.

      function func1(num1) {
      	const var1 = num1;
          
      	function func2(num2) {
          	const var2 = num1 + num2;
              const var3 = func1.var1;
          }
      }

      렉시컬 스코프의 검색 방식은 일차 식별자를 검색하는 데 사용됩니다. 

      예를 들어 이러한 중첩 스코프가 선언되었습니다. 여기서 fun2 내부 num1, num2, func1.var1에 대해서 검색이 발생할 것입니다. 이때 렉시컬 스코프는 fun1, num1, num2 까지 찾으며 func1.var1 에 대해서는 객체 속성 접근 규칙을 통해 var1 에 접근하게 됩니다.

       

      - 스코프 수정

      이러한 렉시컬 스코프를 런타임에 수정할 수 있는 방식으로 eval(), with 가 있습니다. 하지만 엔진이 미리 정의한 렉시컬 스코프가 언제 수정되거나 새로운 렉시컬 스코프가 추가될 것인지 알 수 없습니다. 이러한 렉시컬 스코프를 런타임에 수정하는 경우는 엔진을 통한 최적화가 의미가 없어져 성능적으로 저하가 발생합니다.

       

      함수 기반 스코프

      Javascript 언어는 함수 기반 스코프를 사용하고 있습니다. 선언된 함수들은 각각의 스코프가 생성됩니다.

       

      렉시컬 스코프로부터 숨기

      하지만 JS를 통해 모듈, 라이브러리, API 같은 소프트웨어를 설계하는 경우 '최소 권한의 원칙'에 대한 고민이 많을 것입니다. 당연하게도 라이브러리를 불러와 사용하는 경우 스코프 검색에서 동일한 섀도잉이 발생된다면 실행에 문제가 생길 것입니다. 그로 인해 필자를 비롯한 많은 개발자들은 스코프에서 충돌을 피해야 할 것입니다.

       

      - 함수 스코프에 숨기

      function sumFunction(a) {
      	sum = a + minusFunction(a, 1);
      }
      
      function minusFunction(a, b) {
      	return a - b;
      }
      
      let sum;
      
      sumFunction(5);

      위와 같은 코드는 글로벌 스코프에 sumFunction, minusFunction, sum에 접근이 가능합니다. 이것은 불필요하며 최소 권한의 원칙이 위배됩니다.

      해당 코드를 아래와 같이 하나의 함수 스코프에 다른 식별자들을 넣음으로 글로벌 스코프로부터 숨길 수 있습니다.

      function func1(a) {
      	function fun2(a, b) {
          	return a - b;
          }
      	const sum = a + func2(a, 1);
      }
      
      func1(5);

       

       

      - 글로벌 네임스페이스

      const Library = {
      	function1: function() {
          	...
          }
          function2: function() {
          	...
          }
      }

      네임스페이스를 사용하여 충돌을 회피하는 방식입니다. 글로벌 스코프에 고유한 이름의 객체를 선언합니다. 앞서 서술했듯이 렉시컬 스코프는 일차 식별자에 대한 검색만 수행하므로 Library 내부 function1, function2와 같은 기능은 객체 속성 접근 규칙으로만 접근이 가능하므로 스코프에 의해 충돌이 발생하지 않습니다.

       

      최근에는 네임스페이스 방식이 아닌 스코프 클로저를 이용한 모듈 패턴이 많이 사용됩니다.

       

      - 즉시 실행 함수 표현식 (IIFE)

      일반적인 함수 선언하여 사용하는 방식은 아래와 같습니다.

      function func1() {
      	...
      }
      
      func1()

      해당 방식은 당연히 현재 스코프에서 "func1" 식별자로 검색이 가능합니다.

      이때 함수의 선언과 실행을 동시에 하여 func1 함수 자체를 IIFE를 통해 스코프로부터 숨을 수 있습니다. 일반적으로 IFEE는 익명 함수로 사용합니다.

      // 인자가 없는 경우
      (function () {...})();
      
      // 인자가 있는 경우
      (function (arg) {...})(pram);

      - 블록 스코프

      먼저 블록 스코프가 필요한 이유부터 서술하겠습니다.

      for (var i=0; i<10; i++) {
      	...
      }
      
      
      if(true) {
      	var var1 = ...
      	...
      }

      위와 같은 코드 for, if 문에서 var를 통해 변수를 선언했습니다.

      필자는 변수 i, var1에 대해서 각각 for 문과 if 문 안에서만 유효한 변수라고 생각했습니다. 하지만 var 키워드의 선언은 어디서 선언됐는지가 중요한 것이 아닌 둘러싸인 스코프에 속하게 됩니다. 즉, 해당 경우 글로벌 스코프에 i와 var 가 포함됐습니다. 실제로 글로벌 스코프에서 출력을 통해 확인할 수 있습니다.

       

      블록 스코프는 위에 언급한 내용을 코드 블록 안에 숨기는 도구입니다.

      하지만 JS에서는 블록 스코프가 지원되지 않으나 사용할 방법은 있습니다.

      • with
      • try/catch
      try {
      	....
      } catch(err) {
      	const var1 = ...
      	...
      }

      위와 같은 try catch 문에서 catch 문은 블록 스코프에 속합니다. 즉, err, var1와 같은 식별자는 오직 catch 문 안에서만 존재합니다.

       

      • let

      ES6부터 추가된 새로운 변수 선언 방식입니다. 해당 키워드는 var와 다르게 선언 당시 포함된 블록 ({})에 속하게 됩니다.

      if(true) {
      	let var1 = 1;
          var var2 = 2;
      }
      
      console.log(var1); // ReferenceError
      console.log(var2); //2
      
      for (let i = 0; i < 5; i++) {
      	console.log(i); // 1~5
      }
      
      console.log(i); // ReferenceError

      다시 말해서 위의 코드에 대해서 var1 은 if 문의 블록에 속하게 되고 var2는 글로벌 스코프에 속하게 됩니다. 하지만 코드를 작성하다 보면 여러 블록들을 사용하게 되고 let을 통해 선언된 식별자가 어떤 블록에 속하는지 알기 힘듭니다. 그러므로 식별자를 블록 스코프에 감싸고 싶다면 명시적으로 블록을 선언해주는 것이 좋습니다.

      if(true) {
      	{
      		let var1 = ...;
          	...
          }
          var var2 = ...;
      }

      따라서 let 키워드의 선언은 선언된 위치를 기점으로 그 전에는 명백히 존재하지 않는 식별자입니다.

      • const

      let과 마찬가지로 es6에 추가된 변수 선언 방식입니다. let 과 다른 점은 선언된 변수의 값이 상수라는 것입니다.

       

      Hoisting

      JS는 기본적으로 싱글 스레드입니다. 그로 인해 실행 순서가 위에서부터 아래로 이루어집니다. 하지만 스코프로 인해서 프로그램을 잘못 이해하는 경우가 생깁니다.

      console.log(b);// ReferenceError?
      var b = 3;
      a = 5;
      var a;
      console.log(a); // undifined?

      당연하게도 코드를 읽으면 두 console.log의 출력 값은 ReferenceError, undefined라고 생각합니다.

      하지만 console.log(b)는 undefined, console.log(a)는 5를 출력합니다.

       

      그 이유는 변수 선언 방식인 var와 렉시컬 스코프에 있습니다. 자바스크립트 엔진의 컴파일 레이션 단계 중 선언문을 찾아 스코프와 연결해주는 렉시컬 스코프 과정을 지납니다. 그러므로 var b = 3; 코드는 var b; b = 3; 형식으로 해석됩니다.

      var b;
      var a;
      console.log(b);// undifined
      b = 3;
      a = 5;
      console.log(a); // 5

      이런 식으로 컴파일 레이션 과정을 통해 선언문이 먼저 처리됩니다. 변환된 코드를 확인해보면 출력의 결과가 이해됩니다.

      이런 선언문을 끌어올리는 동작(즉, 선언문이 대입문보다 먼저 처리)을 'Hoisting'이라고 불립니다. 이 과정에서 위 예제처럼 선언문만 위로 올리고 다른 로직은 원래 자리에 그대로 존재합니다.

      함수를 포함한 좀 더 복잡한 내용을 예로 들겠습니다.

      func1();
      func2();
      
      var func1 = function func2 () {
      	console.log(c);
          var c = 4;
      }

      필자는 책을 읽으며 해당 코드의 대해서 func1()는 Type Error, func2()는 실행되어 console.log(c)가 undifined 가 실행될 것이라고 생각했습니다. 하지만 func2() 는 ReferenceError 가 반환됩니다. 그 이유는 함수 표현식에 있었습니다.

      var func1 = function func2 () {
      ...
      }

      여기서 func2는 함수의 선언이 아닌 func2 의 이름을 가지는 함수 표현식을 func1 에 대입하는 것입니다.

      함수 표현식은 이름을 가져도 해당 스코프에서 이름 식별자를 찾을 수 없습니다. 즉, 예제에서 func2 는 글로벌 스코프에 속하지 않으므로 ReferenceError 가 반환됩니다. 위 예제를 컴파일 레이션 과정을 거친 결과로 코드를 작성해보겠습니다.

      var func1;
      func1(); // TypeError
      func2(); // ReferenceError
      
      func1 = function () {
      	var func2 = this;
      	var c;
      	console.log(c);
          c = 4;
      }

      추가로 Hoisting 과정은 변수보다 함수가 먼저 끌어올려집니다.

       

      func1(); // 3
      var func1;
      
      function func1() {
      	console.log('1');
      }
      
      func1 = function {
      	console.log('2');
      }
      
      function func1() {
      	console.log('3');
      }

       

      참고로 해당 Hoisting 은 렉시컬 스코프 과정에서 처리되므로 스코프를 기준으로 처리됩니다. 그러므로 위에서 설명한 let, const는 이 과정에서 먼저 선언되지 않습니다.

       

       

       

      'Javascript' 카테고리의 다른 글

      [Javascript] this?  (0) 2021.09.20

      댓글

    Designed by Tistory.