이번에야말로 클로저를 정복하겠다는 마음으로, 공부한 것을 바탕으로 글을 작성한다.
각각의 H1 제목은 면접 질문으로 나올 법한 것들로 정하였고, 꼬리 질문으로 라이브 코딩 대비 질문들도 첨가했다.
혹시나 이 글을 보고 클로저를 공부하고자하는 사람들은 이해하기 쉽도록 전개하였으니, 위에서부터 차례대로 읽는 것을 추천한다!
정의를 논하기 전에 아래와 같은 코드를 예시로 들고 왔다.
다음의 실행 결과를 한 번 예측해보자
const outerFunction = () => {
const a = 10;
return function(b) {
console.log(a + b)
}
}
const getValue = outerFunction()
getValue(20)
여기서
답이 나오지 않고 에러가 뜬다고 생각한 사람은 함수의 실행과 return에 대해서 한 번 더 살펴보길 권하고,
20이라고 생각한 사람은 좋은 추론이지만 아직 클로저를 모르고 있어서 이 글을 계속해서 읽는 것을 권하고,
마지막으로 30이라고 생각한 사람은 클로저를 아는 사람이고,
어떻게 30이 나왔는지 설명할 수 있다면, 더 이상 정의 부분을 읽지 않고, 관련 문제만 쏙쏙 읽으면 된다!
그래서 클로저가 뭔데? 라고 궁금증을 느끼고 구글링을 해보면 한 가지 정해진 답이 나오지 않는다.
그래서 내가 찾은 클로저의 정의를 한 곳에 모아 보았다.
A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
클로저란 주변 상태(렉시컬 환경)을 참조하는 묶인 함수들 간의 조합을 의미합니다.
다른 말로는, 클로저는 내부 함수에서 상위 함수의 스코프에 접근할 수 있도록 합니다.
자바스크립트에서는 클로저는 함수 생성 시점마다 생성됩니다.
(출처: Closures)
음... 조합이면 구체적으로 어떤 개념인 건지 아직 잡히지 않는다...
함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우를 보통 클로저라고 부릅니다.
스코프를 함수 주변으로 좁히는 것이라고 생각해도 됩니다.
(출처: 러닝 자바스크립트 Learning JavaScript 196쪽)
의도적으로 정의하는 경우?
함수와 중첩 함수의 관계
(출처: {풀스택} JavaScript 11강 - 한번에 정리하는 클로저)
함수와 중첩 함수의 관계! 심플하니 마음에 드는 정의이다.
외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참고할 수 있다. 이러한 중첩 함수를 클로저라고 한다.
(출처: 모던 자바스크립트 Deep Dive 393쪽)
오... 생명 주기와 중첩 함수가 중요한 키워드같은데, 함수의 생명 주기는 도대체 뭐지...?
어떤 곳에서는 함수가 곧 클로저이고, 어떤 곳에서는 함수의 기능 또는 상황 또는 관계라고 하니, 헷갈릴 법하다!
그렇다면 위의 정의들을 바탕으로 정리한 정의는 다음과 같다.
그 이유는 정의를 내리고 이어서 설명하겠다.
자바스크립트에서 함수의 상위 스코프를 참조할 수 있는 작동원리때문에 발생하는,
중첩 함수가 외부 함수의 변수 및 함수에 접근할 수 있는 기능이자 그러한 구조를 갖고 있는 중첩 함수
이 정의를 이해하려면 우선 다음과 같은 개념을 먼저 파악하는 것이 중요하다.
확실히 이해하려면 더 깊숙이 들어갈 수 있지만, 지금은 클로저가 핵심이니 깊이 들어가진 않을 것이다. 사담으로 실행컨텍스트는 작성 중에 있는데, 글 분량 조절이 안돼서 아직 못 올리고 있다. 😅
이에 대한 이해가 없다면 우선은 간단하게 다음과 같이 이해하면 된다.
let/const와 var 식별자 키워드의 차이
에 대해 공부하면 좋을 것 같다.(역시 CS는 파면 팔수록 공부할 게 나온다... 심심할 여력이 없다.)
아무튼 다시 클로저로 돌아오자.
자바스크립트의 실행컨텍스트의 구성 중에서 렉시컬 환경(lexical environment)이 있다.
렉시컬 환경은 식별자(변수와 함수)와 식별자에 연결된 값 그리고 상위 스코프에 대한 참조를 기록하는 자료구조이다.
이 중에서 클로저와 가장 연관이 있는 부분은 바로,
렉시컬 환경에서 상위 스코프에 대한 참조를 기록하는 곳인 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)
이다.
이게 뭔 말이야... 싶은 사람이 분명이 있을 것이다.
처음 예시 코드를 보면서 풀어쓰자면 다음과 같다.
const outerFunction = () => {
const a = 10;
return function(b) {
console.log(a + b)
}
}
const getValue = outerFunction()
getValue(20)
outerFunction()
이 실행될 때, outerFunction 실행컨텍스트가 생성된다.a
가 기록된다.getValue
가 기록된다.getValue
에 outerFunction의 return 값인 function(b) {console.log(a+b)}
(function(b))가 할당된다.getValue(20)
가 실행될 때, function(b) 의 실행컨텍스트가 생성된다.b
가 기록된다.다시 한 번 더 정리하자면,
자바스크립트의 실행컨텍스트에서 코드가 평가되고 실행될 때 관련된 정보, 즉 변수와 함수, 상위 스코프를 기록하는데 이 중에서 외부 렉시컬 환경에 대한 참조에 의해 outerFuntion
은 전역 스코프를, function(b)
는 outerFunction 스코프를 가리킨다.
따라서 function(b)는 a에 대한 변수를 함수 스코프 내에서 선언하지 않았음에도 상위 스코프에 선언된 a를 가져다가 쓸 수 있는 것이다.
클로저는 이러한 결과를 통틀어서 말하는 것이고, 자바스크립트의 작동원리에 의하면 모든 실행컨텍스트는 외부 렉시컬 환경에 대한 참조를 가지고 있기 때문에 모든 함수가 곧 클로저가 될 수 있다. 하지만 외부 스코프를 참조하고 있지 않는 경우의 함수는 일반적으로 클로저라고 이야기하지 않는다.
그래서 앞서 말했던 정의는 위의 원리에 의해 다 동일한 것을 말하고 있다.
함수 내부의 중첩 함수의 외부 렉시컬 환경에 대한 참조는 곧 외부 함수 스코프를 가리키니, 이러한 함수와 중첩 함수의 (식별자를 서로 공유하고 참조하는) 관계라고 이야기할 수 있다.
또한 의도적으로 외부 스코프의 어떤 식별자를 가져오기 위해 정의할 수 있는데, 이렇게 할 수 있는 이유도 곧 앞서 말했던 외부 렉시컬 환경에 대한 참조때문이다.
그 중에서 생명 주기와 관련된 정의는 현재 글에서 완전히 이해하기 어려우니, 추후 실행컨텍스트에 대한 글에서 마저 이어나가도록 하겠다.
그래서 한 마디만 더 얹어서 정의를 마무리하자면 다음과 같다.
외부 렉시컬 환경에 대한 참조에 의해
자바스크립트에서 함수의 상위 스코프를 참조할 수 있는 작동원리때문에 발생하는,
중첩 함수가 외부 함수의 변수 및 함수에 접근할 수 있는 기능이자 그러한 구조를 갖고 있는 중첩 함수
클로저는 같은 스코프를 가지는 함수들 끼리 어떤 식별자를 공유하고 있는지에 따라 특정 함수의 스코프에 무엇이 있고, 무엇이 없는지 관리하기 때문에 중요하다.
더 나아가서 변수와 함수가 서로 어떻게 관계되어 있는지 이해하는 것은 함수형 프로그래밍 그리고 객체 지향 프로그래밍 스타일의 코드에 어떤 일이 있어나는지 파악하는 것과 이어져 있어서 중요하다.
함수형 프로그래밍 === 클로저가 아니다!
함수형 프로그래밍에서 클로저를 쓸 수 있다는 이야기이다.
❗ 변수 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다. 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
(출처: 모던 자바스크립트 Deep Dive 405쪽)
const makeCalculator = /** 구현하세요 */
function increase(a, b){
return a + b
}
function multiply(a, b){
return a * b
}
function decrease(a, b){
return a - b
}
function divide(a, b){
return a / b
}
console.log(makeCalculator(increase, 2)) // 2
console.log(makeCalculator(multiply, 6)) // 12
console.log(makeCalculator(divide, 4)) // 3
console.log(makeCalculator(decrease, 7)) // -4
const makeCalculator = (function() {
let number = 0;
return function (operator, n){
number = operator(number, n)
return number
}
}());
❗ 캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.
(출처: 모던 자바스크립트 Deep Dive 409쪽)
const counter = /** 구현하세요 */
let c = counter();
c.add(5);
c.add(9);
c.getValue(); // 14
const counter = () => {
let value = 0
return {
add: (n) => value += n,
getValue: () => {console.log(value)}
}
}
(function(a){
return (function(){
console.log(a);
a = 23;
})()
})(45);
45
multiply(2, 4) // 8
multiply(3, 5) // 15
const double = multiply(2)
double(2) // 2
double(8) // 16
const hexa = multiply(6)
hexa(6) // 36
hexa(10) // 60
const multiply=(x1, x2)=> {
if (x2){
return x1 * x2
}
return (n) => {
return x1 * n
}
}
누군가는 이렇게까지 알아야 하나
라고 생각할 수도 있다.
물론 그건 개인의 선택이다.
하지만 언어의 작동원리를 파악하면, 여러가지를 설명할 수 있고,
클로저 뿐만 아니라 다른 효과들(호이스팅, this 바인딩 등)도 별개의 개념이 아니라 다 같은 작동원리에 의해 발생하는 것을 이해할 수 있을 것이다.
위 라이브 코딩 문제에 대한 해설 또한 별도로 글을 작성하여 첨부할 예정이니 먼저 원하는 문제가 있다면 댓글 부탁합니다!
그리고 이제는 면접에서 클로저 질문 나왔을 때 당황하지 말고 대답할 자신 있다!