기술면접 대비: 클로저 Closure

Chaejung·2023년 6월 9일
1
post-thumbnail

이번에야말로 클로저를 정복하겠다는 마음으로, 공부한 것을 바탕으로 글을 작성한다.
각각의 H1 제목은 면접 질문으로 나올 법한 것들로 정하였고, 꼬리 질문으로 라이브 코딩 대비 질문들도 첨가했다.
혹시나 이 글을 보고 클로저를 공부하고자하는 사람들은 이해하기 쉽도록 전개하였으니, 위에서부터 차례대로 읽는 것을 추천한다!

클로저란?

정의를 논하기 전에 아래와 같은 코드를 예시로 들고 왔다.
다음의 실행 결과를 한 번 예측해보자

const outerFunction = () => {
	const a = 10;
	return function(b) {
		console.log(a + b)
	}
}

const getValue = outerFunction()
getValue(20)

여기서
답이 나오지 않고 에러가 뜬다고 생각한 사람은 함수의 실행과 return에 대해서 한 번 더 살펴보길 권하고,
20이라고 생각한 사람은 좋은 추론이지만 아직 클로저를 모르고 있어서 이 글을 계속해서 읽는 것을 권하고,

마지막으로 30이라고 생각한 사람은 클로저를 아는 사람이고,
어떻게 30이 나왔는지 설명할 수 있다면, 더 이상 정의 부분을 읽지 않고, 관련 문제만 쏙쏙 읽으면 된다!


그래서 클로저가 뭔데? 라고 궁금증을 느끼고 구글링을 해보면 한 가지 정해진 답이 나오지 않는다.

그래서 내가 찾은 클로저의 정의를 한 곳에 모아 보았다.

A closure is the combination of a function bundled together (enclosed) with references to its surrounding state (the lexical environment). In other words, a closure gives you access to an outer function's scope from an inner function. In JavaScript, closures are created every time a function is created, at function creation time.
클로저란 주변 상태(렉시컬 환경)을 참조하는 묶인 함수들 간의 조합을 의미합니다.
다른 말로는, 클로저는 내부 함수에서 상위 함수의 스코프에 접근할 수 있도록 합니다.
자바스크립트에서는 클로저는 함수 생성 시점마다 생성됩니다.
(출처: Closures)

음... 조합이면 구체적으로 어떤 개념인 건지 아직 잡히지 않는다...

함수가 특정 스코프에 접근할 수 있도록 의도적으로 그 스코프에서 정의하는 경우를 보통 클로저라고 부릅니다.
스코프를 함수 주변으로 좁히는 것이라고 생각해도 됩니다.
(출처: 러닝 자바스크립트 Learning JavaScript 196쪽)

의도적으로 정의하는 경우?

함수와 중첩 함수의 관계
(출처: {풀스택} JavaScript 11강 - 한번에 정리하는 클로저)

함수와 중첩 함수의 관계! 심플하니 마음에 드는 정의이다.

외부 함수보다 중첩 함수가 더 오래 유지되는 경우 중첩 함수는 이미 생명 주기가 종료된 외부 함수의 변수를 참고할 수 있다. 이러한 중첩 함수를 클로저라고 한다.
(출처: 모던 자바스크립트 Deep Dive 393쪽)

오... 생명 주기와 중첩 함수가 중요한 키워드같은데, 함수의 생명 주기는 도대체 뭐지...?

어떤 곳에서는 함수가 곧 클로저이고, 어떤 곳에서는 함수의 기능 또는 상황 또는 관계라고 하니, 헷갈릴 법하다!

클로저에 대한 이해

그렇다면 위의 정의들을 바탕으로 정리한 정의는 다음과 같다.
그 이유는 정의를 내리고 이어서 설명하겠다.

자바스크립트에서 함수의 상위 스코프를 참조할 수 있는 작동원리때문에 발생하는,
중첩 함수가 외부 함수의 변수 및 함수에 접근할 수 있는 기능이자 그러한 구조를 갖고 있는 중첩 함수

이 정의를 이해하려면 우선 다음과 같은 개념을 먼저 파악하는 것이 중요하다.

  • 스코프
  • 실행컨텍스트

확실히 이해하려면 더 깊숙이 들어갈 수 있지만, 지금은 클로저가 핵심이니 깊이 들어가진 않을 것이다. 사담으로 실행컨텍스트는 작성 중에 있는데, 글 분량 조절이 안돼서 아직 못 올리고 있다. 😅

이에 대한 이해가 없다면 우선은 간단하게 다음과 같이 이해하면 된다.

  • 스코프: 코드 실행 단계에서 변수와 함수에 접근할 수 있는 범위
    (더 궁금하다면, let/const와 var 식별자 키워드의 차이에 대해 공부하면 좋을 것 같다.
  • 실행컨텍스트: 자바스크립트가 실행되고 있는 환경을 제공하고 관리하는 곳, 실행 순서와 스코프, 참조에 관한 정보를 갖고 있다.
    (이 부분은 지금 열심히 적고 있으니 완성되면 하이퍼링크로 첨부할 예정이다!)

(역시 CS는 파면 팔수록 공부할 게 나온다... 심심할 여력이 없다.)

아무튼 다시 클로저로 돌아오자.

자바스크립트의 실행컨텍스트의 구성 중에서 렉시컬 환경(lexical environment)이 있다.
렉시컬 환경은 식별자(변수와 함수)와 식별자에 연결된 값 그리고 상위 스코프에 대한 참조를 기록하는 자료구조이다.
이 중에서 클로저와 가장 연관이 있는 부분은 바로,
렉시컬 환경에서 상위 스코프에 대한 참조를 기록하는 곳인 외부 렉시컬 환경에 대한 참조(Outer Lexical Environment Reference)이다.

코드와 함께 보는 클로저

이게 뭔 말이야... 싶은 사람이 분명이 있을 것이다.
처음 예시 코드를 보면서 풀어쓰자면 다음과 같다.

const outerFunction = () => {
	const a = 10;
	return function(b) {
		console.log(a + b)
	}
}

const getValue = outerFunction()
getValue(20)
  1. 코드가 처음으로 실행될 때 전역 실행컨텍스트가 생성된다.
  2. outerFunction() 이 실행될 때, outerFunction 실행컨텍스트가 생성된다.
    1-1. outerFunction 실행컨텍스트 > 렉시컬 환경에 변수 a가 기록된다.
    1-2. outerFunction 실행컨텍스트 > 외부 렉시컬 환경에 대한 참조는 전역 스코프를 가리킨다.
  3. 전역 실행컨텍스트> 렉시컬 환경에 변수 getValue가 기록된다.
    2-1. getValue에 outerFunction의 return 값인 function(b) {console.log(a+b)}(function(b))가 할당된다.
  4. getValue(20)가 실행될 때, function(b) 의 실행컨텍스트가 생성된다.
    3-1. function(b) 실행컨텍스트 > 렉시컬 환경에 매개변수 b가 기록된다.
    3-2. function(b) 실행컨텍스트 > 외부 렉시컬 환경에 대한 참조는 outerFunction 렉시컬 환경을 가리킨다.

다시 한 번 더 정리하자면,
자바스크립트의 실행컨텍스트에서 코드가 평가되고 실행될 때 관련된 정보, 즉 변수와 함수, 상위 스코프를 기록하는데 이 중에서 외부 렉시컬 환경에 대한 참조에 의해 outerFuntion은 전역 스코프를, function(b)는 outerFunction 스코프를 가리킨다.

따라서 function(b)는 a에 대한 변수를 함수 스코프 내에서 선언하지 않았음에도 상위 스코프에 선언된 a를 가져다가 쓸 수 있는 것이다.

클로저는 이러한 결과를 통틀어서 말하는 것이고, 자바스크립트의 작동원리에 의하면 모든 실행컨텍스트는 외부 렉시컬 환경에 대한 참조를 가지고 있기 때문에 모든 함수가 곧 클로저가 될 수 있다. 하지만 외부 스코프를 참조하고 있지 않는 경우의 함수는 일반적으로 클로저라고 이야기하지 않는다.

그래서 앞서 말했던 정의는 위의 원리에 의해 다 동일한 것을 말하고 있다.

함수 내부의 중첩 함수의 외부 렉시컬 환경에 대한 참조는 곧 외부 함수 스코프를 가리키니, 이러한 함수와 중첩 함수의 (식별자를 서로 공유하고 참조하는) 관계라고 이야기할 수 있다.
또한 의도적으로 외부 스코프의 어떤 식별자를 가져오기 위해 정의할 수 있는데, 이렇게 할 수 있는 이유도 곧 앞서 말했던 외부 렉시컬 환경에 대한 참조때문이다.

그 중에서 생명 주기와 관련된 정의는 현재 글에서 완전히 이해하기 어려우니, 추후 실행컨텍스트에 대한 글에서 마저 이어나가도록 하겠다.

그래서 한 마디만 더 얹어서 정의를 마무리하자면 다음과 같다.

외부 렉시컬 환경에 대한 참조에 의해 자바스크립트에서 함수의 상위 스코프를 참조할 수 있는 작동원리때문에 발생하는,
중첩 함수가 외부 함수의 변수 및 함수에 접근할 수 있는 기능이자 그러한 구조를 갖고 있는 중첩 함수

왜 클로저는 중요한가?

클로저는 같은 스코프를 가지는 함수들 끼리 어떤 식별자를 공유하고 있는지에 따라 특정 함수의 스코프에 무엇이 있고, 무엇이 없는지 관리하기 때문에 중요하다.

더 나아가서 변수와 함수가 서로 어떻게 관계되어 있는지 이해하는 것은 함수형 프로그래밍 그리고 객체 지향 프로그래밍 스타일의 코드에 어떤 일이 있어나는지 파악하는 것과 이어져 있어서 중요하다.

클로저의 예시는 무엇이 있는가?

함수형 프로그래밍

함수형 프로그래밍 === 클로저가 아니다!
함수형 프로그래밍에서 클로저를 쓸 수 있다는 이야기이다.

❗ 변수 값은 누군가에 의해 언제든지 변경될 수 있어 오류 발생의 근본적 원인이 될 수 있다. 외부 상태 변경이나 가변 데이터를 피하고 불변성을 지향하는 함수형 프로그래밍에서 부수 효과를 최대한 억제하여 오류를 피하고 프로그램의 안정성을 높이기 위해 클로저는 적극적으로 사용된다.
(출처: 모던 자바스크립트 Deep Dive 405쪽)

예상 면접 문제

const makeCalculator = /** 구현하세요 */

function increase(a, b){
  return a + b
}

function multiply(a, b){
  return a * b
}

function decrease(a, b){
  return a - b
}

function divide(a, b){
  return a / b
}

console.log(makeCalculator(increase, 2)) // 2
console.log(makeCalculator(multiply, 6)) // 12
console.log(makeCalculator(divide, 4))   // 3
console.log(makeCalculator(decrease, 7)) // -4

const makeCalculator = (function() {
  let number = 0;
  return function (operator, n){
    number = operator(number, n)
    return number
  }
}());

캡슐화

❗ 캡슐화는 객체의 상태를 나타내는 프로퍼티와 프로퍼티를 참조하고 조작할 수 있는 동작인 메서드를 하나로 묶는 것을 말한다. 캡슐화는 객체의 특정 프로퍼티나 메서드를 감출 목적으로 사용하기도 하는데 이를 정보 은닉이라 한다.
(출처: 모던 자바스크립트 Deep Dive 409쪽)

예상 면접 문제 출처

const counter = /** 구현하세요 */
let c = counter();
c.add(5); 
c.add(9); 

c.getValue(); // 14

const counter = () => {
  let value = 0
  return {
    add: (n) => value += n,
    getValue: () => {console.log(value)}
  }
}

그 외 클로저를 이용한 라이브 코딩 면접 예시

다음의 실행 결과는?

(function(a){
  return (function(){
    console.log(a);
    a = 23;
  })()
})(45);

45

다음 multiply 함수를 구현하세요.

multiply(2, 4) // 8
multiply(3, 5) // 15

const double = multiply(2)
double(2) // 2
double(8) // 16

const hexa = multiply(6)
hexa(6) // 36
hexa(10) // 60

const multiply=(x1, x2)=> {
  if (x2){
    return x1 * x2
  }
  return  (n) => {
    return x1 * n
  }
}

마무리

누군가는 이렇게까지 알아야 하나라고 생각할 수도 있다.
물론 그건 개인의 선택이다.
하지만 언어의 작동원리를 파악하면, 여러가지를 설명할 수 있고,
클로저 뿐만 아니라 다른 효과들(호이스팅, this 바인딩 등)도 별개의 개념이 아니라 다 같은 작동원리에 의해 발생하는 것을 이해할 수 있을 것이다.

위 라이브 코딩 문제에 대한 해설 또한 별도로 글을 작성하여 첨부할 예정이니 먼저 원하는 문제가 있다면 댓글 부탁합니다!

그리고 이제는 면접에서 클로저 질문 나왔을 때 당황하지 말고 대답할 자신 있다!

참고 자료

profile
프론트엔드 기술 학습 및 공유를 활발하게 하기 위해 노력합니다.

0개의 댓글