TIL 231108 - Closure 부수기

송용승·2023년 11월 7일
1

TIL

목록 보기
11/29
post-thumbnail

Today I Learned

이번 포스팅은 지난번 포스팅과 이어진다면 이어질 수도 있는 내용이다. 어느덧 내일배움캠프는 6주차에 접어들었고, 옷을 여미게 되는 날씨가 되었다. 밖에 안 나가지 않느냐고? 맞다... 아무튼... 시간의 흐름을 실감한다는 이야기다...

털어내고 가자

그렇다면 6주가 지나는 동안 나는 무엇을 했고 얼마나 성장했는가. 어떤 걸 할 수 있고 어떤게 아직 어려운가... 생각을 해보면 실제로 어려운 것과 꺼려지는 것이 나뉜다. 실제 어려운 것의 대부분은 아직은 공부를 해도 온전히 흡수할 수 없는, 투자 대비 효용이 떨어지는 것들이다. Babel 이라던가 Webpack, bundling, code-splitting, .... 그렇다면 반대로 꺼려지는 것은 무엇일까?

  1. 알긴 아는데 정확히는 모르는 것
  2. 프로젝트나 과제를 진행할때 이것저것 참고해서 어떻게 써보긴 했지만 온전히 내가 짠 로직이 아닌 경우
  3. 악명높은 경우

JavaScript 와 지금까지 배운 React 내용중에서 위의 세가지에 해당되는 건 뭐가 있을까?

Closure, async/await, useState (이 친구는 최근 이 목록에서 탈출했다. 조만간 다룰 예정)

역시 이들이다. 얘들은 머릿속에서 떠날 생각을 않는다. 그래서 하나씩 부숴보기로 했다. 일단 클로저부터.

Closure

클로저의 사전적 의미는 폐쇄, 닫힘으로, 이 패턴을 사용함으로써 얻을 수 있는 효과를 잘 보여준다. 클로저는 어떤 별도의 기능이라기보다 렉시컬 스코프와 함수를 조합해 만들 수 있는 패턴인데, 이 패턴은 데이터를 안전하게 은닉하거나 캡슐화 하는데에 많이 쓰인다고 한다. 클로저를 이해하고 사용하기 위해선 실행 컨텍스트와 콜 스택, 렉시컬 환경에 대한 이해가 선행되어야 하는데, 백 날 줄글만 봐선 알 수 없으니 코드를 작성해보자.

하나씩 따져보자

위의 코드는 클로저를 사용해보고자 작성한 코드의 일부인데, 이를 보고 알 수 있는 사실은 다음과 같다.

  1. closureTestinnerFunction 을 반환하는 함수로, 스코프 안에 변수 count 를 갖는다.
  2. innerFunction 함수는 count 를 1만큼 증가시킨 후 반환한다. 그러나 count 에 대한 정보를 가지고 있지는 않다.
  3. countAccumulatorclosureTest 함수가 실행된 후 반환한 innerFunction 을 그대로 담는다.

이제 여기에 자바스크립트 엔진의 변수 환경, 렉시컬 환경, 실행 컨텍스트 그리고 콜 스택의 맥락을 얹어서 다시 보자.

  1. innerFunction 이 클로저에 해당한다.
  2. innerFunctioncountAccumulator 에 할당한 이후 closureTest 는 종료되고 이 함수의 실행 콘텍스트는 콜 스택에서 pop 되어 제거된다.
  3. 하지만 closureTest 에서 선언한 변수 count 는 그대로 남아있다. innerFunction 함수에서 참조하고 있기 때문이다.

여기서 innerFunctioncount 를 어떻게 참조할 수 있는걸까? 앞서 말한 JS 엔진의 맥락에서innerFunction 을 다시 살펴보자.

  1. 이 함수는 count 를 1만큼 증가시켜야하는데, 함수 내부에 변수가 없음을 확인하고 Outer lexical environment 를 확인한다.
  2. 이 과정에서 외부의 함수 closureTest 의 렉시컬 스코프에 접근하고 count 를 찾아내어 참조하는 것이다.
  3. 이 과정이 countAccumulator 함수가 호출될때마다 반복된다.

이제 전역에서 count 의 값을 알 방법은 countAccumulator 를 호출한 결과값을 확인하는 방법밖에는 없다. 완전히 은닉된 변수가 된 것이다.

눈으로 보자

이제 위 코드가 포함되어있는 전체 코드로 구현한 간단한 웹페이지를 보자. 이 웹페이지의 JS로 작성된 부분 중 중요한 부분만 추려서 보면 다음과 같다.

const countBtn = document.querySelector("#counter");
const checkBtn = document.querySelector("#check");

const closureTest = () => {
	let count = 0;

	const innerFunction = (e) => {
		return count++;
	};

	return innerFunction;
};

const countAccumulator = closureTest();

const showCount = () => {
	const target = document.querySelector("#display");
	const countValue = countAccumulator();
	target.innerHTML = `You clicked the button ${countValue} times`;
};
countBtn.addEventListener("click", countAccumulator);
checkBtn.addEventListener("click", showCount);

counter 라는 id를 가진 버튼에 countAccumulator 를 이벤트리스너의 콜백 함수로 등록했다. 또한 count의 값을 확인하기 위해 check 라는 아이디를 가진 버튼에 showCount 라는 콜백 함수를 이벤트리스너에 등록했다. 이 때 showCount 가 출력을 위해 count 값을 가져오는 부분을 보면

target.innerHTML = `You clicked the button ${countAccumulator()} times`;

위와 같은 코드로 target인 display 라는 아이디를 가진 HTML 요소의 innerHTMLcount 의 값을 출력하는데, 이 때에도 countAccumulator 함수를 호출해야 한다. 물론 이 때에도 count 값의 증가는 일어나지만 횟수를 잘 보여주기 위해 부득이하게 count 변수에 increment 연산자를 postfix로 붙였기 때문에 버튼을 누른 횟수만큼 보이는 것이다.

gif 이미지로 보면 아래와 같다.

count 옆의 버튼을 누르기 전까진 값이 보이지 않는 것을 확인할 수 있다.

MDN - Closures

profile
웹 프론트엔드 개발을 익히고 있습니다.

0개의 댓글