[JavaScript] 스코프 Scope 와 클로저 Closure

bbio3o·2021년 2월 20일
0

JavaScript

목록 보기
8/11
post-thumbnail

📌 Scope?

스코프는 '범위'라는 의미로 변수의 값에 접근하고 찾아볼 때 들여다보게 되는 곳입니다.
스코프는 전역 스코프지역 스코프로 나눠볼 수 있습니다.


📌 전역 스코프(Global Scope)

자바스크립트 프로그램을 시작 후, 어떤 함수도 호출하지 않았을 때, 실행 흐름은 전역 스코프에 있습니다.
중괄호 {}의 밖을 전역 스코프라고 하고, 전역 스코프에서 선언된 것들을 전역 변수라고 합니다.
window 객체에 변수를 만드는 것이기 때문에 메모리 낭비나 변수 값이 다른 값으로 재정의 될 가능성이 있어 전역 변수를 많이 이용하는 것을 최대한 피해야합니다.

const name = '아메리카노' 
const price = 2500; 
function displayName() { 
  console.log(`Product name is ${name}`); 
} 
function displayPrice() { 
  console.log(`Price is ${price}won`); 
} 
displayName(); // Product name is 아메리카노
displayPrice(); // Price is 2500won

📌 지역 스코프(Local Scope)

중괄호 {} 안을 지역 스코프라고 합니다.
전역 스코프에 선언된 변수가 전역 변수인 것 처럼 지역 스코프에 선언된 변수는 지역 변수라합니다.
지역 스코프에는 함수 스코프, 블록 스코프 두 가지가 있습니다.

함수 스코프 Function Scope

함수 스코프는 ES6이전(선언에 let, const는 없고 var만 사용하던 시절)까지 자바스크립트가 따르던 스코프입니다.
함수스코프는 함수 내부에서 선언한 변수는 지역 변수이며 함수 외부에서 선언한 변수는 모두 전역 변수로 취급합니다.

var name = '이름이 없습니다.';

function displayProduct() { 
  var name = '아메리카노'; 
  var age = 2500; 
  
  console.log(`Product name is ${name}`); 
  console.log(`Price is ${price}won`); 
} 

{ 
  var name = '중괄호 내부에 있는 name입니다.'; 
} 

// Product name is 아메리카노 
// Price is 2500won
displayProduct(); 

console.log(name); // 중괄호 내부에 있는 name입니다.

위의 코드를 보면 블록 스코프에 name을 정의했음에도 전역 변수로 취급되어서 console.log에 '중괄호 내부에 있는 name입니다.'가 출력된 것을 확인할 수 있습니다.


블록 스코프 Block Scope

블록 스코프는 대부분의 프로그래밍에서 따르던 스코프입니다.
자바스크립트에서는 var의 단점(var 호이스팅, var 키워드 생략, 변수 중복 허용)을 보완하기 위해 ES6부터 let과 const가 등장하면서 도입된 개념으로 모든 중괄호 {} 안에 선언된 변수는 지역 변수로 취급합니다.

{
  const hello = 'Hello World!'
  console.log(hello) // 'Hello World!'
}
console.log(hello) // Error, hello is not defined

📌 스코프 체인 Scope Chain

전역변수와 지역변수의 관계에서 스코프 체인(scope chain)이란 개념이 나옵니다.

내부 함수에서는 외부 함수의 변수에 접근 가능하지만 외부 함수에서는 내부 함수의 변수에 접근할 수 없습니다.
(아래 dessert가 undefined인 것을 확인해보세요)
그리고 모든 함수들은 전역 객체에 접근할 수 있습니다.

var drink = '아메리카노';

function outer() {
  console.log('외부', drink);
  
  function inner() {
    var dessert = '쿠키';
    console.log('내부', drink);
  }
  
  inner();
}
outer();
console.log(dessert); // undefined 외부에서 내부로 접근 불가

inner 함수는 drink 변수를 찾기 위해 먼저 자기 자신의 스코프에서 찾고,
없으면 한 단계 올라가 outer 스코프에서 찾고,
없으면 다시 올라가 결국 전역 스코프에서 찾습니다.
결국 전역 스코프에서 drink 변수를 찾아서 '아메리카노'라는 값을 얻었습니다.
만약 전역 스코프에도 없다면 변수를 찾지 못하였다는 에러가 발생합니다.
이렇게 꼬리를 물고 계속 범위를 넓히면서 찾는 관계를 스코프 체인이라고 부릅니다.


📌 렉시컬 스코핑 Lexical Scoping

스코프는 함수를 호출할 때가 아니라 선언할 때 생기며 정적 스코프라고도 불립니다.
만약 함수가 중첩되어 있을 때, 앞서 설명한 스코프 체인때문에 내부 함수에 찾는 식별자가 없다면 상위 스코프에서 식별자를 찾아 나갑니다.

function outer(){
    var x = 1;

    function inner(){
        console.log(x);
    }

    inner();
}

outer(); // 1

outer();의 실행 결과는 1입니다.
outer 함수 내부에서 inner 함수를 호출하는데, inner함수에는 x가 없기 때문에
상위 스코프인 outer함수에서 x를 찾습니다.

여기서 inner함수의 상위 스코프가 outer함수라는 것은 어떻게 결정된 것일까요?
inner함수가 outer함수 내부에 선언되어 있기 때문일까요?
아니면 inner함수를 호출한 곳이 outer함수 내부이기 때문일까요?
답은 앞서 설명한 것처럼 자바스크립트에서 스코프는 함수를 선언할 때 생기기 때문입니다.

함수의 상위 스코프를 결정하는데에는 두 가지 방법이 있습니다.
첫 번째 방법은 동적 스코프로 함수가 어디서 호출했는지에 따라 상위 스코프를 결정하고,
두 번째 방법은 렉시컬 스코프로 함수가 어디서 선언되었는지에 따라 상위 스코프를 결정합니다.
아래의 예시에서 동적 스코프라면 foo()의 실행 결과는 10이고,
렉시컬 스코프라면 foo()의 실행 결과는 1이 됩니다.

var x = 1;

function foo() {
  var x = 10;
  bar();
}

function bar() {
  console.log(x);
}

foo(); // 10? 1?
bar(); // 1

실행해보면 렉시컬 스코프를 따른다는 것을 확인할 수 있습니다.
bar함수를 호출한 곳은 foo함수 내부였지만, bar함수가 선언된 곳은 전역이기 때문에 x는 전역에 선언된 1의 값을 가지게 됩니다.


📌 클로저 Closure

클로저를 정의해보자면, 이미 생명주기가 끝난 외부 함수의 변수를 참조하는 함수라고 할 수 있습니다.

function outer(){
	let name = 'minji';

	function inner(){
		console.log(`hello! ${name}`);
	}

	inner();

	return inner;
}

let greeting = outer();
greeting();

outer함수 내부에서 inner 함수를 호출했을 때,
렉시컬 스코프에 따라서 inner함수의 상위 스코프는 outer함수 입니다.
따라서 outer함수에 있는 name 변수에 접근할 수 있고 hello! minji를 찍을 수 있습니다.

greeting 변수에는 outer함수의 리턴값인 inner함수가 담깁니다.
outer함수는 이미 종료되어 콜스택에서 빠져 나갔는데(함수의 생명주기가 끝남),
greeting()을 실행해보면 여전히 name 변수에 접근해 hello! minji을 찍는 것을 확인할 수 있습니다.
이처럼 어떤 함수를 렉시컬 스코프 밖에서 호출해도, 원래 선언되었던 렉시컬 스코프를 기억하고 접근할 수 있도록 하는 특성을 클로저라고 합니다.

📌 클로저 사용 예시들

1. 콜백함수 내 에서 외부 데이터를 사용하고자 할 때

function hello(name){
  setTimeout(() => {
    console.log(`hello, ${name}!`);
  }, 1000)
}

hello('minji')

위의 첫 번째 예시를 실행해보면 기대했던 것과 같이 1초 후에 hello, minji!가 실행됩니다.
콜 스택에 hello, setTimeout 함수가 순차적으로 올라가고
setTimeout의 콜백 함수는 비동기로 처리되기 때문에 event queue에 들어간다.
콜스택이 비었을 때 이 콜백 함수가 이벤트 루프를 통해 콜스택에 올라가서 실행되는데,
이 당시에 hello 함수는 종료되었지만 콜백 함수는 여전히 클로저 때문에 hello 함수 내부에 있던 name에 접근할 수 있습니다.


2. 루프에서의 클로저 생성

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

두 번째 예시의 실행 결과는 1초 간격으로 6을 5번 출력합니다.
1초 간격으로 1부터 5을 5번 출력하는 것을 기대했을 수도 있지만
var로 선언한 변수는 함수 스코프를 따르기 때문에 전역에 선언하는 것과 같습니다.
setTimeout의 콜백 함수가 콜스택에 올라왔을 당시에 참조하는 i의 값은 6으로 6을 5번 출력하게 되는 것입니다.
var을 let으로 변경하여 변수 i의 스코프를 블록 범위로 변경해준다면 우리가 예상했던 것처럼 동작합니다.
아래에서 var을 사용하면서도 예상처럼 실행되는 코드를 살펴봅시다.

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

이렇게 setTimeout 함수를 즉시 실행 함수로 감싸주면 반복문이 돌 때마다 새로운 스코프가 만들어집니다.
따라서 setTimeout의 콜백 함수가 하나씩 콜스택에 올라올 때, 렉시컬 스코프와 클로저 때문에 이전에 넘겨 받았던 j의 값을 기억하고 접근할 수 있게 됩니다.


3. 정보은닉, 캡슐화

캡슐화란 기본적으로 관련된 여러가지 정보(프로퍼티, 메서드)를 하나의 틀에 담는 것을 의미합니다.

캡슐화에서 중요한 것은 정보의 공개 여부인데 자바와 C++같은 객체지향 언어에서는 public, pritvate등의 키워드로 해당 정보를 외부로 노출시킬지 여부를 결정할 수 있습니다.

하지만 자바스크립트에서는 이러한 키워드 자체를 지원하지않으므로 클로저를 이용해야 합니다.

var Blog = function(arg) { 
  var name = arg ? arg : 'minji'; 
                         
   return { 
     getName : function() { 
       return name; 
                          }, 
     setName : function(arg) { 
       name = arg; 
     } 
   }; 
 }  // 모듈 패턴 

var blog = Blog(); /* or new Blog(); */ 
console.log(blog.name); // undefined 
console.log(blog.getName()); // 'minji'

name 변수는 반환된 객체의 메서드들이 클로저 역할을 하면서 비공개 멤버로 지정해 외부에서의 접근이 제한되어 마치 name 변수가 private 멤버처럼 보입니다.

이것이 자바스크립트에서 할 수 있는 가장 기본적인 정보 은닉 방법입니다.

그런데 여기서 위 코드의 단점이 있는데,

사용자가 반환받은 객체는 Blog 함수 객체의 프로토타입에 접근할 수 없다는 단점입니다.
이는 Blog를 부모로하는 프로토타입을 이용한 상속을 구현하기가 어렵다는 것을 의미합니다.
이를 보완하려면 객체를 반환하는것이 아닌, 함수를 반환하는것이 좋습니다.

var Blog = (function(arg) { 
  var name = arg ? arg : 'minji'; 
  
  var F = function() {} 
  
  F.prototype = { 
    getName : function() { 
      return name; 
    },
    setName : function(arg) {
      name = arg;
    } 
  }; 
  
  return F; 
})(); 

var blog = new Blog(); 
console.log(blog.getName()); // 'minji'

📌 클로저 사용시 주의할점

스코프 체인이 생성될 때마다 변수 값들을 보존하고 기억하기 때문에 함수가 메모리에서 없어질 때까지 따라다니게 됩니다.
이러한 클로저의 특성 때문에 잘못 사용하면 성능 문제 와 메모리 문제 가 발생할 수 있습니다.

출처1 출처2 출처3 출처4

profile
그림도 그리는 개발자 🎨👩‍💻

0개의 댓글