[면접 회고] 클로저에 대해

이주영·2024년 3월 7일
0

Javascript

목록 보기
11/11
post-thumbnail

서론

이번 주 기술 면접을 보고 왔는데 오랜만에 본 면접이라 그런지,,, 답변을 시원찮게 했다. 우선 작년 취준 이후로 인턴과 DND 프로젝트를 경험하며 여러 지식을 쌓은 것은 확실하지만 다시 본질로 돌아가서 정리해 보아야겠다고 생각이 들었다.

면접관님이 물으셨다.

클로저에 대해 설명해 주시고 핵심적으로 왜 사용하는지 용도를 설명해 주세요.

아무래도 회사 내에서 클로저를 활용해야 하는 부분이 있는 것 같은 느낌을 받았다. 집중적으로 물어봐주셨다.생각해 보고 난 답변했다. "클로저는 독립 변수를 기억하는... 함수...입니다. 리액트 useState에 사용되었던 것으로 알고 있습니다."

조금 더 자세히 설명해 주시겠어요?!

"클로저는 중첩 함수의 생명주기가 끝나도 상위 스코프의 함수에 존재하는 변수를 참조할 수 있게 해주는 방식입니다."라고 했다. 우선 면접관님들은 이 개념에 대해 알고는 있다는 식으로 반응해 주셨지만 스스로 생각해 보면 클로저의 용도를 까먹었다. 아니 몰랐다. 면접이 끝나고 다시 정리해보려고 한다.

본론

클로저의 정의, 목적, 활용 예시와 한계에 대해 정리해보려고 한다.

클로저 정의

클로저는 독립적인 변수를 참조하는 함수이다. 혹은 클로저 안에 선언된 함수는 선언될 때 환경을 기억한다.

function getClousure() {
	var outerVar = 'outerVar';
	return function () {
		return outerVar;
	}
}

var closure = getClosure();
console.log(closure()) // outerVar

결국 자유 변수를 참조하고 있는 익명의 함수를 말하는 것

목적

  1. 변수를 숨긴다.
  • 자바스크립트에서 객체지향 프로그래밍은 Prototype을 통해 객체를 활용해서 구현할 수 있다.
  • 하지만 문제가 발생할 수 있다.
    -> 프로토타입을 활용하여 객체를 만들어나갈 때 어떤 변수는 접근하지 못하도록 하고 싶으나 과거에는 기능이 없었다. 그래서 필요했던 스킬이 클로저를 이용하는 것이었다.

현재 명세를 확인해 보면 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 변수에 접근할 수 없게 된다.

  1. 반복문 변수값
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를 참조하기 때문에 원하는 결과를 얻을 수 있다.

  1. react의 useState?!
    리액트에서 훅은 함수형 컴포넌트안에서 상태와 다른 기능들을 사용할 수 있도록 돕는 함수이다. 훅은 재사용이 가능하고 Stateful한 로직을 작성하는 방법으로써 리액트 16.8 버전에서 소개됐다. 리액트에는 useState와 useEffect 그리고 useContext등 여러 빌트인 훅이 존재한다.

그렇다면 어떻게 구현되어 있는가?!

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();

위의 코드를 하나씩 살펴보자

  1. const React = (function(){...})();: 리액트라는 상수를 선언한다. 그리고 즉시 실행 함수의 결과를 상수에 할당한다. 즉시 실행 함수를 사용하는 이유는 클로저를 만들기 위함이고 useState과 render 함수를 캡슐화하기 위함이다. 외부에서 수정할 수 없도록 말이다.
  2. 즉시 실행 함수를 IIFE라고 부르겠다. IIFE 안을 살펴보면
    1. let _val : 이 라인은 말 그래도 _val 변수를 함수 스코프에 선언한다. 그렇게 하면 컴포넌트 사이에서 공유되는 비공개 변수로 사용할 수 있게 된다.
    2. function useState(initVal){...} : 이거 useState 함수의 구현체이다. 이것은 현재 _val 또는 전달된 initVal로 상태 변수를 초기화합니다. 또한 _val을 제공된 새 값으로 설정하는 setState 함수를 한다. 마지막으로, 상태와 setState 함수를 포함하는 배열을 반환합니다.
    3. 마지막으로 IIFE는 useState과 render 함수를 반환한다.
  3. function Component() {...} : 이건 예시 컴포넌트인데 랜더 함수에 의해 렌더링된 컴포넌트를 말한다. 이 함수는 React 객체에서 상태 및 setState 함수를 가져오는 useState 호출을 정의한다. Component 함수는 render 메서드와 setIndex 메서드를 가진 객체를 반환하는데 render 메서드는 현재 인덱스 값을 콘솔에 기록하며, setIndex 메서드는 인덱스를 1씩 증가시키는 로직이다.
  4. let App = React.render(Component); 이 줄은 Component 함수를 인자로 사용하여 React.render 함수를 호출합니다. 반환된 값을 App 변수에 할당합니다. App 객체는 render 메서드를 가져야 합니다.

위의 코드에서 _val은 클로저로 구현된다. 클로저는 함수와 해당 함수가 선언된 렉시컬 환경의 조합인데 이 경우 useState 함수는 _val 변수에 접근할 수 있다. 그로 인해 함수 외부에 있는 클로저를 참고하기에 useState은 변경된 값을 참조할 수 있게 된다.

useState 함수가 호출될 때마다, useState 함수 외부에 선언된 동일한 _val 변수를 참조합니다. 결과적으로 useState 함수의 여러 호출 간 및 Component의 다른 렌더 간에 상태가 보존될 수 있게 된다.

참고 : https://endeavourmonk.medium.com/usestate-hook-implementation-state-management-in-javascript-769f37a64dc8

3-1. 18버전 이후의 리액트 useState
추가 예정
참고 자료 : https://react.dev/reference/react-dom/render

한계점

  1. 메모리 관리
  • 클로저에 null 값을 할당해서 없애기 전 까지는 클로저는 Private 변수를 참조하고 있어서 메모리가 계속 필요하다.
  • 따라서 사용이 끝난 클로저는 null 값을 할당하여 참조를 제거해야 한다.
  1. 스코프 체인 검색 비용
  • 클로저는 Private 변수에 접근하기 위해 스코프 체인을 따라가야 한다.
  • 이는 시간이 필요한 작업이라 변수에 바로 접근하는 것에 비해 추가적인 시간이 소요된다.

결론

객체 지향 프로그래밍을 자바스크립트를 통해 구현하려고 할때 필요한 기술이라고 생각된다. 클로저라는 것이 아직도 표준 명세에 표기되어있지 않지만 여전히 중요한 스킬인 것 같다고 생각된다.

profile
https://danny-blog.vercel.app/ 문제 해결 과정을 정리하는 블로그입니다.

0개의 댓글