[JavaScript] Scope(스코프)와 Closure(클로저) 정리

유진·2021년 1월 31일
1
post-thumbnail

1. Scope

자바스크립트에서의 함수는 선언되는 동시에(lexical) 자신만의 Scope(범위) 를 가진다. 함수가 실행될 때가 아니다. 선언될 때이다. 그래서 자바스크립트에서 함수가 유효한 범위를 Lexical Scope(어휘적 범위)라고 한다. 말 그대로, 코드가 쓰여진 대로 범위를 가진다는 것이다.

그리고 함수의 범위에 따라 그 안에 있는 변수의 범위도 결정된다. 변수의 범위에 따라 비슷해 보이는 코드가 전혀 다르게 동작할 수도 있다. 따라서 코드를 짤 때 함수의 범위를 아는 것은 매우 중요하다.

함수의 범위(Scope)를 결정하는 요소는 다음과 같다.

  1. Local Scope Vs. Global Scope
  2. Function Scope Vs. Block Scope
  3. 전역변수와 window 객체
  4. 선언 없이 초기화된 전역 변수

1) Local Scope Vs. Global Scope (지역범위 vs 전역범위)

안쪽 Scope에서 바깥 변수/함수에 접근하는 것은 가능하지만, 바깥쪽 Scope에서 안쪽 변수/함수에 접근할 수는 없다.

let last_name = 'lee';
function func() {
  let first_name = 'youjin';
  return last_name + ' ' + first_name;
}

func(); // 'lee youjin'
first_name; // ReferenceError
  • func() 함수는 지역 범위(local scope)이다. 지역 범위에서는 바깥 변수/함수에 접근할 수 있으므로, func() 함수 내에서 전역변수 last_name에 접근하였다. 따라서 func() 함수를 실행했을 때는 문제가 없다.
  • first_name은 지역 범위인 func() 함수 내에서 선언되었다. 따라서 func() 함수의 외부에서는 first_name에 접근할 수 없다.

심화된 예제를 보자.

let last_name = 'lee';
function func() {
  let first_name = 'youjin';
  let last_name = 'park';
  return last_name + ' ' + first_name;
}

func(); // 'park youjin'
console.log(last_name); // 'lee'
  • 지역변수는 전역변수보다 우선순위가 높기 때문에, 변수 이름이 같으면 지역변수가 더 우선된다. 여기서는 last_name이 전역과 func() 함수에 선언되었다. func() 함수 안에서 선언된 last_name가 지역변수이므로 더 우선순위가 높다.
  • 그러나 지역변수는 해당 scope 안에서만 유효하며, 바깥 함수에서는 안쪽 함수/변수에 접근하지 못하므로 전역에서의 last_name은 'lee'이다.

2) Function Scope Vs. Block Scope

자바스크립트에서 변수를 선언하는 키워드는 크게 3가지가 있다.

  • var
  • let
  • const

var는 Function Scope를 가진다. 다시 말해, var로 선언된 변수의 scope는 곧 내가 속한 함수의 scope이다.

letconst는 Block Scope를 가진다. let이나 const로 선언된 변수의 scope는 내가 속한 블록(중괄호 { })의 scope이다. (블록의 scope이라 하니 어렵게 느껴지는데, 그냥 중괄호의 시작부터 끝까지이다.)

이렇게만 말하면 감이 잘 안잡히니 예제로 보자.

/* var 키워드를 사용했을 때 */
for (var i=0; i<5; i+=1) {
  console.log(i);		// 0 1 2 3 4
};
console.log(i); 		// 5

/* let 키워드를 사용했을 때 */
for (let j=0; j<5; j+=1) {
  console.log(j);		// 0 1 2 3 4
}
console.log(j);			// ReferenceError
  • var를 사용한 변수는 함수의 scope를 따른다. 여기서는 전역이므로 코드 어디서나 i에 접근할 수 있다. 따라서 for문이 끝나도 i에 접근할 수 있다.
  • let을 사용한 변수는 블록의 scope를 따른다. for문은 블록({ })으로 감싸져 있다. let으로 선언된 j는 for문의 블록에서만 유효하다. 따라서 for문 밖에서는 j에 접근할 수 없다. 바깥 함수에서는 내부 함수/변수에 접근할 수 없기 때문이다.

3) 전역변수와 window 객체

자바스크립트에서는 전역 범위를 대표하는 객체 window가 있다.

자바스크립트의 window 객체

그리고 전역 범위에서 선언된 함수와, 전역 범위에서 var 키워드로 선언된 변수window 객체와 연결된다.

사전역에서 선언된 var 변수와 함수는 모두 window의 속성과 메서드인 셈이다.

var name = 'youjin';

function sayHi() {
  return 'hi, ' + name;
}

console.log(window.name === name);   // true
console.log(window.sayHi === sayHi); // true

주의! 전역 범위에 너무 많은 변수를 선언하지 않도록 조심하자.
전역 범위에 보편적인 이름을 가진 변수를 선언하게 되면, 다른 코드를 불러올 때 변수끼리 충돌할 수도 있다. 또한 지역변수와 이름이 겹치면 내가 예상한 결과가 아닌 엉뚱한 결과가 나올 수 있다! 특히 후자의 경우 코드가 길어질 수록 유지보수하기 힘드니 주의!

4) 선언 없이 초기화된 전역 변수

선언 키워드(var, let, const) 없이 변수를 초기화하면 안된다! 함수 내에서 변수를 선언할 지라도 선언 키워드가 없으면 전역 변수로 취급된다. 심지어 전역변수인데 window 객체와 연결도 안되서 추적도 못한다.

function func() {
  name = 'youjin';
  console.log(name);
}
func();					  // 'youjin'
console.log(name);		  // 'youjin'
console.log(window.name); // undefined

Strict Mode(엄격 모드)를 사용하면 자바스크립트는 문법적으로 실수할 수 있는 부분을 에러로 판단한다.

'use strict';  // Strict Mode로 코드 실행

function func() {
  name = 'youjin';		// Reference Error 발생
  console.log(name);
}
func();					  
console.log(name);		  
console.log(window.name); 

2. Closure

클로저란 외부 함수의 변수에 접근할 수 있는 내부 함수, 또는 이러한 작동 원리를 일컫는 말이다.

function outerFunc(){
  let outerVal = 'outer';
  console.log(outerVal);
  
  function innerFunc() {
    let innerVal = 'inner';
    console.log(innerVal);
  }
  return innerFunc;
}

let globalVal = 'gloabl';
let innerFunc = outerFunc();
innerFunc();

이 예제에서는 innerFunc()가 클로저 함수이다. 클로저 함수에서는 지역변수(innerVal)과 내가 포함된 외부 함수의 변수(outerVal), 그리고 전역 변수(globalVal)에 모두 접근할 수 있다.

이러한 클로저 특성을 이용한 유용한 방법들이 있다.

  1. 커링
  2. 클로저 모듈 패턴

1) 커링

클로저 함수의 외부 함수를 템플릿처럼 사용할 수 있다.

function callFamily (last_name) {
  return function (first_name) {
    return `Hey, ${first_name} ${last_name}`;
  }
}

callFamily('James')('Jones');  // Hey, James Jones

let callLees = callFamily('Lee');

callLees('youjin');	 // Hey, youjin Lee
callLees('youngjae'); // Hey, youngjae Lee

let callHollys = callFamily('Holly');

callLees('wendy');	 // Hey, wendy Holly
callLees('honey'); // Hey, honey Holly

2) 클로저 모듈 패턴

변수를 클로저 함수의 스코프에 가두어 함수 밖으로 노출시키지 않는 방법이다. 공개하고 싶지 않은 변수가 있을 때 유용하다.

function counter() {
  let initialVal = 3;
  
  return {
    add: function() { initialVal += 2 },
    sub: function() { initialVal -= 2 },
    getVal: function() { return initialVal }
  };
}

let calc1 = counter();
calc1.add();
calc1.add();
calc1.getVal();  // 7

let calc2 = counter();
calc2.add();
calc2.sub();
calc2.sub();
calc2.getVal();  // 1

예제를 보면 calc1calc2initialVal을 노출시키지 않으면서 initialVal을 조작하는 것을 볼 수 있다.

profile
제가 또 기가막힌 한 줌의 트러플 소금 같은 존재그등요

0개의 댓글