[Javascript] Scope(유효범위),Closure(클로저)

신세원·2020년 9월 19일
2

javascript

목록 보기
18/19
post-thumbnail

            	<한쪽에서 보이는 취조실 유리, 스코프의 접근과 비슷하다>

javascript에서 Scope(유효범위)라는 개념은 너무 굉장히 중요하다.

그렇다면 왜 중요할까?

  • Scope(유효범위)어떤 변수들에게 접근할 수 있는지를 정의하고 javascript 뿐만 아니라 모든 프로그래밍 언어 코드의 가장 기본적인 개념의 하나이다.

  • javascript의 Scope(유효범위)는 다른 언어의 유효범위와는 다르다.

Scope(유효범위) 개념은 쉬운 내용이면서 쉽게 이해하지 못할 함정에 빠지기도 한다.

기본이 탄탄해야 된다는 말이 있듯이 기본이 되야하는 Scope(유효범위)에 대해 알아보도록 하자.

Scope(유효범위)

Scope를 직역하면 영역,범위라는 뜻이다. 하지만 프로그래밍 언어측면에서 유효범위는

어느 범위까지 참고하는지, 변수와 매개변수(parameter)의 접근성과 생존기간을 뜻한다.

따라서 유효범위를 잘 숙지하고 있으면 변수와 매개변수의 접근성과 생존기간을 잘 제어할 수 있다.

javascript에선 두 가지의 종류의 스코프가 있다.

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

전역 스코프(Global Scope)

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

const globalVariable = "hello world"

전역 스코프를 선언하면, 코드 모든 곳에서 해당 변수를 참조할 수 있다.(함수에서도 포함)

const fruit = 'apple'

function delicious(){
  	console.log(fruit)
}

console.log(fruit) // 'apple'
console.log(delicious) // 'apple'

전역 스코프에 변수를 선언할 수 있어도, 가급적이면 그러지 않는 것이 좋다.

왜냐하면, 두개 이상의 변수 이름이 중복되어 충돌될 수 있기 때문이다.

만약 변수에 const,let을 사용하여 선언하였다면, 이름에 충돌이 발생할 때마다 에러가 발생한다.

//이렇게 하면 안돼요!!
let a = 'something'
let a = 'something else' //Error, a has already been declared

만약 var을 사용하였다면 두번째 변수가 첫번째 변수를 덮어쓰게 된다.

이러면 디버깅이 어려워지기 때문이 이렇게 사용하면 안된다.(ES6에선 var를 사용하지 않는다.)

//이렇게 하면 안돼요!!
var a = 'something'
var a = 'something else'

console.log(a); // 'something else'

그래서 우리는 전역변수가 아닌, 지역변수로 변수를 선언해야 한다.

지역 스코프(Locol Scope)

우리 코드의 특정 부분에서만 사용할 수 있는 변수는 지역 스코프에 있다. 그리고 이런 변수들은 지역 변수라고 불린다.

javascript에서는 두가지 지역 변수가 존재한다.

  • 함수 스코프(function Scope)
  • 블록 스코프(block Scope)

먼저 함수 스코프(function Scope)에 대해 알아보자.

함수 스코프(function Scope)

우리가 함수 내부에서 변수를 선언하게 되면, 그 변수는 함수내에서만 접근이 가능하다.

함수 바깥에서는 해당 변수에 접근할 수 없다.

function sayHello(){
const hello = 'hello world'
  	console.log(hello);
}

sayHello() // 'hello world'
console.log(hello) //  Error, hello is not defined

위 예제를 살펴보면 변수 hello는 sayHello의 스코프 내에 존재한다는 것을 알 수 있다.

블록 스코프(Block Scope)

중괄호({}) 내부에서 const,let 변수를 선언하면, 그 변수들은 블록 내부에서만 접근이 가능하다.

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

위 예제에서 볼 수 있듯이 변수 hello는 중괄호 내부의 스코프에 존재한다.

함수 호이스팅(Function hoisting)과 스코프

함수가 함수 선언식(Function Declaration)으로 선언되면, 현재 스코프의 최상단으로 호이스팅 된다.

다음 예제에서는 두 가지 경우가 같은 결과를 보인다.

// 아래 두 가지 경우는 같은 결과를 보인다.
sayHello()
function sayHello () {
  console.log('Hello world');
}
// 위 코드와 동일하다.
function sayHello () {
  console.log('Hello world');
}
sayHello()

반면 함수 표현식(function expression)으로 선언될 경우 함수는 최상단으로 호이스팅 되지 않는다.

sayHello() // Error, sayHello is not defined
const sayHello = function () {
  console.log('Hello world')
}

이렇게 함수 선언문과 함수 표현식을 알아보았고, 두 가지의 방식이 다르기 때문에 함수 호이스팅에 혼란스러 울 수 있어 아무렇게나 사용하면 안된다.

언제나 함수를 호출하기 전에 선언해 놓아야 한다.

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

함수들이 각각 선언되었을 때, 서로의 스코프에는 접근할 수 없다.

아래의 예제에서, 함수 second는 변수 firstFunction에 접근할 수 없습니다.

function first () {
  const firstFunction = 'hello world'
}
function second () {
  first()
  console.log(firstFunction) // Error, firstFunction is not defined
}

함수가 다른 함수 내부에서 정의 되었다면 내부 함수는 외부 함수의 변수에 접근할 수 있다.

이런 행동을 렉시컬 스코핑(lexical scoping)이라고 부른다.

function outerFunction () {
  const outer = 'I’m the outer function!'
    
  function innerFunction() {
     const inner = 'I’m the inner function!'
     console.log(outer) // I’m the outer function!
  }
    
  console.log(inner) // Error, inner is not defined
}

맨 처음에 취조실 사진을 보았을것이다. 스코프가 어떻게 동작하는지 그림을 그려보자면, 취조실 특수 유리(단방향 투과성 유리)를 상상하면 된다.

만약 스코프 내부에 스코프가 있다면, 여러장의 취조실 유리가 겹쳐진 것처럼 그림을 그릴 수 있다.

우리가 바깥을 바라볼 수는 있지만, 바깥에 있는 사람들은 우리를 볼 수 없다.

이제 스코프에 대한 이해가 끝났으니, 클로저에 대해 공부 해보도록 하자.

Closure(클로저)

MDN에서 클로저는 아래와 같이 정의된다.

클로저는 함수와 함수가 선언된 어휘적 환경의 조합이다.
클로저를 이해하려면 자바스크립트가 어떻게 변수의 유효범위를 지정하는지(Lexical scoping)를 먼저 이해해야 한다.

그치만 필자인 나도 이렇게 보기만 해선 너무 추상적이라 더 내용을 찾아보았다.

아래는 실제 V8엔진의 클로저 테스트코드와 사람들이 말하는 클로저의 의미이다.

종합해보면, 함수가 무엇을 기억하고 다시 사용한다고 알 수 있는데 여전히 애매모호하다.

클로저를 현대 프로그래밍에서 다음과 같이 해석하여 정의할 수 있다.

클로저 = 함수 + 함수를 둘러싼 환경(Lexical environment)

함수를 둘러싼 환경이라는 것은 앞에서 설명한 렉시컬 스코프이다.

함수를 만들고 그 함수 내부의 코드가 탐색하는 스코프를 함수 생성 당시의 렉시컬 스코프로 고정하면 바로 클로저가 되는 것이다.

렉시컬 스코핑은 위에서 정리해보았다.

Javascript의 클로저

javascript에서 클로저는 함수가 생성되는 시점에 생성된다. = 함수가 생성될때, 그 함수의 렉시컬 환경을 포섭(closure)하여 실행될 때 이용한다.

실제로 javascript에서 모든 함수는 클로저이지만, 실제로 우리는 모든 함수를 전부 클로저라고 부르지 않는다.

예제를 보며 클로저의 의미를 조금 더 정확하게 파악해보자.

function foo() {
    const color = 'blue';
    function bar() {
        console.log(color);
    }
    bar();
}
foo();

bar함수는 우리가 부르는 클로저일까?

barfoo안에 속하기 때문에 foo를 외부 스코프(outer lexical environment) 참조로 저장한다.

그리고 barfoocolor를 정확히 참조 할 것이다.

그렇다면 bar는 우리가 흔히 부르는 클로저 일까?

정답은 'No'이다.

barfoo안에 정의되고 실행되었을 뿐, foo밖으로 나오지 않았기 때문에 클로저라 부를수 없다.

그러면 클로저라 부르는 코드 예제를 보기로하자.

const color = 'red';

function foo() {
    const color = 'blue'; // 2
    function bar() {
        console.log(color); // 1
    }
    return bar;
}

const baz = foo(); // 3
baz(); // 4
  1. barcolor를 찾아 출력하는 함수로 정의되었다.
  2. bar는 외부 스코프의 참조로 foo의 스코프인 color를 참조하였다.
  3. barglobalbaz라는 값으로 할당했다.
  4. global에서 baz(=bar)를 호출했다.
  5. baz(=bar)는 자신의 스코프에서 color를 찾는다.
  6. 없다. 자신의 외부 스코프 참조를 찾아간다.
  7. 자신의 외부 스코프인 foo의 스코프를 뒤진다.
  8. color를 찾았다. 값은 blue이다. 따라서 blue가 출력된다.

그렇다면 첫번째의 코드와 현재 코드의 차이점을 비교해보자.

유심있게 보아야 할 부분은 2~4번,7번이다.

bar는 자신이 생성된 렉시컬 스코프에서 벗어나 global에서 baz라는 이름으로 호출이 되었고, 스코프 탐색은 현재와 관련 없는 foo를 탐색한다.

bazbar로 초기화 할때는 이미 bar외부 스코프(outer lexical environment) 참조foo로 결정한 이후이다.

때문에 global에서 아무리 bar를 호출해도 여전히 foo에서 color를 찾는것이다.

이런 baz(=bar)함수를 '클로저'라고 부른다.

마지막으로 클로저의 문제중 많은 사람들을 헷갈리게 했던 유명한 문제가 있다.

아래를 보도록 하자.

반복문 클로저

function count() {
    var i;
    for (i = 1; i < 10; i++) {
        setTimeout(function timer() {
            console.log(i);
        }, i*100);
    }
}
count();

우리의 목표는 1,2,3...9를 0.1초마다 출력하는 것이 목표였는데, 막상 실행시켜보면 10이 9번 출력 되었다.

무엇이 문제일까?

위 코드의 실행순서를 나열해 보자.

  1. timer 함수는 상위 스코프인 count에게 i를 요청한다.
  2. timer 는 0.1초 뒤에 호출된다.
  3. 첫 0.1초가 지날 동안 이미 i10이 되었다.
  4. timer는 0.1초 주기로 호출될 때마다 count에서 i를 찾는다.
  5. timer는 이미 10이 되어 버린 i만 출력하게 된다.

그렇다면 의도대로 1~9까지 순서대로 출력하고 싶으면 어떻게 해야할까?

1. 새로운 스코프를 추가하여 반복 시마다 그곳에 각각 따로 값을 저장하는 방식

function count() {
    var i;
      for (i = 1; i < 10; i += 1) {
          (function(countingNumber){
          setTimeout(function timer() {
              console.log(countingNumber);
          }, i*100);
      })(i);
  }
}
count();

중간에 IIFE(즉시실행함수)를 덧붙여 setTimeout()에 걸린 익명함수를 클로저로 만들었다.

앞서 말한대로 클로저는 만들어진 환경을 기억한다.

이 코드에서 i는 1부터 시작해서 IIFE(즉시실행함수)내에 countingNumber라는 형태로 주입되어 반복문이 실행되고,

클로저에 의해 각기 다른 환경속에 포함된다.

반복문은 10회 반복되므로 10개의 환경이 생길 것이고, 10개의 서로 다른 환경에 10개의 서로 다른 countingNumber가 생긴다.

2. ES6에서 추가된 블록 스코프를 이용하는 방식

ES6이후론 이 예제가 문제가 되지 않는다.

function count() {
     'use strict';
     for (let i = 1; i < 10; i += 1) {
         setTimeout(function timer() {
             console.log(i);
         }, i * 100);
     }
 }
 count();

ES6이후론 이 예제가 문제가 되지 않는다.

let과 const를 사용하면 위와 같은 문제가 발생하지 않는다.

profile
생각하는대로 살지 않으면, 사는대로 생각하게 된다.

1개의 댓글

comment-user-thumbnail
2020년 9월 21일

와 깔끔하게 정리 잘하셨네요 유용하게 참고하고 갑니다 댓글 꾹

답글 달기