들어가기 전에

자바스크립트 개발자라면 알아야 할 33가지 개념 #6 함수와 블록 스코프 (번역)

스코프(Scope)

자바스크립트에서 스코프는 어떤 변수에 접근할 수 있는지를 정의합니다. 일반적으로 2가지의 스코프가 존재합니다. 전역 스코프와 지역 스코프가 존재하죠.

전역 스코프(Global Scope)

만일 만일 변수가 모든 함수에 속하지 않고 {}괄호안에 들어있지도 않다면 우리는 그 변수를 전역 변수라 합니다.

사실 Node.js에서의 전역 변수 선언은 약간 다르지만 여기서는 웹 브라우저 내부에서의 자바스크립트만 다룰 것이기 때문에 Node.js에서의 전역변수 선언은 다루지 않습니다.

const globalVariable = 'some value'

일단 전역 변수를 선언하면, 자바스크립트 코드 어디에서든 불러 쓸 수 있습니다. 심지어 함수 내부에서도요.

const hello = 'Hello CSS-Tricks Reader!'

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

console.log(hello); // 'Hello CSS-Tricks Reader!'
sayHello(); // 'Hello CSS-Tricks Reader!'

전역 변수에서 변수 선언을 할 수 있더라도, 전역 변수에서는 변수 선언을 하지 않는 것이 권장됩니다. 왜냐하면 두개 혹은 그 이상의 변수들이 같은 이름을 가지게 되어 네이밍 충돌(naming collisions)이 발생할 확률이 있기 때문입니다.

만일 우리가 const 또는 let 키워드로 같은 이름의 변수를 선언하게 된다면 우리는 에러를 보게 될 것입니다.

// Don't do this!
let thing = 'something'
let thing = 'something else' // Error, thing has already decleared

만일 우리가 var 키워드로 변수 선언을 한다면, 우리의 두번째 변수가 첫번째 변수를 덮어씁니다. 이러한 코드를 만들게 되면 디버그 하기가 매우 힘드니 우리는 절대로 이러한 코드를 작성하면 안됩니다.

// Don't do this!
var thing = 'something'
var thing = 'something else' // perhaps somewhere totally different in your code
console.log(thing) // 'something else'

우리는 전역변수 사용을 자제해야 합니다.
지역변수 사용을 지향합시다.

지역 스코프(Local Scope)

코드 내 특정 구역에서만 사용할 수 있는 변수를 지역변수라고 합니다.
자바스크립트에서는 두가지 종류의 지역 변수가 있습니다.

  1. 함수 스코프 지역 변수
  2. 블록 스코프 지역 변수

함수 스코프 지역변수부터 알아봅시다.

함수 스코프(Function Scope)

함수 내에서 변수를 선언했을 때, 우리는 함수 안에서만 이 변수에 접근할 수 있습니다. 우리가 함수 밖으로 나오게 된 이후에는 함수 내부에 있는 변수에 접근할 수 없습니다.

아래 sayHello함수 안에 들어있는 hello라는 변수의 예제를 봅시다.

function sayHello () {
    const hello = 'Hello CSS-Tricks Reader!';
    console.log(hello);
}

sayHello(); // 'Hello CSS-Tricks Reader!'
console.log(hello) // Error, hello is not defined

블록 스코프(Block Scope)

우리가 변수를 {} 괄호 안에 constlet키워드로 선언했을 때, 우리는 {}괄호 안에서만 이 변수에 접근할 수 있습니다.

아래 예제에서, hello{}괄호 안의 스코프를 갖습니다.

{
    const hello = 'Hello CSS-Tricks Reader!'
    console.log(hello) // 'Hello CSS-Tricks Reader!'
}

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

블록 스코프는 함수 스코프의 부분집합입니다. 함수는 {}괄호로 작성되어야 하기 때문이죠. 물론 화살표 함수로 즉시 리턴하면 {}없이 함수를 만들 수도 있습니다. 그렇지 않은 경우에는 모두 {}괄호로 작성되어야 하죠.

함수 호이스팅과 스코프

(주: 호이스팅은 '끌어올리다' 라는 의미를 가지고 있습니다.)

function 키워드와 함께 선언된 함수들은 항상 현재 스코프의 가장 위로 호이스팅됩니다.
그래서 다음 코드의 결과는 같습니다.

// 이 코드의 결과는 아래의 코드와 같습니다.
sayHello();
function sayHello () {
    console.log('Hello CSS-Tricks Reader!');
}

// 이 코드의 결과는 위의 코드의 결과와 같습니다.
function sayHello() {
    console.log('Hello CSS-Tricks Reader!'); 
}
sayHello();

함수 표현식으로 작성된 함수들은 현재 스코프의 가장 위로 호이스팅 되지 않습니다.

sayHello(); // Error, sayHello is not defined
const sayHello = function () {
    console.log('Hello CSS-Tricks Reader!');  
}

이러한 두가지 방법 때문에 함수 호이스팅은 헷갈릴 수 있습니다. 그리고 사용되어선 안됩니다. 항상 함수는 사용 전에 미리 선언하세요.

함수는 각자의 스코프에 접근할 수 없다

함수를 각각 선언했을 때, 함수는 다른 함수의 스코프에 접근할 권한을 갖고 있지 않습니다. 심지어 함수 내에서 다른 함수를 불러오더라도 스코프는 사용할 수 없습니다.

아래 예제에서, secondfirstFunctionVariable에 접근할 권한이 없습니다.

function first () {
    const firstFunctionVariable = `I'm part of first`;
}

function second () {
    first();
    console.log(firstFunctionVariable); // Error, firstFunctionVariable is not defined
}

내부 스코프(Nested Scope)

함수가 다른 함수 안에서 만들어졌고 안쪽 함수(inner function)는 바깥 함수(outer function)의 변수에 접근 가능합니다. 이러한 것을 어휘적 스코프(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
}

스코프가 어떻게 작동하는지 시각화하기 위해서는 안에서는 바깥이 보이지만 바깥에서는 안이 보이지 않는 유리(one-way glass)를 생각하시면 쉽습니다.

one-way-glass_l5dg2g.webp

안에서는 바깥이 보이지만, 바깥에서는 안이 보이지 않습니다.

우리가 스코프 안에 스코프를 계속 둔다면, 한방향 유리(one-way glass)의 여러 레이어를 생각해보시면 편합니다.

glass-layers_iohiy2.webp

여러 겹의 함수들은 여러 겹의 한방향 유리를 의미합니다.

여기까지 잘 이해하셨으면 클로져(closures)가 무엇인지도 금방 이해하게 되실 겁니다.

클로져(Closures)

우리가 함수 안에서 또다른 함수를 만들 때마다 사실 우리는 클로져를 만든겁니다. 안쪽 함수가 클로져입니다. 일반적으로 반환시키기 위해서 클로져를 만듭니다. 우리는 반환된 클로져를 이용하여 바깥 함수의 변수들을 사용할 수 있습니다.

function outerFunction () {
    const outer = `I see the outer variable!`;

    function innerFunction () {
        console.log(outer);
    }

      return innerFunction;
}

outerFunction()(); // I see the outer variable!

안쪽 함수(inner function)가 반환되는 기능을 구현할 때 함수 선언 중 반환 문을 작성함으로써 우리는 코드의 길이를 조금 더 줄일 수 있습니다.

function outerFunction () {
    const outer = `I see the outer variable!`;

    return function innerFunction () {
        console.log(outer);
    }
}

outerFunction()() // I see the outer variable!

클로져는 바깥 함수의 변수에 접근할 수 있기 때문에 주로 두가지 이유로 쓰입니다.

  1. 사이드 이펙트(side effects)를 제어하기 위해서
    • (주: 사이드 이펙트란 어떤 함수 내에서 자신의 스코프가 아닌 변수들을 제어하는 것을 말합니다.)
  2. private 변수를 만들기 위해서

클로져로 사이드 이펙트 제어하기

함수에서 값을 반환하는 것과는 별도의 무언가를 하는 경우 사이드 이펙트가 발생할 수 있습니다. Ajax 요청, timeout, 심지어 console.log 문까지 많은 것들이 사이드 이펙트를 유발할 수 있습니다.

function (x) {
    console.log('A console.log is a side effect!');  
}

사이드 이펙트를 제어하기 위해 클로져를 사용할 때, 우리가 만든 Ajax나 timeout과 같은 코드를 망칠 수 있는 것을 고려해야 합니다.

배운 것들을 확실히 알기 위해 예제를 통해 배워봅시다.

친구의 생일을 위해 생일 케이크를 만들고 싶다고 가정해봅시다. 이 케이크는 만드는데 1초가 걸립니다. 그래서 우리는 1초 후에 made a cake를 로깅하는 함수를 만들었습니다.

여기서는 더 짧고 이해하기 쉬운 코드를 위해 ES6의 화살표 함수를 사용합니다.

function makeCake() {
    setTimeout(_ => console.log(`Made a cake`), 1000);  
}

우리가 볼 수 있듯, 이 케이크 만드는 함수는 timeout이라는 사이드 이펙트를 가지고 있습니다.

이제 추가로 친구에게 케이크의 맛을 고르게 해봅시다. 그러기 위해, 우리는 makeCake 함수에 맛을 추가할 겁니다.

function makeCake(flavor) {
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);  
}

함수를 실행했을 때, 케이크는 그 즉시 1초 후에 만들어진다는 것을 알아두세요.

makeCake('banana');
// Made a banana cake!

여기서 문제는 우리가 케이크의 맛을 안 뒤에 바로 만들고 싶지는 않다는 것입니다. 시기가 적절할 때 만들고 싶다는 것입니다.

이 문제를 해결하기 위해, 우리는 우리의 맛을 저장할 수 있는 prepareCake 함수를 작성할 수 있습니다. 그 후 prepareCake 내부에 makeCake 클로져를 반환할 수 있습니다.

지금부터 우리가 원할 때마다 반환된 함수를 1초 안에 불러올 수 있습니다.

function prepareCake (flavor) {
  return function () {
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);  
  }
}

const makeCakeLater = prepareCake('banana!');

// and later in your code
makeCakeLater();
// Made a banana cake!

이게 사이드 이펙트를 줄이기 위해 클로져가 사용되는 방법입니다. 이제 당신은 내키는대로 내부 클로져를 사용하는 함수를 만들 수 있습니다.

클로져로 private 변수 만들기

(주: private 변수란, 외부에서 접근할 수 없고 내부에서만 사용되는 변수를 말합니다.)

지금까지 배워 알고 있듯, 함수 내부에서 만들어진 변수는 바깥 변수에서 접근할 수 없습니다. 이러한 이유로 그 변수들은 private 변수라고 불립니다.

하지만, 때때로 우리는 private 변수에 접근할 필요가 있습니다. 우린 클로져를 이용하여 private 변수에 접근할 수 있습니다.

function secret (secretCode) {
  return {
    saySecretCode () {
      console.log(secretCode);  
    }
  }
}

const theSecret = secret('CSS Tricks is amazing');
theSecret.saySecretCode()
// 'CSS Tricks is amazing'

위 예제에서 saySecretCode는 유일하게 기존 secret 함수 밖에서 secretCode를 노출하는 함수입니다. 보통 이러한 경우, 이 함수는 privileged function이라 불립니다.

개발자도구로 스코프 디버깅하기

크롬과 파이어폭스의 개발자도구는 우리가 현재 스코프에서 볼 수 있는 변수를 디버깅하기 더욱 편리하게 만들어줍니다. 이 기능을 사용하기 위해서는 두가지 방식이 있습니다.

첫번째 방법은 자바스크립트 코드 내부에 debugger라는 키워드를 추가하는 것입니다. 이 키워드는 브라우저에서 자바스크립트 실행을 일시정지하고 우리가 디버그 할 수 있게 도와줍니다.

다음은 prepareCake를 이용한 예제입니다.

function prepareCake (flavor) {
  // Adding debugger
  debugger
  return function () {
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);  
  }
}

const makeCakeLater = prepareCake('banana');

우리가 개발자 도구를 열고 소스 탭으로 가면, 우리가 사용 가능한 변수들이 보일 것입니다.

debugger1_zmrmjz.webp

prepare의 스코프 디버깅하기

debugger키워드를 클로져에 추가할 수도 있습니다. 이번엔 스코프 변수들이 어떻게 변하는지 보세요.

function prepareCake (flavor) {
  return function () {
    // Adding debugger
    debugger
    setTimeout(_ => console.log(`Made a ${flavor} cake!`), 1000);
  }
}

const makeCakeLater = prepareCake('banana');

debugger2_fyj91h.webp

클로져 스코프에서 디버깅하기

디버깅 기능을 사용하는 두번째 방법은 코드에 브레이크 포인트를 직접 추가하는 것입니다. sources탭을 클릭하고 line의 번호를 클릭하면 브레이크 포인트 추가가 가능합니다.

breakpoint_twihuy.webp

브레이크 포인트 추가하여 디버깅하기

요약

스코프와 클로져는 엄청나게 이해하기 어려운 것이 아닙니다. 한쪽에서만 보이는 유리를 통해 생각해보면 그들은 간단합니다.

함수 내에서 변수를 선언했을 때, 우리는 함수 내부에서만 그 변수에 접근이 가능합니다. 이 변수들은 함수 스코프를 갖는다고 말할 수 있습니다.

만일 어떠한 함수 안에 내부 함수를 정의한다면, 이 함수는 클로져라고 불릴 수 있습니다. 클로져는 외부 함수로의 접근 권한을 갖습니다.