[번역] 자바스크립트 스코프와 클로저(JavaScript Scope and Closures)

한쪽에서만 보이는 취조실 특수 유리. 스코프의 접근 권한과 비슷한 원리다. (역자)
본 글은 JavaScript Scope and Closures by Zell Liew의 번역 글입니다.

스코프와 클로저는 자바스크립트에서 굉장히 중요합니다. 하지만 제가 처음 자바스크립트를 시작할 때 이 두 개념이 굉장히 헷갈렸어요. 대체 스코프와 클로저가 무엇인지, 여러분이 이해할 수 있도록 도와드리고자 이 글을 준비했습니다.

그럼, 스코프부터 시작해 보도록 하죠.

스코프(Scope)

자바스크립트에서 스코프란 어떤 변수들에 접근할 수 있는지를 정의합니다. 스코프엔 두 가지 종류가 있는데요, 전역 스코프(global scope)와 지역 스코프(local scope)가 있죠.

전역 스코프(Global Scope)

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

이 설명은 웹 브라우저의 자바스크립트에만 유효합니다. 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!'

비록 전역 스코프에 변수를 선언할 수는 있어도, 그러지 않는 것이 좋습니다. 왜냐하면, 두 개 이상의 변수의 이름이 충돌하는 경우가 생길 수도 있기 때문이죠. 만약 변수를 constlet을 사용하여 선언했다면, 이름에 충돌이 발생할 때마다 에러가 발생합니다. 이렇게 되면 안 되죠.

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

만약 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)

여러분 코드의 특정 부분에서만 사용할 수 있는 변수는 지역 스코프에 있다고 할 수 있습니다. 이런 변수들은 지역 변수라고 불리죠.

자바스크립트에서는 두 가지의 지역 변수가 존재합니다. 바로 함수 스코프(function scope)와 블록 스코프(block scope)죠.

먼저 함수 스코프부터 알아보도록 합시다.

함수 스코프(Function Scope)

여러분이 함수 내부에서 변수를 선언하면, 그 변수는 선언한 변수 내부에서만 접근할 수 있습니다. 함수 바깥에서는 해당 변수에 접근할 수 없죠.

아래의 예제를 살펴보면 변수 hellosayHello의 스코프 내에 존재한다는 것을 알 수 있습니다.

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)

여러분이 중괄호({}) 내부에서 const 또는 let으로 변수를 선언하면, 그 변수들은 중괄호 블록 내부에서만 접근할 수 있습니다.

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

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

함수를 선언할 때는 중괄호를 사용해야 하므로 블록 스코프는 함수 스코프의 서브셋(subset) 입니다(여러분이 화살표 함수(arrow function)를 사용해서 암시적(implicit) 반환을 하는게 아니라면 말이죠).

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

함수가 함수 선언식(function declaration)으로 선언되면, 현재 스코프의 최상단으로 호이스팅(hoist) 됩니다.

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

// This is the same as the one below
sayHello()
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
// This is the same as the code above
function sayHello () {
console.log('Hello CSS-Tricks Reader!')
}
sayHello()

반면 함수가 함수 표현식(function expression)으로 선언되면, 함수는 현재 스코프의 최상단으로 호이스팅되지 않습니다.

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

이렇게 두 방식의 행동이 다르기 때문에, 함수 호이스팅은 혼란스러울 수 있으므로 사용하면 안 됩니다. 언제나, 함수를 호출하기 전에 선언해놓아야 합니다.

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

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

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

function first () {
const firstFunctionVariable = 'I’m part of first'
}
function second () {
first()
console.log(firstFunctionVariable) // Error, firstFunctionVariable is not defined
}

네스팅된 스코프(Nested scopes)

함수가 다른 함수 내부에서 정의되었다면, 내부 함수는 외부 함수의 변수에 접근할 수 있습니다. 이런 행동을 렉시컬 스코핑(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
}

스코프가 어떻게 동작하는지 그림을 그려보자면, 취조실 특수 유리(단방향 투과성 유리)를 상상하시면 됩니다. 여러분이 바깥을 바라볼 수는 있지만, 바깥에 있는 사람들은 여러분을 볼 수 없죠.

함수의 스코프는 마지 단방향 투과성 유리와 같다. 당신은 바깥을 볼 수 있지만, 바깥사람들은 당신을 볼 수 없다.

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

여러 레이어로 이루어진 함수는 여러 레이어로 이루어진 단방향 투과성 유리와 같다.

지금까지 스코프에 대한 모든 것을 이해했으니, 이제 여러분은 클로저에 대해 이해하실 준비가 되었습니다.

클로저(Closures)

함수 내부에 함수를 작성할 때마다, 여러분은 클로저를 생성한 것입니다. 내부에 작성된 함수가 바로 클로저죠. 클로저는 차후에 외부 함수의 변수를 사용할 수 있기 때문에 대개 반환하여 사용합니다.

function outerFunction () {
const outer = 'I see the outer variable!'
  function innerFunction() {
console.log(outer)
}
  return innerFunction
}
outerFunction()() // I see the outer variable!

여기에서 내부 함수는 반환되기 때문에, 함수를 선언하자마자 반환되도록 코드를 조금 더 짧게 수정할 수 있습니다.

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 변수 생성하기

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

함수에서 값을 반환할 때를 제외하고 무언가를 행할 때 사이드 이펙트(side effects)가 발생합니다. 여러 가지 것들이 사이드 이펙트가 될 수 있는데요, 예를 들어 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 변수라고 불립니다.

하지만, 해당 변수들에 접근해야 할 필요가 종종 발생합니다. 이것 또한 클로저를 활용해서 할 수 있죠.

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)라고 부르기도 합니다.

개발자 도구(DevTools)를 사용하여 스코프 디버깅 하기

크롬이나 파이어폭스의 개발자도구를 사용하면 현재 스코프에서 접근할 수 있는 변수들을 쉽게 디버깅할 수 있습니다. 두 가지 방법을 사용하여 이 기능을 사용할 수 있죠.

첫 번째 방법은 바로 debugger 키워드를 코드에 추가하는 것입니다. 이렇게 하면 브라우저상에서 자바스크립트의 실행을 일시 정지하여 디버깅할 수 있습니다.

prepareCake의 예제에 적용해봅시다.

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

크롬에서 개발자 도구를 열어 Sources 탭(파이어폭스에서는 Debugger 탭)으로 이동하면 사용할 수 있는 변수들을 확인할 수 있습니다.

preareCake 스코프 디버깅하기

또, debugger키워드를 클로저 내부로 옮길 수 있습니다. 이번에는 스코프의 변수들이 어떻게 변했는지 살펴보세요.

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

클로저 스코프 디버깅하기.

디버깅을 하는 두 번째 방법으로는, 직접 Sources(또는 Debugger) 탭에서 코드 라인을 클릭하여 breakpoint를 추가하는 방식이 있습니다.

breakpoint를 추가하여 스코프 디버깅하기

정리하며

스코프와 클로저는 이해하기에 몹시 어려운 개념들은 아닙니다. 취조실 유리가 동작하는 원리로 생각해본다면 꽤 간단히 이해할 수 있죠.

함수 내부에서 변수를 선언할 때, 그 변수는 함수 내부에서만 접근할 수 있습니다. 이런 변수들은 함수의 스코프 위에 존재한다고 말합니다.

함수 내부에서 함수를 선언할 때, 내부의 함수는 클로저라고 불립니다. 이 클로저는 외부 함수에서 생성된 변수들에 접근할 수 있는 권한을 가지고 있습니다.

궁금증이 생기셨다면 어떤 질문이든지 남겨주세요. 제가 최대한 빨리 답해보도록 하겠습니다.

이 글이 마음에 드셨다면, 제가 블로그뉴스레터에 쓴 다른 프론트엔드 관련된 글도 마음에 드실 거에요. 그리고 제가 새로 만든 (무료에요!) 이메일 수업(JavaScript Roadmap)도 있습니다.

One clap, two clap, three clap, forty?

By clapping more or less, you can signal to us which stories really stand out.