스코프와 클로저

자바스크립트에서 스코프와 클로저는 빠질 수 없는 부분이다.(물론 어떤것도 뺄 순 없지만…) 다른 언어를 접하던 프로그래머들이 다르게 작동하는 자바스크립트의 스코프와 접하기 어려운 클로저 때문에 고생이다.

자바스크립트에서 클로저는 상당히 어려운 부분에 속한다 하지만 이해만하고 익숙해진다면 객체지향 언어보다 유연함을 느낄 수 있을것이다.

스코프(scope)

스코프란 현재 접근할 수 있는 변수들의 범위를 뜻한다 어떠한 변수가 스코프 안에 선언되었으면 해당 스코프 안에서는 변수에 접근해서 읽거나 쓸 수 있고, 스코프 밖에서는 해당 변수에 접근할 수 없다.

스코프에는 크게 두 종류의 종류가 있다.

  • 전역 스코프(global scope)
  • 지역 스코프(local scope)

전역 스코프 (global scope)

전역 스코프는 변수가 함수 바깥이나 중괄호 {} 바깥에 선언되었다면, 전역 스코프에 정의 된다.

1) 전역 변수

전역 변수를 선언하면 코드 모든 곳에서 해당 변수를 사용할 수 있다 함수에서도 가능하다.

//1) 전역변수
var globalVar = "global";

function hello () {
  console.log(globalVar);
}

console.log(globalVar);
hello();

//global
//global

2) const,let

let globalLet = "globar";
let globalLet = "globarLet";

/*
에러
Uncaught SyntaxError: Identifier 'globalLet' has already been declared
*/

var globalVar = "globarVar";
var globalVar = "globarVariable";

console.log(globalVar);
// globarVariable

왠만하면 전역 스코프에 변수를 선언 안하는것이 좋으며, const, let을 사용하여 두개 이상의 변수의 이름이 같은 경우 이름 충돌이 발생하여 에러가 발생된다 만약 var 의 경우 두개 이상의 변수를 선언할 경우 두번째 변수가 첫번째 변수를 덮어쓰게 된다. 이러한 문제는 프로그래머들에게 디버깅의 어려움을 발생 시킨다.

언제나 전역 변수보다는 지역 변수로 변수를 선언해야 한다.

const,let,var은 다음에 더 자세히 다루도록 하자

지역 스코프 (local scope)

코드의 특정 부분에만 사용이 가능한 변수이며 자바스크립트에서는 두 가지의 지역 스코프가 있다.

  • 함수 스코프

    • 함수 내부에서 변수를 선언한다면, 그 변수는 선언한 함수 내부에서만 사용이 가능하다.
function localFunc () {
  var localF = "Hello local scope";
  console.log(localF);
}
localFunc();
console.log(localF);
//"Hello local scope"
//에러 - Uncaught ReferenceError: localF is not defined
  • 블록 스코프

    • 중괄호 {} 내부에서 const,let 으로 변수를 선언하면, 그 변수들은 중괄호 블록 내부에서만 접근이 가능하다.
    • 함수를 선언 할 때 중괄호를 사용해야 하므로 블록 스코프는 함수 스코프의 서브셋(subset)이다.(화살표 함수를 사용하여 반환하는 경우는 제외)
    {
    const blockHello = "block scope Hello"
    console.log(blockHello);
    }
    console.log(blockHello);
    //"block scope Hello"
    //Uncaught ReferenceError: blockHello is not defined

함수 호이스팅(Function hoisting)

함수는 함수 선언식(Function declaration)으로 선언될 경우, 현재 스코프의 최상단으로 호이스팅(hoisting) 된다.

//호이스팅으로 같은 결과를 볼 수 있다.

hoistingFunc();
function hoistingFunc () {
  console.log('function hoisting');
}

function hoistingFunc () {
  console.log('function hoisting');
}
hoistingFunc();
//'function hoisting'
//'function hoisting'

하지만 함수 표현식으로 선언할 경우, 함수는 스코프의 최상단으로 호이스팅 되지 않는다.

hoistingFunc();
var hoistingFunc = function () {
  console.log('function hoisting');
}
//Uncaught SyntaxError: Identifier 'hoistingFunc' has already been declaredat

두 방식이 결과가 다르기 때문에 함수 호이스팅은 사용하면 안된다 이유는 함수 호이스팅은 프로그래머에게 혼란을 줄 수 있기 때문이다. 함수를 호출 하기전에 선언해 두어야한다.

함수는 서로의 스코프에 접근할 수 없다

함수들이 각각 선언 되었을 때, 서로의 스코프에는 접근할 수 없다. 어떤 함수가 다른 함수에서 사용더라도 불가하다.

function firstFunc () {
  var firstVar = "First Variable";
}

function secondFunc () {
  firstFunc();
  console.log(firstVar)
}
secondFunc();

//Uncaught ReferenceError: firstVar is not definedat secondFunc

secondFunc 함수는 firstFunc 함수안에 변수 firstVar에 접근 할 수 없다.

렉시컬 스코프

렉시컬 스코핑은 함수가 다른 함수 내부에서 정의 된다면, 내부 함수는 외부 함수의 변수에 접근할 수 있다

function lexicalFunc () {
  var lexicalVar = "렉시컬 스코핑 outer";
  
  function lexicalInner () {
    var lexicalin = "렉시컬 스코핑 inner";
    console.log(lexicalVar);
  }
  lexicalInner();
  console.log(lexicalin);
}
lexicalFunc();
//렉시컬 스코핑 outer
//Uncaught ReferenceError: lexicalin is not definedat lexicalFunc

위 결과 처럼 외부 함수가 내부 함수의 변수에는 접근 할 수 없다

스코프의 동작을 예를 들자면 영화에 나오는 경찰들의 취조실을 보면 이해가 쉽다. 취조하는 방을 안쪽에서는 볼 수 있지만 취조하는 방에서는 안쪽을 볼 수 없는 구조이다.

클로저(Closures)

클로저란 함수 내부에 함수를 작성할 때 마다 클로저를 생성하는 것이다 내부에 작성된 함수가[오류로 인한 수정] 외부 함수에서 내부 함수로는 닫혀 있지만, 내부 함수에서 외부 함수로는 열려있는 구조를 바로 클로저라 한다. 클로저는 외부 함수의 변수를 사용할 수 있기 때문에 대개 반환하여 사용한다.

특정 함수가 참조하는 변수들이 선언된 렉시컬 스코프는 계속 유지되는데, 그 함수와 스코프를 묶어서 클로저라고 한다. (더 쉽게 말하면 이미 생명 주기가 끝난 외부 함수의 변수를 참조하는 함수)
function outer () {
	var counter = 0;
  return function inner () {
    return ++counter;
  };
}
var increase = outer();

console.log(increase());
console.log(increase());

// 1
// 2

가장 기본적인 환경은 스코프 안에 스코프가 있을때, 즉 function 안에 function이 선언되었을 때이다.

클로저는 외부 함수의 변수에 접근할 수 있기 때문에 일반적으로 두가지 목적을 위해 사용한다.

  1. 사이드 이펙트 제어
  2. Private 변수 생성

사이드 이펙트(side effects) 제어

함수에서 값을 반환할 때를 제외하고 무언가를 행할 때 사이드 이펙트1가 발생한다. 여러가지 있지만 예를 들면 Ajax 요청, timeout을 생성할 때, console.log를 선언할때 조차 사이드 이펙트가 발생한다.

보통 Ajax나 timeout과 같이 코드 흐름을 방해하는 것을 위해 클로저를 활용하여 사이드 이펙트를 제어한다.

예를 들어 친구들에게 파스타를 만들어준다고 생각해보자

function makePasta () {
	setTimeout(function(){
		console.log('파스타를 만들자!');
	},1000)
}
makePasta();
//파스타를 만들자!

위 함수를 실행하면 1초 뒤 ‘파스타를 만들자’를 볼 수 있다.

하지만 사람마다 좋아하는 파스타가 다르기 때문에 어떤 파스타를 좋아하는지 알 수 있게 변경해보자

function makePasta (flavor) {
	setTimeout(function(){
		console.log((flavor) + ' 파스타가 좋아!');
	},1000)
}
makePasta('미트볼');
//미트볼 파스타가 좋아!

하지만 위 코드와 같이 진행 한다면 맛을 알자마자 파스타를 만들어야 한다. 요리하는 사람이 원하는 시점에 파스타를 만들 수 있어야한다.

function preparePasta (flavor) {
  return function () {
    setTimeout(function(){
			console.log((flavor) + ' 파스타를 만들자!');
		},1000)
  }
}
var makepasta = preparePasta('봉골레');
makepasta();
//봉골레 파스타를 만들자!

위 코드와 같이 클로저를 사용하여 사이드 이펙트를 줄일 수 있다. 원할 때 내부 클로저를 호출할 수 있는 함수를 만들었다.

Private 변수 와 클로저

함수 내의 변수는 함수 바깥에서 접근할 수 없다 접근 할 수 없기 때문에 private(은밀한) 변수라고 불린다.

하지만, private 변수에 접근해야할 때가 있다, 이것 또한 클로저를 사용할 수 있다.

function secretFunc (secretCode) {
  return {
    saySecretCode () {
      console.log(secretCode)
    }
  }
}
const theSecret = secret('CSS Tricks is amazing')
theSecret.saySecretCode()

위 코드는 saySecretCode는 유일하게 secret 함수 바깥에서 secretCode를 노출하는 함수(클로저)이다 따라서, 이런 함수를 특권함수(Privileged function)라고 부른다.

스코프와 클로저의 개념은 꼭 알아두어야하며 아직 모르는 부분도 많다 나중에 더 자세히 심도 깊게 알아볼 생각이다.

참고자료


  1. ‘부작용’이라고 해석할 수 있지만 프로그래밍에서는 ‘실행 중에 어떤 객체를 접근해서 변화가 일어나는 행위(라이브러리 I/O, 객체 변경 등)’ 라고 정의할 수 있다.