2주차 발표자료 전형진 - TECH-SHARING-STUDY/FE_STUDY GitHub Wiki
A closure is the combination of a function and the lexical environment within which that function was declared. - MDN
MDN에서 클로저를 설명하는 문장입니다. 저 문장에서 핵심 키워드는 function과 lexical environment 입니다. function은 당연히 함수를 의미하는 것이고, 문제는 lexical environment라는 단어입니다. 이 단어가 무엇인지 알아야 클로저가 어떻게 동작하는지 이해할 수 있습니다.
Lexical environment을 설명하기 앞서, 실행 컨텍스트에 대해 짤막하게 설명하겠습니다. 왜냐하면 실행 컨텍스트 안에 lexical environment가 포함되기 때문입니다.
실행할 코드에게 제공할 환경 정보들을 모아 놓은 객체를 말합니다. 여기서 말하는 환경 정보란 다음과 같습니다.
- variable environment
- 현재 컨텍스트 내의 식별자들에 대한 정보 + 외부 환경 정보
- 위 정보에 대한 초기 스냅샷
- lexical environment <== 우리가 관심 가져야 할 부분
- 초기에는 variable environment와 같지만, 변경 사항이 실시간으로 반영된다.
- this binding
- this 식별자가 참조하고 있는 대상 객체
실행 컨텍스트가 활성화(생성)될 때 위와 같은 정보를 수집하여 객체로 만들고, 실행 컨텍스트와 관련된 코드를 실행합니다. 이 때 실행 컨텍스트를 활성화하는 방법은 자바스크립트에서 크게 3가지로 볼 수 있습니다. (ES 5 기준)
- 전역 공간 진입
- eval 함수 호출
- 함수 호출
거의 대부분 함수 호출을 통해 실행 컨텍스트를 생성하기 때문에, 함수 관점으로 앞으로의 개념을 설명하겠습니다.
lexical environment은 실행 컨텍스트(execution context) 객체 안에 있는 정보로, 내부에는 다음과 같은 2가지 정보를 가지고 있습니다.
- environment record
- 현재 호출된 함수와 관련된 코드의 식별자 정보들을 가지고 있습니다.
- 즉, 함수 내부에 선언된 변수, 선언된 함수, 매개변수 등의 정보를 가집니다.
- outer environment reference
- 현재 호출된 함수가 선언된 컨텍스트의 lexical environment에 대한 참조를 가집니다.
- 즉, 해당 함수가 선언된 시점의 lexical environment를 참조합니다.
여기서 우리가 집중해야 하는 정보가 바로 outer environment reference 입니다. MDN 정의에서 the lexical environment within which that function was declared.
을 직역하자면, 함수가 선언된 시점의 lexical environment
입니다. 사실 이것이 바로 outer environment reference를 말하기 때문입니다.
function foo() {
var a = 1;
var b = 2;
function bar() {
console.log(a);
}
return bar;
}
var bar = foo();
bar(); // 1 출력
위 예제를 보면 bar 함수가 선언된 곳은 foo 함수 내부입니다. 즉, bar함수의 outer environment reference는 foo 함수의 lexical environment를 참조하고 있습니다. 그렇기 때문에 bar 함수에서 a라는 식별자가 없음에도, outer environment reference를 통해 foo의 a 변수에 접근할 수 있습니다.
눈치 챘을 수도 있지만, 방금 a를 찾아가는 방식이 바로 스코프 체인입니다. lexical environment의 outer environment reference 덕분에 스코프 체인이 가능한 것입니다. 각 함수의 outer environment reference는 자신이 선언된 시점의 lexical environment만 참조하고 있기 때문에 가장 가까운 요소부터 차례대로 접근할 수 있고, 다른 순서대로는 접근할 수 없습니다. 흔히 스코프 체인이 연결 리스트 형태를 띈다고 말하는 이유가 바로 이 점때문입니다.
여담으로 이런 구조적 특성 덕분에 여러 스코프에 걸쳐 동일한 식별자가 있는 경우, 스코프 체인 상 가장 먼저 발견된 식별자에 접근하게 되는데, 이를 변수 은닉화 혹은 Shadowing라 합니다.
드디어 클로저를 설명할 차례입니다. 다시 정의를 살펴봅시다.
A closure is the combination of a function and the lexical environment within which that function was declared. - MDN
핵심 키워드인 function과 lexical environment에 대해 알았으니, 이를 combination 해봅시다. 어떤 함수가 자신이 선언된 당시의 lexical environment를 활용해 무엇을 할 수 있는지 알게 되면 클로저가 무엇인지 알 수 있습니다. 사실 할 수 있는 것이라고 해봤자 lexical environment를 통해 외부에 선언된 변수를 접근하는 것 밖에 없습니다. 하지만 내부적인 동작을 살펴보면, 단순히 접근하는 것이 전부가 아닙니다.
// 위와 같은 예제입니다.
function foo() {
var a = 1;
var b = 2;
function bar() {
console.log(a);
}
return bar;
}
var bar = foo();
bar(); // 1 출력
보통 함수 내부의 변수들은 함수 스코프를 벗어나면 자연스레 가비지 컬렉션의 대상이 됩니다. 원래 함수 foo의 호출이 다 끝나면, 변수 a는 사라져야 정상입니다. 하지만 변수 a는 사라지지 않습니다. 아예 가비지 컬렉션의 대상에서 제외됩니다. 그 이유는 내부에 선언된 함수 bar가 outer environment reference를 통해 함수 foo의 lexical environment를 참조하여 변수 a에 접근하고 있기 때문입니다. 이것이 바로 클로저의 성질입니다.
즉, 클로저는 어떤 함수가 자신이 선언된 당시에 알 수 있었던 변수를 가비지 컬렉팅되지 않도록 만들어, 나중에 실행되었을 때 해당 변수에 접근할 수 있는 함수(혹은 현상)라 볼 수 있습니다. 위 예제에 대입해보면, 클로저는 함수 bar가 될 것이고, 외부 환경의 변수인 a가 사라지지 않도록 유지시켜 나중에 자신이 호출될 때 접근할 수 있도록 합니다.
커링 함수는 여러 개의 인자를 받는 함수를 하나의 인자를 받는 함수로 쪼개서 순차적으로 실행시키는 형태의 함수를 말한다. 미리 전달 받은 인자들은 클로저로 인해 기억되고, 마지막 인자를 받아야 비로소 함수가 평가됩니다.
// 두 개의 인자를 받은 함수 fn을 커링 함수로
function curry(fn) {
return function(a) {
return function(b) {
// 두 번째 인자를 받을 때까지 평가를 미루어진다.
// 클로저로 인해 미리 받아둔 fn과 a를 기억할 수 있다.
return fn(a,b);
}
}
}
// 화살표 함수 버전
// const curry = fn => a => b => fn(a, b);
const add = curry((a, b) => a + b);
const add3 = add(3);
console.log(add3(2)); // 5
console.log(add3(7)); // 10
console.log(add(3)(7)); // 10
리덕스 미들웨어는 리듀서가 액션을 처리하기 전,후로 특정 로직을 실행시킬 수 있는 함수로, 다중 커링 형태의 함수이다. 결국 리덕스 미들웨어도 클로저입니다.
const middleware = store => nextRunner => action => newRunner(action);
// or
function middleware(store) {
return function(nextRunner) {
return function(action) {
// 리듀서 실행되기 전 로직
const result = nextRunner(action);
// 리듀서 실행된 후 로직
return result;
}
}
}
하이어오더 컴포넌트는 기존 컴포넌트에 새로운 기능이나 자주 사용되는 기능들을 기존 컴포넌트의 코드 변경없이 추가할 때 사용하며, 새로운 기능이 덧입혀진 컴포넌트를 반환하는 함수입니다.
function withLogging(Component) {
return function (props) {
console.log('Component render!');
return <Component {...props} />
}
}
const Component = () => {
return <div>...</div>
}
export default withLogging(Component);