JavaScript

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

Gray Park 2020. 8. 6. 10:26
728x90
반응형

클로저를 이해하는 데 오래 걸렸다. 그냥 딱 눈에 들어오는 게 아니라, 신경 써서 봐야 보였다. 근데 오늘 죽치고 보고 있으니까 제법 눈에 들어온다. 오늘은 스코프와 클로저에 대해 정리해보자.

시작하기에 앞서, 짧고 강렬하고 쉽게 이해할 수 있는 정리

함수를 선언하는 것은 연예인과 같다. 불러주지않으면 아무것도 하지 않는다. 연예인을 불러주면 ( 함수를 호출하면 ) 그 때에서야 방송에 연예인이 활동 ( 함수가 실행 ) 된다.

그래서 스코프나, 클로져를 구분할 때에 함수를 호출하는 부분부터 보면 이해가 쉽다. 아래의 예제를 호출부터 보도록 하자.

var x = '유재석';
var y = '이효리';
var z = '비';

function 놀면뭐하니 (name) { // 3번: x가 name자리에 파라미터로 넘어옴. 함수 내에서는 name이란 변수로 대신 기능함
  var x = '유재석';
  var y = '이효리';
  var z = '비';
  var result = '';
  
  function 유재석 () { // 6번: 바로 위 함수의 x를 result에 담아 리턴
    x = '유두래곤'
    return result = x;
  }
  function 이효리 () {
    y = '린다G'
    return result = y;
  }
  function 비룡 () {
    z = '비룡'
    return result = z;
  }
  if ( name === x ) { // 4번 : name(파라미터)과 x('유재석')을 비교함
    return 유재석(); // 5번: 함수실행
  }
  if ( name === y ) {
    return 이효리();
  }
  if ( name === z ) {
    return 비룡();
  }
}

var 싹쓰리멤버1 = 놀면뭐하니; // 2번: 함수할당
var 싹쓰리멤버2 = 놀면뭐하니;
var 싹쓰리멤버3 = 놀면뭐하니;
var 싹쓰리 = `${싹쓰리멤버1(x)}  ${싹쓰리멤버2(y)}  ${싹쓰리멤버3(z)}`; // 1번: 함수호출

console.log(싹쓰리멤버1(x), 싹쓰리멤버2(y), 싹쓰리멤버3(z));
// '유두래곤', '린다G', '비룡' // 7번: '유두래곤'
console.log(싹쓰리);
// '유두래곤  린다G  비룡'

번호 순서대로 보자.

1번: 함수호출이 일어난 시점부터 우리가 원하는 코드가 실행되므로 여기부터 보면 된다.

2번: 싹쓰리멤버1만 보겠다. 싹쓰리멤버1에는 함수 놀면뭐하니가 담겨있다.

3번: 놀면뭐하니 함수가 실행되고 파라미터로 1번에서 넣어준 (x)가 넘어온다. 여기서 x는 line 1에서 전역스코프에 선언된 전역변수 x이다.

4번: 파라미터로 ('유재석')이 변수 name에 담겨있고, name과 ( 놀면뭐하니 ) 안에 로컬 스코프에서 선언된 x ( '유재석' ) 과 비교한다.

5번: 내부함수 유재석을 실행한다.

6번: 내부함수인 유재석에는 변수가 선언되어있지 않다. 가장 작은 지역 스코프이고, 가지고 있는 변수는 없지만 외부함수 ( 놀면뭐하니 ) 에서 선언해 준 변수를 사용할 수 있다. 따라서 이 내부함수 유재석은 클로져함수이다. 외부함수의 변수 x에 ( '유두래곤' ) 을 담아 리턴한다.

7번: '유두래곤' 이 나오는 걸 볼 수 있다.

 

이게 이해되면 스코프와 클로져를 완벽히 이해했다고 볼 수 있다.

 

 

Scope ( 스코프 )

스코프는 '변수 접근 규칙에 따른 유효 범위'라고 한다. 한국말 맞나...?

내 블로그니까, 나중에 내가 다시봐도 알아듣기 쉽게 이야기하자.

스코프는 변수가 변수로써 활동할 수 있는 범위이다.

간단한 예로, 아래의 예제에서 console.log(a) 로 line 4에서 선언한 a를 출력하려고 하니 안된다.

let arr = [1, 2, 3];

let fn = function (arr) {
  let a = 'a';
  return arr.push(a);
}

console.log(arr);
// [1, 2, 3]
console.log(a);
// Uncaught ReferenceError: a is not defined

답은 간단하다. a는 fn이라는 함수 안에서 선언된 문자열 'a'를 담고 있는 변수이다. 함수 밖에서는 접근할 수 없다.

 

그렇다면 arr은 어떨까?

arr은 line 1에서 선언되어 console.log(arr) 로 출력하니 결과가 잘 나온다. 같은 범위 내에서 선언되었기 때문이다.

 

이처럼 스코프의 개념은 변수의 활동범위로 확인할 수 있다. let이나 const로 선언한 변수는 이 범위에서 자유롭지 못하다.

아래의 예제를 보고 이제 용어설명을 시작하자.

const 유재석 = ['개그맨', 'MC유', '방송인', '가수', '댄서'];

let 나혼자산다 = function (출연자) {
	const 무지개회원 = ['화사', '박나래', '손담비', '기안84', '이시언', '헨리', '성훈'];
    for ( let 회원 of 무지개회원 ) {
      if ( 출연자 === 회원 ) {
        return '어서오세요. 무지개회원이시네요!';
      }
    }
    return '죄송합니다. 무지개회원이 아니시네요.';
}

let mc유나오나요 = 나혼자산다(유재석);
console.log(mc유나오나요);
// 죄송합니다. 무지개회원이 아니시네요.

Global Scope ( 전역 스코프 ) / Local Scope ( 지역 스코프 )

유재석은 모든 방송사에서 탐내는 대표 MC이다. 위의 코드가 작성된 배경 ( window ) 이 MBC라고 한다면, 유재석은 어디든 갈 수 있는 존재 ( 전역 변수, Global Variable ) 이다. 당연히 전역 변수인 유재석은 전역 스코프 ( Global Scope )를 돌아다닐 수 있다.

 

유재석은 방송국 내의 스튜디오를 자유롭게 다닐 수 있으므로 ( 전역변수이므로 ), 나혼자산다 스튜디오에 방문하려고 했다.

그런데 유재석이 나혼자산다 스튜디오 ( 함수 ) 에 들어가려고 하자,  나혼자산다 측은 유재석이 무지개회원이 아니라고 답변했다.

답답한 유재석이 나혼자산다 무지개회장인 박나래를 불러 이야기하려고 밖으로 나오게 하였으나, 나혼자산다 스튜디오 출입권한만 가진 박나래는 스튜디오 밖으로 나오지 못한다. 여기서 박나래를 포함한 무지개회원은 지역변수 ( Local Variable )이고, 활동 범위는 지역 스코프 ( Local Scope ) 이다.

 

  1. 유재석과 같이 전역 변수를 선언하면, 코드 모든 곳에서 해당 변수를 사용할 수 있다.
  2. 무지개회원과 같이 지역 변수를 선언하면, 지역 스코프를 벗어난 사용이 불가능하다.

유재석은 단 한 명이다. 다른 사람이 유재석이 될 수 없다. 이것을 코드에 표현해준 것이 const이다. const는 constant ( 상수 ) 의 약자로, 고유한 값을 의미한다. 하지만, 유재석은 개그맨으로 시작해 MC, 방송인, 댄서, 가수까지 그 활동영역을 넓혀갔다. 유재석이라는 변수는 const로 선언되었지만, 그의 활동 내용은 언제든지 추가, 수정될 수 있다.

 

방송국에는 PD가 있다. 김태호 PD처럼 하나의 방송을 오래 하는 PD가 있는가 하면, 시청률이 낮아 여러 번 PD가 바뀐 방송도 있다. PD가 바뀌어도 되는 방송이 있고, 바뀌면 재미가 없어지는 방송이 있다. 특히, 요즘은 PD가 함부로 바뀌면 큰일이 난다. 이런 경우를 방지하기 위해 우리는 변수 선언을 let 또는 const를 이용해 local scope로 한다.

변수에는 var이라는 또 다른 선언문이 있는데, var로 선언한 변수는 항상 전역 변수로 선언되고, 언제든지 갈아치워 질 수 있기에 지양한다. 아래의 표를 통해 쉽게 이해하자.

  let const var
유효 범위 Block Scope Block Scope Function Scope
값 재정의 가능 불가능 가능
재선언 불가능 불가능 가능

위에서 설명한 내용은 Global Scope와 Local Scope였다. 그러나 바로 위의 표에는 전혀 다른 스코프가 나온다. 당황하지 말자, 단지 Local Scope 에는 두 가지 형태의 로컬 스코프가 있을 뿐이다. 그것이 블록 스코프 ( Block Scope )와 함수 스코프 ( Function Scope ) 이다.

 

Function Scope ( 함수 스코프 ) / Block Scope ( 블록 스코프 )

Q: 왜 함수 스코프와 블록 스코프인가요?

A: 말 그대롭니다. 저 친구는 함수 단위로만 놀고, 이 친구는 블록단위로 놀아서 함수 스코프와 블록 스코프입니다.

함수 스코프는 직관적으로 이해가 잘 된다. 그에 반해 블록 스코프는 약간 애매하다. 정확히 구분 짓자. 블록은 중괄호{}를 말한다.

아래의 예시를 보자.

let fn = function () {
  const a = 0;
  if ( a === 0 ) {
    var b = 1;
    let c = 2;
  }
  console.log(a);
  // 0
  console.log(b);
  // 1
  console.log(c);
  // Uncaught ReferenceError: c is not defined
}

fn();
0 // console.log(a);
1 // console.log(b);
// Uncaught ReferenceError: c is not defined

const로 선언된 a는 블록 스코프가 유효 범위이다. 위의 경우에 선언된 블록이 함수 전체이므로 지금은 block scope === function scope이다.

b는 var로 선언되어 함수 스코프를 가지는 반면, c는 let으로 선언되어 블록 스코프를 가진다. 결과적으로 c는 if문에 갇혀 나가지 못하기 때문에 console.log(c)에서 에러를 일으킨다.

 

Closures ( 클로저 )

클로저의 정의는 외부 함수의 변수를 사용할 수 있는 내부 함수이다. 말이 어렵다. 난 쉬운 게 좋아. 예제를 봅시다.

function fn1 () { // 1번함수
  const a = 'a'; // 1번함수에 초기화된 변수 a
  
  function fn2 () { // 2번함수
    const b = 'b'; // 2번함수에 초기화된 변수 b
    return a+b; // 2번함수에서 리턴되는 값 a+b
  }
  
  return fn2; // 1번함수에서 리턴되는 2번함수. 괄호()가 안붙어있어서 실행하지 않았다 !중요!
}
// 변수 result에 1번함수를 실행시킨 결과를 담았다.
let result = fn1(); // 1번함수를 실행시키면 결과로 fn2가 온다. 괄호()가 없으므로 실행되지 않았다.
// 변수 result는 1번함수의 리턴값인 fn2를 가지고 있다.
console.log(result()); // 여기에 괄호()를 붙여 실행하니 2번함수가 실행되어 a+b가 리턴된다.
// ab

위의 예제와 주석을 꼼꼼히 읽자. 굉장히 쉽다. 1번 함수의 리턴 값은 2번 함수이다. 그리고 이것을 변수에 담았다. 어디서 많이 보던 방법 아닌가?

let result = fn2;

변수 result에 2번 함수를 담았다. 2번함수 fn2 대신에, fn2를 선언한 부분을 대신하여 넣어보겠다.

const a = 'a';
let result = function fn2 () { // 2번함수
    const b = 'b'; // 2번함수에 초기화된 변수 b
    return a+b; // 2번함수에서 리턴되는 값 a+b
  }
  
result();
// ab
fn2();
// Uncaught ReferenceError: fn2 is not defined

이거 바로 실행해보자. 분명 함수 이름을 result로 하는 함수 표현식인데 fn2가 붙어있다! 그러나 그 결과에서는, result만 함수로 인식되고 fn2는 정의되지 않은 걸로 나온다!

 

이를 통해 유추하자. 결국 클로저는 함수를 변수처럼 선언해둔 것이다. 클로저 함수를 사용하기 위해서는 조금 더 큰 범위의 함수스코프에서 클로저함수를 호출해야 한다. 그리고 그 방법의 중간에 조금 더 큰 범위의 함수를 변수에 담고, 마치 일반 함수 호출처럼 사용하면 클로저 함수가 실행된다.

 

클로저 함수에 대한 이해가 끝났다면, 조금 명확하게 하자.

클로저함수 외부의 변수를 클로저 함수는 사용할 수 있다. ( 클로저 대신에 내부라는 표현으로 바꾸어 보라! 당연한 말이 된다)

다음에 보호해야 마땅한 다양한 이벤트나 사이드 이펙트를 사용하게 될 때, 해당 내용과 함께 다시 리마인드 해보자.

728x90
반응형