TIL 1 | 클로저 이해하기

grighth12·2021년 8월 3일
16

TIL

목록 보기
1/15

목차


클로저를 이해하기 위한 배경 지식

렉시컬 스코프

함수가 호출되는 위치가 아닌, 함수가 선언(정의)되는 위치에 의해 함수의 상위 스코프를 정적으로 결정하는 것이다.

  • 예제 코드 1
const x = 1;

function outerfunc(){
  const x = 10;
  innerFunc();
}

function innerFunc(){
  console.log(x);
}

outerFunc();
  • 실행 결과 1
    1

  1. 렉시컬 스코프(함수의 상위 스코프)는 outerFunc, innerFunc이 동일하게 전역이다. 둘의 정의 위치가 같은 것을 코드 상에서 확인할 수 있다.
  2. 따라서, innerFuncouterFunc의 스코프에 존재하는 x(10)에 접근할 수 없고 전역에 존재하는 변수인 x(1)를 참조하게 된다.

- 예제 코드 2
const x = 1;

function outerfunc() {
  const x = 10;
  
  function innerFunc() {
    console.log(x);
  }
  
  innerFunc();
}

outerFunc();
  • 실행 결과 2
    10

  1. outerFunc의 정의 위치는 예제 코드 1과 다름이 없으므로 outerFunc의 렉시컬 스코프는 전역이다. innerFunc의 정의 위치는 outerFunc의 내부이므로, innerFunc의 렉시컬 스코프는 outerFunc(의 렉시컬 환경)이다.
  2. 따라서, innerFunc은 자신의 상위 스코프인 outerFuncx(10)을 참조하게 된다.

클로저 정의

  • 함수가 선언된 환경의 스코프를 기억하여 함수가 스코프 밖에서 실행될 때에도 기억한 스코프에 접근할 수 있게 만드는 문법이다.
  • 중첩 함수가 외부 함수의 식별자를 참조하고 있으며, 중첩 함수가 외부 함수보다 더 오래 유지될 때 이 중첩 함수를 클로저라고 한다.
    • 배경지식에서 살펴보았던 예제 코드 2는 일반적으로 클로저라고 부르지 않는다.
    • 중첩 함수(innerFunc)가 외부 함수(outerFunc)의 식별자를 참조하고는 있으나, 외부 함수보다 중첩 함수가 더 생명 주기가 짧기 때문이다.

예제로 이해하는 클로저

  • 예제 1
const x = 1;

function outer() {
  const x = 10;
  const inner = function() { console.log(x); }
  return inner;
}

const innerFunc = outer();
innerFunc();
  1. outer함수는 outer를 호출하면 중첩함수 inner 을 반환하고 생명주기를 마감한다.
  2. inner함수가 outer 함수(의 렉시컬 환경)를 상위 스코프로 저장하고 있으므로,
    outer 함수의 렉시컬 환경에 대한 참조가 끊어지지 않고,
    outer 함수의 렉시컬 환경이 Garbage Collection의 대상이 되지 않으므로
    outer함수의 렉시컬 환경이 유지된다.
  3. inner 함수는 outer 함수 스코프의 지역 변수 x를 참조할 수 있게 된다.
  4. 따라서 innerFunc의 실행결과는 10이 된다.

조금 더 복잡한 예제를 살펴 보자. 클로저 관련 스레드에 멘토님이 올려주신 예제들이다.😎

  • 예제 2
function counting1() {
	let i = 0;
	for(i = 0; i < 5; i++) {
		setTimeout(function() {
			console.log(i);
		}, i * 100);
	}
}

counting1();
  1. setTimeout이 참조하고 있는 icounting1 스코프의 i이다.
  2. setTimeout의 콜백함수가 실행될 때 참조하는 i의 값은 이미 5가 되어있다.
  3. 실행 결과로 5가 다섯 번 찍히게 된다.
  • 예제 3
function counting2() {
	let i = 0;
	for(let i = 0; i < 5; i++) {
		setTimeout(function() {
			console.log(i);
		}, i * 100);
	}
}

counting2();
  1. setTimeout이 참조하고 있는 icounting2 스코프의 i가 아닌, for loop 스코프의 i이다.
  2. setTimeout의 콜백함수가 실행될 때 각각의 for loop 블록 스코프를 참조하도록 클로저가 생긴다.(i=0부터 i=4까지 각각의 setTimeout의 콜백함수가 참조하고 있다.)
  3. 실행 결과로 0 1 2 3 4가 순차적으로 찍힌다.

클로저의 활용

  • 상태를 안전하게 은닉하고 특정 함수에게만 상태 변경을 허용할 수 있다.
  • 즉, 은닉화 할(내부 변수와 함수를 감출) 수 있다.

함수 호출 횟수를 카운팅하는 함수를 다음과 같이 구현했다고 생각해보자.

let num = 0;

const increase = function() {
  return ++num;
}

console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

주석으로 달아놓은 결과와 같이, 코드는 잘 동작한다. 하지만 오류를 발생시킬 가능성이 큰 코드이다. 카운팅을 기록하는 변수 num이 전역 변수로 관리 되어 누구나 접근 가능하고 변경 가능하기 때문이다. increase 함수를 호출 할 때만 num을 증가시킬 수 있어야 바람직하다.

클로저를 이용하면 다음과 같이 구현할 수 있다.

const increase = (function () {
  let num = 0;
  return function () {
    return ++num;
  }
}());
console.log(increase()); // 1
console.log(increase()); // 2
console.log(increase()); // 3

이렇게 되면 IIFE의 변수 num은 오직 increase만 접근할 수 있게 된다. IIFE는 오직 한 번 실행되므로 numincrease 호출 시마다 0으로 초기화 한다고 오해해서는 안된다.

increase 의 렉시컬 스코프는 자신이 정의된 위치인 IIFE이며, IIFE(렉시컬 환경)의 num을 참조하고 있다.


마치며

생각보다 클로저를 이해하기 위해 학습해야 할 것들이 많았지만, 얕은 수준에서라도 이해하기 위해 클로저를 주제로 TIL을 작성해 보았다.
추후에 실행 컨텍스트, 비동기 처리를 어떻게 하는지를 학습하게 된다면 좀더 깊은 수준의 이해를 할 수 있을 것 같다.
콜 스택, Web API, Task Queue와 함께 비동기 처리 이해라던가, 실행 컨텍스트 같은 배경 지식을 좀 더 흡수하고 나면 더 명확한 표현으로 글을 쓸 수 있을 것 같다.
마침 이번 주 스터디 주제를 실행 컨텍스트로 잡았으니, 학습 후에 좀 더 분명한 용어로 이 글을 수정해보려고 한다!
아하 모먼트가 곧 찾아올 것 같다😀


참고 자료 및 강의

모던 자바스크립트 딥다이브 24장 클로저
프로그래머스 프론트엔드 데브코스 Day1 [강의] 스코프와 클로저
profile
데굴데굴 굴러가고 있습니다

3개의 댓글

comment-user-thumbnail
2021년 8월 11일

명쾌한 설명 감사합니다

1개의 답글
comment-user-thumbnail
2022년 8월 7일

읽어본 클로저 글들 중 가장 이해하기 쉽네요. 감사합니다.

답글 달기