이번 주 기술 면접을 보고 왔는데 오랜만에 본 면접이라 그런지,,, 답변을 시원찮게 했다. 우선 작년 취준 이후로 인턴과 DND 프로젝트를 경험하며 여러 지식을 쌓은 것은 확실하지만 다시 본질로 돌아가서 정리해 보아야겠다고 생각이 들었다.
면접관님이 물으셨다.
클로저에 대해 설명해 주시고 핵심적으로 왜 사용하는지 용도를 설명해 주세요.
아무래도 회사 내에서 클로저를 활용해야 하는 부분이 있는 것 같은 느낌을 받았다. 집중적으로 물어봐주셨다.생각해 보고 난 답변했다. "클로저는 독립 변수를 기억하는... 함수...입니다. 리액트 useState에 사용되었던 것으로 알고 있습니다."
조금 더 자세히 설명해 주시겠어요?!
"클로저는 중첩 함수의 생명주기가 끝나도 상위 스코프의 함수에 존재하는 변수를 참조할 수 있게 해주는 방식입니다."라고 했다. 우선 면접관님들은 이 개념에 대해 알고는 있다는 식으로 반응해 주셨지만 스스로 생각해 보면 클로저의 용도를 까먹었다. 아니 몰랐다. 면접이 끝나고 다시 정리해보려고 한다.
클로저의 정의, 목적, 활용 예시와 한계에 대해 정리해보려고 한다.
클로저는 독립적인 변수를 참조하는 함수이다. 혹은 클로저 안에 선언된 함수는 선언될 때 환경을 기억한다.
function getClousure() {
var outerVar = 'outerVar';
return function () {
return outerVar;
}
}
var closure = getClosure();
console.log(closure()) // outerVar
결국 자유 변수를 참조하고 있는 익명의 함수를 말하는 것
현재 명세를 확인해 보면 Private class fieds라는 명세를 확인할 수 있다.
class ClassWithPrivateField {
#privateField;
constructor() {
this.#privateField = 42;
this.#randomField = 444; // Syntax error
}
}
const instance = new ClassWithPrivateField();
instance.#privateField === 42; // Syntax error
현재는 클래스의 인스턴스의 값에 접근하지 못하도록 #식별자를 사용하여 구현할 수도 있다.
참고 공식 문서 : https://developer.mozilla.org/ko/docs/Web/JavaScript/Reference/Classes/Private_properties
function Person(name) {
this._name = name;
}
Person.prototype.sayHi = function () {
console.log('hi! my name is' + this._name);
}
var brian = new Person('david);
brian.sayHi(); // 'hi my name is brian'
david._name = 'someoneElse'
brian.sayHi(); // 'hi my name is someoneElse'
자바스크립트에서 숨기고 싶은 변수인데 숨길 수 없게 된다. 이 문제를 클로져로 해결할 수 있다.
function sayHi(name) {
var _name = name;
return function() {
console.log('hi, my name is' + _name)
}
}
var brian = sayHi('brain')
sayHi(); // 'hi my name is brian'
brian._name = 'someoneElse'
sayHi(); // 'hi my name is brian'
//
원하는 의도대로 private 변수에 접근할 수 없게 된다.
var i;
for(i = 0; i < 10; i++) {
setTimeout(function() {
console.log(i) // 10이 10번
}, 100)
}
해결 1. IFFE 함수를 활용하여 클로저를 만든다.
var i;
for(i = 0; i < 10; i++) {
(function(j) {
setTimeout(function() {
console.log(j);//0,1,2,3,4,5,6,7,8,9
}, 100)
})(i)
}
위의 코드에서 Private 변수는 J이다.
1. for문 안에 IIFE를 실행한다.
2. 첫 번째 i 값이 0이 Private 변수인 J에 할당되어 J는 0이 된다.
3. 이제 setTimeout 안에서 익명함수는 j 값을 참조한다.
4. clousre인 익명함수는 백그라운드에서 0.1초를 기다린다.
5. 0.1 초후 테스트 큐에 쌓인다.
6. 두 번째부터 10번째까지 모든 IIFE가 위 과정을 반복 실행한다.
7. 모든 실행이 종료되면 콜 스택이 비워진다.
8. 테스크 큐에 쌓인 익명함수가 이벤트 루프에 의해 콜스택에 쌓인다.
9. 이때 익명함수는 Private 변수를 참조해서 실행한다.
해결 2. 블록 스코프를 활용한다.
function func() {
for (let i=0; i<10; i++) {
setTimeout(function() {
console.log(i);
}, i*500);
} }
console.log(func()); // 0,1,2,3,4,5,6,7,8,9
함수 스코프가 아닌 블록 스코프를 갖는 let을 사용하면 for문 내의 스코프를 갖기 때문에 새로운 i가 선언되고 반복이 끝난 이후의 값으로 초기화된다. 따라서 setTimeout()의 클로저인 콜백함수가 i를 참조하기 위해 상위 스코프를 검색할 때 블록 스코프에서 매 반복마다 선언 및 초기화된 i를 참조하기 때문에 원하는 결과를 얻을 수 있다.
그렇다면 어떻게 구현되어 있는가?!
const React = (function () {
let _val;
function useState(initVal) {
const state = _val || initVal;
console.log("useState called");
const setState = (newVal) => (_val = newVal); //setter function
return [state, setState];
}
function render(Component) {
const C = Component();
C.render();
return C;
}
return { useState, render };
})();
function Component() {
const [index, setIndex] = React.useState(0);
return {
render: () => console.log("index: ", index),
setIndex: () => setIndex(index + 1),
};
}
let App = React.render(Component);
App.setIndex();
App = React.render(Component);
App.setIndex();
App = React.render(Component);
App.setIndex();
App = React.render(Component);
App.render();
위의 코드를 하나씩 살펴보자
const React = (function(){...})();
: 리액트라는 상수를 선언한다. 그리고 즉시 실행 함수의 결과를 상수에 할당한다. 즉시 실행 함수를 사용하는 이유는 클로저를 만들기 위함이고 useState과 render 함수를 캡슐화하기 위함이다. 외부에서 수정할 수 없도록 말이다. let _val
: 이 라인은 말 그래도 _val
변수를 함수 스코프에 선언한다. 그렇게 하면 컴포넌트 사이에서 공유되는 비공개 변수로 사용할 수 있게 된다. function useState(initVal){...}
: 이거 useState 함수의 구현체이다. 이것은 현재 _val
또는 전달된 initVal
로 상태 변수를 초기화합니다. 또한 _val
을 제공된 새 값으로 설정하는 setState 함수를 한다. 마지막으로, 상태와 setState 함수를 포함하는 배열을 반환합니다.function Component() {...}
: 이건 예시 컴포넌트인데 랜더 함수에 의해 렌더링된 컴포넌트를 말한다. 이 함수는 React 객체에서 상태 및 setState 함수를 가져오는 useState 호출을 정의한다. Component 함수는 render 메서드와 setIndex 메서드를 가진 객체를 반환하는데 render 메서드는 현재 인덱스 값을 콘솔에 기록하며, setIndex 메서드는 인덱스를 1씩 증가시키는 로직이다.let App = React.render(Component);
이 줄은 Component 함수를 인자로 사용하여 React.render 함수를 호출합니다. 반환된 값을 App 변수에 할당합니다. App 객체는 render 메서드를 가져야 합니다.위의 코드에서 _val
은 클로저로 구현된다. 클로저는 함수와 해당 함수가 선언된 렉시컬 환경의 조합인데 이 경우 useState 함수는 _val
변수에 접근할 수 있다. 그로 인해 함수 외부에 있는 클로저를 참고하기에 useState은 변경된 값을 참조할 수 있게 된다.
useState 함수가 호출될 때마다, useState 함수 외부에 선언된 동일한 _val
변수를 참조합니다. 결과적으로 useState 함수의 여러 호출 간 및 Component의 다른 렌더 간에 상태가 보존될 수 있게 된다.
3-1. 18버전 이후의 리액트 useState
추가 예정
참고 자료 : https://react.dev/reference/react-dom/render
객체 지향 프로그래밍을 자바스크립트를 통해 구현하려고 할때 필요한 기술이라고 생각된다. 클로저라는 것이 아직도 표준 명세에 표기되어있지 않지만 여전히 중요한 스킬인 것 같다고 생각된다.