(TIL) D+11 스코프, 클로저

JulyK9·2022년 7월 12일
0

📝 스코프

  • 스코프란? 변수의 유효범위
  • 스코프의 의미와 적용 범위, 주요 규칙
  • 전역 스코프와 지역 스코프
  • block scope와 function scope
  • 변수 선언 키워드(let, const, var)와 스코프와의 관계
  • 전역 객체란 무엇인가

✅ 스코프란

  • 변수 접근 규칙에 따른 유효 범위 (변수가 선언된 위치에 따라 유효 범위가 결정됨)
    • 변수 이름, 함수 이름, 클래스 이름과 같은 식별자가 본인이 선언된 위치에 따라 다른 코드에서 자신이 참조될 수 있을지 없을지 결정되는 것
  • 스코프 위치의 기준
    • 스코프 안과 바깥의 범위가 나눠지는 기준 ⇒ 블록, 함수
  • 스코프의 규칙
    1. 안쪽 스코프에서 바깥쪽 스코프로는 접근할 수 있지만 반대는 불가능
      • 바깥쪽 스코프에서 선언한 변수 ⇒ 안쪽 스코프에서 사용 가능
      • 안쪽에서 선언한 변수 ⇒ 바깥쪽 스코프에서 사용 불가
    2. 스코프는 중첩이 가능
    3. 전역 스코프(가장 바깥쪽), 지역 스코프(전역이 아닌 다른 모든 스코프)
      • 지역 스코프에서 선언한 변수(지역변수), 전역 스코프에서 선언한 변수(전역 변수)
    4. 지역 변수는 전역 변수보다 적요에 있어 더 높은 우선순위를 가짐

✅ 예제로 알아보는 스코프

  • 순서대로 콘솔에 출력되는 결과는?
let name = '김코딩';

function showName() {
  name = '박해커';     // 전역변수 name에 '박해커'를 할당
  console.log(name); // '박해커'
}

console.log(name); // '김코딩'
showName();
console.log(name); // '박해커' showName() 실행후 전역변수 name 에 박해커가 할당되었으므로

✅ 상위 스코프

  • 함수가 호출되는 시점에 결정 → 동적 스코프 (프로그램 런타임도중 실행 컨텍스트나 호출 컨텍스트에 의해서 스코프가 결정됨)
  • 함수가 정의되는 시점에 결정 → 정적 스코프 ( === 렉시컬 스코프) ⇒ 자바스크립트!!!
    “자바스크립트에서 함수는 태어나면서 자신의 내부슬롯에 상위 스코프의 참조를 저장한다!”

✅ 스코프 종류

  • 함수 레벨 스코프 : 함수로 둘러싼 범위 (function 키워드를 사용하면 함수 스코프임) (only 함수)
  • 블록 레벨 스코프 : 중괄호로 둘러싼 범위, 화살표 함수로 둘러싼 범위(⇒ 같은 함수여도 블록 스코프로 취급됨) (if문, for문, 함수 …)
  • 예제
  for (let i = 0; i < 5; i++) {
    console.log(i);   // 다섯번 반복
  }
  console.log('final i:', i);   // ReferenceError
    
 // 블록 스코프 안에서 정의된 변수 i는 블록 범위를 벗어나는 즉시 접근할 수 없게 되기 때문

✅ 스코프 관련 변수 선언 주의할 점

  • window 객체 (브라우저 only)
    • 브라우저 창을 대표하는 객체. 그러나 브라우저 창과 관계없이 전역 항목도 담고 있음
    • var로 선언된 전역 변수 및 함수 선언식으로 만든 전역 함수는 window 객체에 속함
  • 전역 변수 최소화
    • 편리한 대신, 다른 함수 혹은 로직에 의해 의도치 않은 변경이 발생할 수 있음(side effect)
  • let, const 사용
    • var는 블록 스코프 무시, 재선언 해도 에러 안남 ⇒ 버그 유발
    • 전역 변수를 var로 선언할 경우 window 기능을 덮어씌워 내장 메서드 기능을 사용할 수 없게 만듦
  • 선언 키워드(var, let, const)없이 변수 할당하지 말기
    function showAge() {
    	// age 같이 선언 키워드 없이 변수에 값을 할당 => 전역 변수로 취급됨
    	age = 90;
    	console.log(age);  // 90
    }
    
    showAge();
    console.log(age);  // 90
    console.log(window.age);  // 90
    
    // age는 선언한 적 없으나, 변수에 값을 할당하면서 var로 선언된 전역 변수처럼 작동해버림
  • 실수 방지를 위해 strict mode 를 사용할 수 있음 : js 파일 상단에 ‘use strict’; 입력

📝  클로저

함수와 함수가 선언된 어휘적(lexical) 환경의 조합
이 환경은 클로저가 생성된 시점의 유효 범위 내에 있는 모든 지역 변수로 구성됨
자바스크립트는 함수가 호출되는 환경과 별개로
기존에 선언되어 있던 환경, 즉 어휘적 환경 을 기준으로 변수를 조회하려고 함
이와 같은 이유로 "외부 함수의 변수에 접근할 수 있는 내부 함수"를 클로저 함수라고 함

  • 어휘적 환경 : 변수 및 함수 선언의 형태

클로저는 반환된 내부함수가 자신이 선언됐을 때의 환경(Lexical environment)인 스코프를 기억하여
자신이 선언됐을 때의 환경(스코프) 밖에서 호출되어도 그 환경(스코프)에 접근할 수 있는 함수를 말한다.
간단히 말하면 클로저는 자신이 생성될 때의 환경(Lexcial envrionment)을 기억하는 함수

실행 컨텍스트의 관점에 설명하면,
내부함수가 유효한 상태에서 외부함수가 종료하여 외부함수의 실행 컨텍스트가 반환되어도,
외부함수 실행 컨텍스트 내의 활성 객체(Activation object) (변수, 함수 선언 등의 정보를 가지고 있다)는 내부함수에 의해 참조되는 한 유효하여
내부함수가 스코프 체인을 통해 참조할 수 있는 것을 의미한다

✅ 클로저 함수의 정의와 특징

  • 함수를 리턴하는 함수 (함수와 함수가 선언된 형태)
  • 내부 함수는 외부 함수에 선언된 변수에 접근 가능

✅ 클로저가 갖는 스코프의 범위

  • 리턴하는 함수에 의해 스코프(변수의 접근 범위)가 구분됨
  • 클로저의 핵심은 스코프를 이용해서 변수의 접근 범위를 닫는(closure: 폐쇄) 데에 있음.
    ⇒ 함수를 리턴하는 것만큼이나, 변수가 선언된 곳이 중요함.

✅ 클로저 활용 (유용하게 쓰이는 몇 가지 패턴 이해)

  1. 데이터를 보존하는 함수

    • 외부 함수(adder)의 실행이 끝나도, 외부 함수 내 변수(x) 사용 가능
    • state 가 의도치 않게 변경되지 않도록 state를 안전하게 은닉
    • 특정 함수에게만 state 변경을 허용하기 위해 사용

    💡 일반적인 함수는 실행이 끝나고 나면 함수 내부 변수를 사용할 수 없음.
    이와 달리, 클로저는 외부 함수의 실행이 끝나도 외부 함수 내 변수가 메모리 상에 저장됨
    (어휘적 환경을 메모리에 저장하기 때문에 가능한 일)

    // 예시
    
    const adder = function (x) {
    	return function (y) {
    		return x + y;
    	}
    }
    
    const add5 = adder(5);
    add5(7); // 12
    add5(10); // 15
  1. 클로저 모듈 패턴 (정보의 접근 제한 : 캡슐화)
    • 클로저를 이용해 내부 함수를 단 하나만 리턴하는 것에 그치지 않고, 객체에 담아 여러 개의 내부 함수를 리턴하도록 만듦
        const makeCounter = () => {
        	let value = 0;
        
        	return {
        		increase: () => {
        			value = value + 1
        		},
        		decrease: () => {
        			value = value - 1
        		},
        		getValue: () => value;
        	}
        }
        
        const counter1 = makeCounter();  // makeCounter를 실행하여 변수에 담음
        counter1 // { increase: f, decrease: f, getValue: f }
  • (makeCounter 함수를 바꾸지 않고) value 라는 변수에 값을 새롭게 할당할 수 있는 방법은?
    ⇒ 없다! 즉 변수 value는 직접 수정이 불가능! ( by 스코프 규칙 : 외부 스코프에서는 내부 스코프의 변수에 접근할 수 없음! )
    ⇒ 대신 리턴하는 객체가 제공하는 메서드를 통해 ‘value’ 값을 간접적으로 조작할 수 있음!
  • 이것이 정보의 접근 제한 (캡슐화)
    ⇒ 만일 스코프로 value 값을 감싸지 않았더라면, value 값은 전역 변수여야만 했을 것
    ⇒ 하지만 makeCounter라는 함수가 value 값을 보존하고 있기 때문에, 전역 변수로 따로 만들 필요가 없음
    클로저를 통해 불필요한 전역 변수 사용을 줄이고, 스코프를 이용해 값을 보다 안전하게 다룰 수 있음
  1. 모듈화
  • 재활용이 가능한 makCounter 함수
    ⇒ 여러 개의 counter를 만드는 것이 가능
    ⇒ 함수 재사용성을 극대화하여 함수 하나를 완전히 독립적 부품 형태로 분리하는 것 (모듈화)
    ⇒ 클로저를 통해 데이터와 메서드를 같이 묶어서 다룰 수 있어서 클로저는 모듈화에 유리함
        const counter1 = makeCounter();
        counter1.increase(); // 1
        counter1.increase(); // 2
        counter1.decrease(); // 1
        counter1.getValue();  // 1
        
        const counter2 = makeCounter();
        counter2.decrease(); // -1
        counter2.decrease(); // -1
        counter2.decrease(); // -1
        counter2.getValue();  // -3
        
        // counter1과 counter2의 value는 서로에게 영향을 끼치지 않음!
        // makeCounter에 의해 리턴된 객체는, makeCounter를 실행할 때에 선언되는 value 값을 각자 독립적으로 가짐 

✅ 스코프, 클로저 관련 문제

  • 문제1. 다음 코드를 실행시킨 후 result 값은? (전역변수와 함수 스코프내 지역변수인 매개변수 구분)
    let x = 10;  // 전역에 선언된 전역 변수
    
    function add (y) {
      return x + y;   // 내부에 선언된 x가 없으므로 스코프 계층에 따라 다음 영역인 전역 스코프에 있는 전역 변수 x가 적용되게 됨
    }
    
    function strangeAdd (x) {    // 여기서 x는 매개변수로 strangeAdd 함수 스코프 안에서 사용되는 지역 변수가 된다
      return add(x) + add(x);    // 아래와 같이 인자가 전달되면 지역변수 x가 해당 함수 스코프 안에서 기능을 하게됨
    }
    
    let result = strangeAdd(5);
    
    // 함수 strangeAdd는 매개변수 x를 통해 전달받은 값을 다시 add 함수에 전달하는 구조로 이루어짐
    // add 함수는 매개변수 y를 통해 받은 값을 전역에 선언된 x와 더하여 반환
    // strangeAdd(5)를 실행하면 add(5) + add(5)가 반환되고, add(5)는 15를 돌려줍니다. 결과적으로 result의 값은 30
    
    // 이건 매개변수 x 와 전역변수 x 를 구분하고 코드를 해석할줄 아는지 묻는 문제인거 같다
  • 문제2. 다음 코드를 실행시킨 후 result 값은? (스코프 체이닝)
    let x = 10;
    
    function outer () {
      let x = 20;
      function inner () {
        return x;
      }
    
      return inner();
    }
    
    let result = outer();
    
    // outer 함수 스코프에는 20을 값으로 하는 변수 x와 함수 inner가 선언되어 있음
    // outer는 inner 함수를 실행시킨 값을 반환
    // inner함수는 x를 반환. 하지만 inner 함수 스코프 내에 x라는 변수가 없기 때문에,
    // 이때 x는 바로 한 단계 위인 outer 함수 스코프의 x가 됨
    // 따라서 result는 20이 됨
    // 위 코드에서처럼 스코프가 위계적으로 겹칠 경우 안쪽 스코프부터 바깥 스코프로, 순차적인 스코프 체이닝이 일어남
  • 문제3. 다음 코드를 실행시킨 후 result 값은? (스코프 계층에 따라 어떤 스코프에 있는 변수가 바뀌는가)
    let x = 10;
    
    function outer () {
      let x = 20;
    
      function inner () {
        x = x + 10;    // 여기서 전역 변수에 반영되는 거라고 생각했는데 스코프 계층에 따라 적용됐던 한단계 위 스코프의 변수가 바뀌는 거네!
        return x;
      }
      inner();
    }
    
    outer();
    let result = x;
    
    // outer 함수를 실행하면, outer 함수 스코프 내에서 inner 함수가 호출
    // inner는 변수 x의 값에 10을 더하는 함수입니다. 이때, inner에 의해 값이 변경되는 x는 inner의 바로 한 단계 위 스코프인 outer에 속한 x (핵심!)
    // 즉 inner가 실행되면서, outer함수 스코프의 변수 x값이 30으로 바뀜 (이 부분이 헷갈렸음)
    // 하지만 변수 result에 할당된 값은 전역 스코프의 x이므로, outer함수가 호출되어도 아무런 영향을 받지 않음
    // 참고로 outer() 함수는 undefined 가 나옴 return을 안했기 때문
  • 문제4. 다음 코드를 실행시킨 후 result 값은?
    let x = 10;
    
    function outer () {
      x = 20;    // 이 부분을 놓침 
    
      function inner () {
        let x
        x = x + 20;
        return x;
      }
      inner();
    }
    
    outer();
    let result = x;
    
    // outer 함수는 전역 변수 x에 20을 재할당합니다. 따라서 result의 값은 20이 됩니다.
    // outer 내부에서 inner 함수가 호출되고 있긴 하지만, inner 함수는 바깥 스코프에 아무런 영향을 미치지 않습니다.
    // inner 함수는 어떤 값을 반환할까요? NaN 을 반환함
    // let x 로 선언만 해주고 값을 아무 것도 할당을 안해준 상태기 때문에 x 는 undefined 인데
    // undefined를 숫자 연산을 하면 NaN 결과가 나오게 됨. 따라서 리턴값 x, innner() 는 NaN 라는 결과가 나옴  
  • 문제5. 다음 코드를 실행시킨 후 result 값은?
    let x = 10;
    
    function outer () {
      x = 20;                // 전역변수 x 에 20을 할당
      function inner () {
        x = x + 20;         // inner함수 스코프 내에 x 변수가 없으니까 상위 스코프에 있는 x = 20이 적용되고
      }
      inner();            // 따라서 inner() 실행 결과는 40 인데 이것도 x = 40 이므로 전역 변수를 40으로 바꾸는 결과가 됨 
    }
    
    outer();           // 따라서 outer() 함수를 실행하면 전역 변수가 40으로 바뀌게 됨
    let result = x;
    
    // outer, inner 함수 모두 전역에 선언된 x값을 변경하는 함수
    // outer가 실행되며 전역의 x가 20이 되고,
    // 이어서 outer 내부에서 inner가 호출되면서 전역의 x값에 또다시 20이 더해짐
    // 따라서 결과는 40

✅ 클로저에 대한 추가 공부

profile
느리지만 꾸준하게. 부족하거나 잘못된 부분은 알려주시면 감사하겠습니다.

0개의 댓글